本篇文章中將要說明,要如何的擴展 node 應用
,從上一篇文章中我們知道, node 它很適合高 I/O
的任務,而不適合高 cpu 的
任務,最主要的原因在於它的架構,它是單執行緒架構
,但是無論單體的伺服器能力在強大,單一執行緒的效能一定會有界限,因此我們必須將應用程式擴展運用。
根據The Art of Scalabiltiy
的內容來知道,在擴展時,可以用下列三個維度來描述可擴展性。這也是被稱為擴展立方(scale cube)
的東東。
- X 軸 : 複制
- Y 軸 : 以服務/功能分解
- Z 軸 : 以資料來分解
基本上Y軸
擴展的方法是屬於微服務(Microservices)
的範圍所以本篇也不詳細說明,而Z
軸則屬於資料庫
方法所以也不加以說明。
我們本篇將要說明X軸 : 複制
,它的白話文概念如下 :
將應用程式加以複制 N 個,這也代表每個實體只須處理 N 分之一的工作量。
傳統的系統可以利用多執行緒,來完整使用整台機器的效能,但 node 則否,因為它是單一執行緒,並且在 64 位元下有1.7GB
的限制,接下來我們將介紹 node 擴展的基本機制 cluster
。
cluster
cluster
是在 node 中的內建模組,他讓我們可以建立一個 cluster,可通過父進程來管理一堆子進程,在 cluster 中父進程被稱為master process
,而子進程則被稱為worker process
。
每個傳送的連線都會先到master process
然後會在將工作分配到worker process
中。
我們根據上一篇的程式碼來進行修改。下面程式碼中,首先請先看if(cluster.isMaster)
裡面,當執行時,會使用cluster fork
根據 cpu 的數量來新增 process,然後每次fork
時都會執行else
裡面的程式。
const http = require('http');
const child_process = require('child_process');
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log("master process:" + process.pid);
console.log("cpu num: " + numCPUs.toString());
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
http.createServer(function (req, res) {
console.log("process run:" + process.pid);
res.writeHead(200);
res.write(fib(40).toString());
res.end();
}).listen(8000, function () {
console.log('started');
console.log("process:" + process.pid);
});
}
function fib(n) {
return n > 1 ? fib(n - 1) + fib(n - 2) : 1;
}
而我們的執行輸出的結果可以看到,我們的master process
的 pid 為95199
,其餘四個 worker process
的 pid 如下。接下來我們每次打這隻 api 時,會直接從這 4 個 process 中選一個出來執行。
master process:95199
cpu num: 4
started
process:95202
started
process:95200
started
process:95201
started
process:95203
那 node 他是如何決定要用那個 process 呢 ?
自版本0.11.2
時變導入了一個循環式負載平衡演算法
,它的基本概念就是輪流
平均的分配所有可用伺服器的負載。
那我們要著麼樣相互溝通了呢 ?
這個事實上在上一章節有提,process
間的相互溝通主要使用IPC
的方法,而在 cluster 中因為每個 worker process 的產生都是使用 child_process.fork()
來產生,所以相對的他也有提供message
來讓我們進行溝通。
那我們為什麼不直接用child_process
呢 ?
答案是方便,多進程的運行,我們同時還需要考慮到進程通信
、子進程管理
、負載均衡
等問題,雖然child_process
可以自已寫程式來處理,但cluster
就已經幫我們處理好了,為何不直接拿來用呢 ? 對吧。
如果有一個 process 掛掉了會如何 ?
在某些時後,如果某個 process 掛掉了,會如何呢 ? 當然不會著麼樣,只要有處理的話。
cluster
當然有考慮到這點,這種功能事實上在可擴展性上很重要,我們簡單的寫段程式碼,讓某個 process 來個隨機掛點,如下程式碼,大約每幾秒鐘就會 error 一次。
http.createServer(function (req, res) {
....
}).listen(8000, function () {
setTimeout(function () {
throw new Error('Ooops');
}, Math.ceil(Math.random() * 3) * 10000);
});
然後會監聽cluster
的exit
,該事件代表如果任何一個 worker 離開該master process
則會觸發。當我們發生事件時,會先判斷是否錯誤,如果是的話,則在fork()
一個worker
。
雖然掛掉的 worker 可能還在重新建立,但是不會影響到我們應用程式的使用。
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
var worker = cluster.fork();
cluster.on('exit', function (worker, code) {
if (code != 0 && !worker.suicide) {
console.log('Worker crashed. Starting a new worker');
cluster.fork();
}
})
}
}
我想更新應該程式但不想停機
在實務上,某些大型的應該程式是 24 X 7 的在跑,就算是更新也不能停機,所以要著麼解決呢 ? 可行的解決方案是實作 :
零停機時間的重啟
比較白話文的來說明實作過程就是 :
一次只重新啟動一個 worker ,其餘的繼續工作
我們實作的方法參考Miario Casciaro 的 nodejs設計模式一日
,首先我們會在SIGUSR2
中設置監聽器,當接受到 SIGUSR2 信號時會一個一個將 worker 重新啟動。
其中我們有使用 unix 信號,它也是一樣 IPC 的方法,它是一種異歲的通知機制,主要用來和某個 process 說一個事情已經被發生。
if (cluster.isMaster) {
console.log("master process:" + process.pid);
console.log("cpu num: " + numCPUs.toString());
process.on('SIGUSR2', function () {
console.log("Received SIGUSR2 from system");
console.log("Restarting workers");
var workers = Object.keys(cluster.workers);
function restartWorker(i){
if ( i >= workers.length) return;
var worker = cluster.workers[workers[i]];
console.log('Stopping worker:' + worker.process.pid);
worker.disconnect();
worker.on('exit', function () {
if(!worker.exitedAfterDisconnect) return;
var newWorker = cluster.fork();
newWorker.on('listening',function () {
restartWorker(i+1);
})
})
}
restartWorker(0);
})
}
為了要模擬這種狀態,我們需要使用下面指令,來 kill 掉我們master process
,然後當執行這行時,就會執行process.on('SIGUSR2')
裡面的指令開啟重新的一個一個啟動 worker。
kill -SIGUSR2 <PID>
結果如下。
Restarting workers
Stopping worker:14239
started
process:14249
Stopping worker:14240
started
process:14255
Stopping worker:14241
started
process:14256
Stopping worker:14242
started
process:14257
不過除了上面自已寫以外,當然還有其它的東西可以完成這項工作。
那就是 forever 或 pm2
這套工具最主要的功用是持繼的保持後台的運作
。
就算你的程式發生錯誤,他也會自動的幫你重新啟動,而且就像我們上面的說的,要更新應用程式時,他也會保持系統的持繼運作。
他的用法很簡單。
npm install -g forever
然後在執行下面指令,這樣就完成了。
forever start xxxx.js
結論
嚴格來說本篇文章大部份都針對* X 軸 : 複制
的方法來進行說明,本篇中所提到的 cluster,就是用來複制的方法之一,但這只是之一,在傳統上也有一些技巧更常被使用到的,那就是在不同 port 或不同機器上啟動應用程式的多個實例,然後在使用一個反向代理器
來處理,下一篇文章中我們將會繼續對於X軸的擴展 : 複制
的方法進行討論。