延續上篇文章騷年,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