1. 程式人生 > >如何建立一個可靠穩定的Web伺服器

如何建立一個可靠穩定的Web伺服器

延續上篇文章騷年,Koa和Webpack瞭解一下?

本篇文章主要講述的是如何通過Node建立一個穩定的web伺服器,如果你看到這裡想起了pm2等工具,那麼你可以先拋棄pm2,進來看看,如果有哪些不合適的地方,懇請您指出。

建立一個穩定的web伺服器需要解決什麼問題。

  • 如何利用多核CPU資源。
  • 多個工作程序的存活狀態管理。
  • 工作程序的平滑重啟。
  • 程序錯誤處理。
  • 工作程序限量重啟。

如何利用多核CPU資源

利用多核CPU資源有多種解決辦法。

  • 通過在單機上部署多個Node服務,然後監聽不同埠,通過一臺Nginx負載均衡。

    這種做法一般用於多臺機器,在伺服器叢集時,採用這種做法,這裡我們不採用。

  • 通過單機啟動一個master程序,然後fork多個子程序,master程序傳送控制代碼給子程序後,關閉監聽埠,讓子程序來處理請求。

    這種做法也是Node單機叢集普遍的做法。

所幸的是,Node在v0.8版本新增的cluster模組,讓我們不必使用child_process一步一步的去處理Node叢集這麼多細節。

所以本篇文章講述的是基於cluster模組解決上述的問題。

首先建立一個Web伺服器,Node端採用的是Koa框架。沒有使用過的可以先去看下 ===> 傳送門

下面的程式碼是建立一個基本的web服務需要的配置,看過上篇文章的可以先直接過濾這塊程式碼,直接看後面。

