前面有提到 node 最大的優點就是他能夠善用單執行緒非同步的優點,同時處理多個 IO Request,但這同時也是他的缺點。單執行序意味著無論有多少顆 CPU,我們一個時間也只能在一個 CPU 上去執行我們的程式,當請求一多,這無疑是個負擔。
不過 node 提供了 cluster 這個 module 可以幫助我們解決這個問題!讓我們可以同時擁有單執行緒非同步的效能,又能善用多個 CPU。
我們可以透過 node 本身提供的 node:cluster
這個 module 來建立 cluster。
Cluster 本身是一種從屬架構,當我們使用 node app.js
,會產生主行程 (main-process),而每當 main process 執行 cluster.fork();
則會產生子行程,並且每個 process 會有一個專屬的用來識別的 pid。
以官方程式碼為例,當我們用 node app.js
執行以下程式碼, cluster.isPrimary 屬性可以判斷目前執行的是不是 primary process。然後他使用了 for 迴圈,產生了與 cpu 數量相等的子行程,並設置事件監聽。
對於每個被產生的子行程,他們也會執行此段程式碼,不同的是他們會在 cluster.isPrimary
得到 false
,並且啟用在 8000 port 上進行監聽的 http 服務。
const cluster = require('node:cluster');
const http = require('node:http');
const numCPUs = require('node:os').cpus().length;
const process = require('node:process');
if (cluster.isPrimary) {
console.log(`Primary ${process.pid} is running`);
// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});
} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.writeHead(200);
res.end('hello world\n');
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
以上面的範例來講,看似有許多 process 同時監聽 8000 port,一般來講這會造成衝突。
在 cluster 的機制中,8000 port 統一為 main process 進行監聽,當他收到請求後,會再透過主副行程之間進行溝通的 IPC 通道,將 http 請求轉發給子行程(默認為 round-robin 的排程方式來分配請求給子行程)。
我們說過,cluster 的主副行程之間會使用 IPC channel 來進行溝通,那麼如何完成這件事呢?
我們只要分別在主副行程分別使用 work.on('message', callback)
和 process.on('message', callback)
,並且使用 process.send(msg)
,便可以完成在主副行程間傳遞資料。
例如這個範例,當有請求進入 http://127.0.0.1:8000 ,便可以讓他們開始尬聊。
var cluster = require('cluster');
var http = require('http');
const numCPUs = require('node:os').cpus().length;
var worker;
if (cluster.isMaster) {
for (var i = 0; i < numCPUs; i++) {
worker = cluster.fork();
worker.on('message', function(msg) {
if (msg) {
console.log(msg);
worker.send(`${process.pid}: hello worker`);
}
});
}
} else {
process.on('message', function(msg) {
if (msg) {
console.log(msg)
}
});
http.Server(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
process.send(`${process.pid}: hello master`);
}).listen(8000);
console.log(`Worker ${process.pid} started`);
}
另外,除了 IPC 以外,通常 process 之間也會使用例如 Redis, Kafka or RabbitMQ,這些工具來達到資料共享或是彼此溝通的功能。
今天的文章就到這邊。
明天我們來認識一下另一個可以放我們輕鬆使 node 完成 multi-process 服務的另一種工具,PM2。