在nodejs中建立cluster

flydean發表於2021-01-31

在nodejs中建立cluster

簡介

在前面的文章中,我們講到了可以通過worker_threads來建立新的執行緒,可以使用child_process來建立新的子程式。本文將會介紹如何建立nodejs的叢集cluster。

cluster叢集

我們知道,nodejs的event loop或者說事件響應處理器是單執行緒的,但是現在的CPU基本上都是多核的,為了充分利用現代CPU多核的特性,我們可以建立cluster,從而使多個子程式來共享同一個伺服器埠。

也就是說,通過cluster,我們可以使用多個子程式來服務處理同一個埠的請求。

先看一個簡單的http server中使用cluster的例子:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主程式 ${process.pid} 正在執行`);

  // 衍生工作程式。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作程式 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作程式可以共享任何 TCP 連線。
  // 在本例子中,共享的是 HTTP 伺服器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('你好世界\n');
  }).listen(8000);

  console.log(`工作程式 ${process.pid} 已啟動`);
}

cluster詳解

cluster模組源自於lib/cluster.js,我們可以通過cluster.fork()來建立子工作程式,用來處理主程式的請求。

cluster中的event

cluster繼承自events.EventEmitter,所以cluster可以傳送和接收event。

cluster支援7中event,分別是disconnect,exit,fork,listening,message,online和setup。

在講解disconnect之前,我們先介紹一個概念叫做IPC,IPC的全稱是Inter-Process Communication,也就是程式間通訊。

IPC主要用來進行主程式和子程式之間的通訊。一個工作程式在建立後會自動連線到它的主程式。 當 'disconnect' 事件被觸發時才會斷開連線。

觸發disconnect事情的原因有很多,可以是主動呼叫worker.disconnect(),也可以是工作程式退出或者被kill掉。

cluster.on('disconnect', (worker) => {
  console.log(`工作程式 #${worker.id} 已斷開連線`);
});

exit事件會在任何一個工作程式關閉的時候觸發。一般用來監測cluster中某一個程式是否異常退出,如果退出的話使用cluster.fork建立新的程式,以保證有足夠多的程式來處理請求。

cluster.on('exit', (worker, code, signal) => {
  console.log('工作程式 %d 關閉 (%s). 重啟中...',
              worker.process.pid, signal || code);
  cluster.fork();
});

fork事件會在呼叫cluster.fork方法的時候被觸發。

const timeouts = [];
function errorMsg() {
  console.error('連線出錯');
}

cluster.on('fork', (worker) => {
  timeouts[worker.id] = setTimeout(errorMsg, 2000);
});

主程式和工作程式的listening事件都會在工作程式呼叫listen方法的時候觸發。

cluster.on('listening', (worker, address) => {
  console.log(
    `工作程式已連線到 ${address.address}:${address.port}`);
});

其中worker代表的是工作執行緒,而address中包含三個屬性:address、 port 和 addressType。 其中addressType有四個可選值:

  • 4 (TCPv4)
  • 6 (TCPv6)
  • -1 (Unix 域 socket)
  • 'udp4' or 'udp6' (UDP v4 或 v6)

message事件會在主程式收到子程式傳送的訊息時候觸發。

當主程式生成工作程式時會觸發fork,當工作程式執行時會觸發online。

setupMaster方法被呼叫的時候,會觸發setup事件。

cluster中的方法

cluster中三個方法,分別是disconnect,fork和setupMaster。

cluster.disconnect([callback])

呼叫cluster的disconnect方法,實際上會在cluster中的每個worker中呼叫disconnect方法。從而斷開worker和主程式的連線。

當所有的worker都斷開連線之後,會執行callback。

cluster.fork([env])

fork方法,會從主程式中建立新的子程式。其中env是要新增到程式環境變數的鍵值對。

fork將會返回一個cluster.Worker物件,代表工作程式。

最後一個方法是setupMaster:

cluster.setupMaster([settings])

預設情況下,cluster通過fork方法來建立子程式,但是我們可以通過setupMaster來改變這個行為。通過設定settings變數,我們可以改變後面fork子程式的行為。

我們看一個setupMaster的例子:

const cluster = require('cluster');
cluster.setupMaster({
  exec: 'worker.js',
  args: ['--use', 'https'],
  silent: true
});
cluster.fork(); // https 工作程式
cluster.setupMaster({
  exec: 'worker.js',
  args: ['--use', 'http']
});
cluster.fork(); // http 工作程式

cluster中的屬性

通過cluster物件,我們可以通過isMaster和isWorker來判斷程式是否主程式。

可以通過worker來獲取當前工作程式物件的引用:

const cluster = require('cluster');

if (cluster.isMaster) {
  console.log('這是主程式');
  cluster.fork();
  cluster.fork();
} else if (cluster.isWorker) {
  console.log(`這是工作程式 #${cluster.worker.id}`);
}

可以通過workers來遍歷活躍的工作程式物件:

// 遍歷所有工作程式。
function eachWorker(callback) {
  for (const id in cluster.workers) {
    callback(cluster.workers[id]);
  }
}
eachWorker((worker) => {
  worker.send('通知所有工作程式');
});

每個worker都有一個id編號,用來定位該worker。

cluster中的worker

worker類中包含了關於工作程式的所有的公共的資訊和方法。cluster.fork出來的就是worker物件。

worker的事件和cluster的很類似,支援6個事件:disconnect,error,exit,listening,message和online。

worker中包含3個屬性,分別是:id,process和exitedAfterDisconnect。

其中id是worker的唯一標記。

worker中的process,實際上是ChildProcess物件,是通過child_process.fork()來建立出來的。

因為在worker中,process屬於全域性變數,所以我們可以直接在worker中使用process來進行傳送訊息。

exitedAfterDisconnect表示如果工作程式由於 .kill() 或 .disconnect() 而退出的話,值就是true。如果是以其他方式退出的話,返回值就是false。如果工作程式尚未退出,則為 undefined。

我們可以通過worker.exitedAfterDisconnect 來區分是主動退出還是被動退出,主程式可以根據這個值決定是否重新生成工作程式。

cluster.on('exit', (worker, code, signal) => {
  if (worker.exitedAfterDisconnect === true) {
    console.log('這是自發退出,無需擔心');
  }
});

// 殺死工作程式。
worker.kill();

worker還支援6個方法,分別是:send,kill,destroy,disconnect,isConnected,isDead。

這裡我們主要講解一下send方法來傳送訊息:

worker.send(message[, sendHandle[, options]][, callback])

可以看到send方法和child_process中的send方法引數其實是很類似的。而本質上,worker.send在主程式中,這會傳送訊息給特定的工作程式。 相當於 ChildProcess.send()。在工作程式中,這會傳送訊息給主程式。 相當於 process.send()。

if (cluster.isMaster) {
  const worker = cluster.fork();
  worker.send('你好');

} else if (cluster.isWorker) {
  process.on('message', (msg) => {
    process.send(msg);
  });
}

在上面的例子中,如果是在主程式中,那麼可以使用worker.send來傳送訊息。而在子程式中,則可以使用worker中的全域性變數process來傳送訊息。

總結

使用cluster可以充分使用多核CPU的優勢,希望大家在實際的專案中應用起來。

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/nodejs-cluster/

本文來源:flydean的部落格

歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

相關文章