Node之可擴展性 --- Node的Cluster
nodejs
Lastmod: 2019-12-15

本篇文章中將要說明,要如何的擴展 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);
    });

然後會監聽clusterexit,該事件代表如果任何一個 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軸的擴展 : 複制的方法進行討論。

參考資料

comments powered by Disqus