const Koa = require('koa');
const app = new Koa();
const koaNunjucks = require('koa-nunjucks-2');
const koaStatic = require('koa-static');
const KoaRouter = require('koa-router');
const router = new KoaRouter();
const path = require('path');
const colors = require('colors');
const compress = require
('koa-compress'); const AngelLogger = require('../angel-logger') const cluster = require('cluster'); const http = require('http'); class AngelConfig { constructor(options) { this.config = require(options.configUrl); this.app = app; this.router = require(options.routerUrl); this.setDefaultConfig(); this.setServerConfig(); } setDefaultConfig() { //靜態檔案根目錄 this.config.root = this.config.root ? this.config.root : path.join(process.cwd(), 'app/static'); //預設靜態配置 this.config.static = this.config.static ? this.config.static : {}; } setServerConfig() { this.port = this.config.listen.port; //cookie簽名驗證 this.app.keys = this.config.keys ? this.config.keys : this.app.keys; } } //啟動伺服器 class AngelServer extends AngelConfig { constructor(options) { super(options); this.startService(); } startService() { //開啟gzip壓縮 this.app.use(compress(this.config.compress)); //模板語法 this.app.use(koaNunjucks({ ext: 'html', path: path.join(process.cwd(), 'app/views'), nunjucksConfig: { trimBlocks: true } })); this.app.use(async (ctx, next) => { ctx.logger = new AngelLogger().logger; await next(); }) //訪問日誌 this.app.use(async (ctx, next) => { await next(); // console.log(ctx.logger,'loggerloggerlogger'); const rt = ctx.response.get('X-Response-Time'); ctx.logger.info(`angel ${ctx.method}`.green,` ${ctx.url} - `,`${rt}`.green); }); // 響應時間 this.app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; ctx.set('X-Response-Time', `${ms}ms`); }); this.app.use(router.routes()) .use(router.allowedMethods()); // 靜態資源 this.app.use(koaStatic(this.config.root, this.config.static)); // 啟動伺服器 this.server = this.app.listen(this.port, () => { console.log(`當前伺服器已經啟動,請訪問`,`http://127.0.0.1:${this.port}`.green); this.router({ router, config: this.config, app: this.app }); }); } } module.exports = AngelServer; 複製程式碼

在啟動伺服器之後,將this.app.listen賦值給this.server,後面會用到。

一般我們做單機叢集時,我們fork的程序數量是機器的CPU數量。當然更多也不限定,只是一般不推薦。

const cluster = require('cluster');
const { cpus } = require('os'); 
const AngelServer = require('../server/index.js');
const path = require('path');
let cpusNum = cpus().length;

//超時
let timeout = null;

//重啟次數
let limit = 10;
// 時間
let during = 60000;
let restart = [];

//master程序
if(cluster.isMaster) {
  //fork多個工作程序
  for(let i = 0; i < cpusNum; i++) {
    creatServer();
  }

} else {
  //worker程序
  let angelServer = new AngelServer({
    routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址
    configUrl: path.join(process.cwd(), 'config/config.default.js')  
    //預設讀取config/config.default.js
  });
}

// master.js
//建立服務程序  
function creatServer() {
  let worker = cluster.fork();
  console.log(`工作程序已經重啟pid: ${worker.process.pid}`);
}

複製程式碼

使用程序的方式,其實就是通過cluster.isMastercluster.isWorker來進行判斷的。

主從程序程式碼寫在一塊可能也不太好理解。這種寫法也是Node官方的寫法,當然也有更加清晰的寫法,藉助cluster.setupMaster實現,這裡不去詳細解釋。

通過Node執行程式碼,看看究竟發生了什麼。

首先判斷cluster.isMaster是否存在,然後迴圈呼叫createServer(),fork4個工作程序。列印工作程序pid

cluster啟動時,它會在內部啟動TCP服務,在cluster.fork()子程序時,將這個TCP服務端socket的檔案描述符傳送給工作程序。如果工作程序中存在listen()監聽網路埠的呼叫,它將拿到該檔案的檔案描述符,通過SO_REUSEADDR埠重用,從而實現多個子程序共享埠。

程序管理、平滑重啟、和錯誤處理。

一般來說,master程序比較穩定,工作程序並不是太穩定。

因為工作程序處理的是業務邏輯,因此,我們需要給工作程序新增自動重啟的功能,也就是如果子程序因為業務中不可控的原因報錯了,而且阻塞了,此時,我們應該停止該程序接收任何請求,然後優雅的關閉該工作程序。

//超時
let timeout = null;

//重啟次數
let limit = 10;
// 時間
let during = 60000;
let restart = [];

if(cluster.isMaster) {
  //fork多個工作程序
  for(let i = 0; i < cpusNum; i++) {
    creatServer();
  }

} else {
  //worker
  let angelServer = new AngelServer({
    routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址
    configUrl: path.join(process.cwd(), 'config/config.default.js') //預設讀取config/config.default.js
  });

  //伺服器優雅退出
  angelServer.app.on('error', err => {
    //傳送一個自殺訊號
    process.send({ act: 'suicide' });
    cluster.worker.disconnect();
    angelServer.server.close(() => {
      //所有已有連線斷開後,退出程序
      process.exit(1);
    });
    //5秒後退出程序
    timeout = setTimeout(() => {
      process.exit(1);
    },5000);
  });
}

// master.js
//建立服務程序  
function creatServer() {

  let worker = cluster.fork();
  console.log(`工作程序已經重啟pid: ${worker.process.pid}`);
  //監聽message事件,監聽自殺訊號,如果有子程序傳送自殺訊號,則立即重啟程序。
  //平滑重啟 重啟在前,自殺在後。
  worker.on('message', (msg) => {
    //msg為自殺訊號,則重啟程序
    if(msg.act == 'suicide') {
      creatServer();
    }
  });

  //清理定時器。
  worker.on('disconnect', () => {
    clearTimeout(timeout);
  });

}

複製程式碼

我們在例項化AngelServer後,得到angelServer,通過拿到angelServer.app拿到Koa的例項,從而監聽Koa的error事件。

當監聽到錯誤發生時,傳送一個自殺訊號process.send({ act: 'suicide' })。 呼叫cluster.worker.disconnect()方法,呼叫此方法會關閉所有的server,並等待這些server的 'close'事件執行,然後關閉IPC管道。

呼叫angelServer.server.close()方法,當所有連線都關閉後,通往該工作程序的IPC管道將會關閉,允許工作程序優雅地死掉。

如果5s的時間還沒有退出程序,此時,5s後將強制關閉該程序。

Koa的app.listenhttp.createServer(app.callback()).listen();的語法糖,因此可以呼叫close方法。

worker監聽message,如果是該訊號,此時先重啟新的程序。 同時監聽disconnect事件,清理定時器。

正常來說,我們應該監聽processuncaughtException事件,如果 Javascript 未捕獲的異常,沿著程式碼呼叫路徑反向傳遞迴事件迴圈,會觸發 'uncaughtException' 事件。

但是Koa已經在middleware外邊加了tryCatch。因此在uncaughtException捕獲不到。

在這裡,還得特別感謝下大深海老哥,深夜裡,在群裡給我指點迷津。

限量重啟

通過自殺訊號告知主程序可以使新連線總是有程序服務,但是依然還是有極端的情況。 工作程序不能無限制的被頻繁重啟。

因此在單位時間規定只能重啟多少次,超過限制就觸發giveup事件。

//檢查啟動次數是否太過頻繁,超過一定次數,重新啟動。
function isRestartNum() {

  //記錄重啟的時間
  let time = Date.now();
  let length = restart.push(time);
  if(length > limit) {
    //取出最後10個
    restart = restart.slice(limit * -1);
  }
  //1分鐘重啟的次數是否太過頻繁
  return restart.length >= limit && restart[restart.length - 1] - restart[0] < during;
}

複製程式碼

同時將createServer修改成

// master.js
//建立服務程序  
function creatServer() {
  //檢查啟動是否太過頻繁
  if(isRestartNum()) {
    process.emit('giveup', length, during);
    return;
  }
  let worker = cluster.fork();
  console.log(`工作程序已經重啟pid: ${worker.process.pid}`);
  //監聽message事件,監聽自殺訊號,如果有子程序傳送自殺訊號,則立即重啟程序。
  //平滑重啟 重啟在前,自殺在後。
  worker.on('message', (msg) => {
    //msg為自殺訊號,則重啟程序
    if(msg.act == 'suicide') {
      creatServer();
    }
  });
  //清理定時器。
  worker.on('disconnect', () => {
    clearTimeout(timeout);
  });

}

複製程式碼

更改負載均衡策略

預設的是作業系統搶佔式,就是在一堆工作程序中,閒著的程序對到來的請求進行爭搶,誰搶到誰服務。

對於是否繁忙是由CPU和I/O決定的,但是影響搶佔的是CPU。

對於不同的業務,會有的I/O繁忙,但CPU空閒的情況,這時會造成負載不均衡的情況。
因此我們使用node的另一種策略,名為輪叫制度。

cluster.schedulingPolicy = cluster.SCHED_RR;
複製程式碼

最後

當然建立一個穩定的web服務還需要注意很多地方,比如優化處理程序之間的通訊,資料共享等等。

本片文章只是給大家一個參考,如果有哪些地方寫的不合適的地方,懇請您指出。

完整程式碼請見github

參考資料:深入淺出nodejs