延續上篇文章騷年,Koa和Webpack瞭解一下?javascript
本篇文章主要講述的是如何經過Node建立一個穩定的web服務器,若是你看到這裏想起了pm2等工具,那麼你能夠先拋棄pm2,進來看看,若是有哪些不合適的地方,懇請您指出。css
- 如何利用多核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.isMaster
和cluster.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.listen
是http.createServer(app.callback()).listen();
的語法糖,所以能夠調用close方法。
worker監聽message
,若是是該信號,此時先重啓新的進程。 同時監聽disconnect
事件,清理定時器。
正常來講,咱們應該監聽process
的uncaughtException
事件,若是 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