如何建立一個可靠穩定的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.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