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

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

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

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

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

如何利用多核CPU資源

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

  • 經過在單機上部署多個Node服務,而後監聽不一樣端口,經過一臺Nginx負載均衡。html

    這種作法通常用於多臺機器,在服務器集羣時,採用這種作法,這裏咱們不採用。java

  • 經過單機啓動一個master進程,而後fork多個子進程,master進程發送句柄給子進程後,關閉監聽端口,讓子進程來處理請求。node

    這種作法也是Node單機集羣廣泛的作法。git

所幸的是,Node在v0.8版本新增的cluster模塊,讓咱們沒必要使用child_process一步一步的去處理Node集羣這麼多細節。github

因此本篇文章講述的是基於cluster模塊解決上述的問題。web

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

下面的代碼是建立一個基本的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

相關文章
相關標籤/搜索