Cluster(集群)[翻譯]
一個Node.js實例運行在一個線程里。為了利用系統多核的優勢,用戶有些時候想通過啟動一群Node.js進程去處理負載。
cluster模塊可以輕鬆的創建共享相同埠的子進程。
const cluster = require(cluster);const http = require(http);const numCPUs = require(os).cpus().length;if (cluster.isMaster) {console.log(`Master ${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 serverhttp.createServer((req, res) => {res.writeHead(200);res.end(hello world
);}).listen(8000);console.log(`Worker ${process.pid} started`);}
現在運行Node.js工作進程之間將分享8000埠:
$ node server.jsMaster 3596 is runningWorker 4324 startedWorker 4520 startedWorker 6056 startedWorker 5644 started
請注意,Windows系統中的工作進程上現在還不能設置一個命名管道伺服器。
它是如何工作的
child_process.fork()方法產生工作進程,所以它們可以跟父進程通過IPC通信,並且來回傳遞服務句柄。
cluster模塊支持兩種分配傳入連接。
第一種方式(除了Windows外默認的方式)是循環法,主進程監聽一個埠接收新的連接,通過循環的方式分配給子進程,並且通過其中的一些內置智能避免子進程過載。
第二種方式是主進程創建監聽的socket,並且把它傳遞給感興趣的工作進程。然後,工作進程直接接收過來的連接。
理論上,第二種方式能提供最好的性能。但是在實踐中,由於操作系統調度程序變化莫測,導致分配十分不均衡。在8個進程中其中的兩個進程載入了70%的連接。
因為server.listen()把絕大多數工作都交給了主進程,因此在一個普通的Node.js進程和一個集群工作進程(a cluster worker)之間有三個不同點:
1、server.listen({fd: 7}) 因為消息被傳遞給主進程,在父進程中文件描述符7將會被監聽,並且將該句柄傳遞到工作進程,而不是監聽工作進程對文件描述符7引用的意圖。
2、server.listen(handle) 明確的監聽句柄使工作進程使用提供的句柄,而不是跟主進程對話。
3、server.listen(0) 通常情況下,這會使伺服器監聽任意一個介面。然而,在一個集群中使用listen(0)時,每一個工作進程都會接收到相同的「隨機」埠。本質上,埠第一時間是隨機的,但是以後是可以預見的。如果要監聽一個唯一埠,可以根據集群工作進程ID來生成一個埠號。
Node.js沒有提供路由邏輯。因此設計一個不過渡依賴像session和登錄這樣的內存數據對象的應用非常重要。
因為每個工作進程都是獨立的,它們可以在不影響其它工作進程的情況下,根據程序需要被殺死或者重新生成。只要有工作進程存活,伺服器就會繼續接收連接。如果沒有存活的工作進程,已有的連接將會被丟棄,新的連接將會被拒絕。然而,Node.js並不會自動管理工作進程的數量。應用程序根據自己的需求管理工作進程池。
雖然cluster模塊的主要應用場景是網路,但它同樣可以應用在其它需要工作進程的地方。
Class: Worker
在v0.7.0添加
一個Worker對象包含了所有worker的公共信息和方法。在主進程中可以使用cluster.workers。在工作進程中可以使用cluster.worker。
Event:』disconnect』
在v0.7.7添加
跟cluster.on(『disconnect』)事件相同,但是它是特定屬於該工作者。
cluster.fork().on(disconnect, () => {// Worker has disconnected});
Event:』error』
在v0.7.3添加
該事件跟child_process.fork()提供的』error』事件相同。
在一個工作進程中,process.on(『error』)可能被使用。
Event:』exit』
在v0.11.2
- code <number> 如果正常退出的退出碼。
- signal <string> 導致進程被殺死的信號的名稱(例如:』SIGHUP』)。
跟cluster.on(『exit』)相同,但是只針對這一個工作進程。
const worker = cluster.fork();worker.on(exit, (code, signal) => {if (signal) {console.log(`worker was killed by signal: ${signal}`);} else if (code !== 0) {console.log(`worker exited with error code: ${code}`);} else {console.log(worker success!);}});
Event:』listening』
在v0.7.0添加
- address <Object>
跟cluster.on(『listening』)事件相同,但是只針對這個工作進程。
cluster.fork().on(listening, (address) => {// Worker is listening});
在工作進程中不會發射。
Event:』message』
在v0.7.0添加
- message <Object>
- handle <undefined> | <Object>
跟cluster的』message』事件相同,但是只針對這個工作進程。
在一個工作進程里,process.on(『message』)也可以被使用。
參考 process event: 『message』。
例子,下面是一個集群,它在主進程中使用消息系統保持對請求數量的統計:
const cluster = require(cluster);const http = require(http);if (cluster.isMaster) {// Keep track of http requestslet numReqs = 0;setInterval(() => {console.log(`numReqs = ${numReqs}`);}, 1000);// Count requestsfunction messageHandler(msg) {if (msg.cmd && msg.cmd === notifyRequest) {numReqs += 1;}}// Start workers and listen for messages containing notifyRequestconst numCPUs = require(os).cpus().length;for (let i = 0; i < numCPUs; i++) {cluster.fork();}for (const id in cluster.workers) {cluster.workers[id].on(message, messageHandler);}} else {// Worker processes have a http server.http.Server((req, res) => {res.writeHead(200);res.end(hello world
);// notify master about the requestprocess.send({ cmd: notifyRequest });}).listen(8000);}
Event:』online』
在v0.7.0添加
跟cluster.on(『online』)事件一樣,但是只針對這個工作進程。
cluster.fork().on(online, () => {// Worker is online});
在工作進程中不發射。
worker.disconnect()
歷史
v7.3.0 現在這個方法返回一個worker的引用。
v0.7.7 在v0.7.7添加
- 返回:<cluster.Worker> 一個worker的引用。
在工作進程中,這個函數會關閉所有服務,在所有這些服務中等待』close』事件,然後斷開IPC通道。
導致.exitedAfterDisconnect被設置。
注意,當一個服務關閉後,它將不會接收新的連接,但是連接可以被其它工作進程接收。已存在的連接允許正常關閉。當沒有連接存在時(參考server.close()),工作進程的IPC通道會關閉從而允許工作進程優雅退出。
上述的內容只適用於伺服器連接,客戶端連接不會被工作進程自動關閉,並且在退出之前斷開並不會等它們關閉。
注意在工作進程中,雖然有process.disconnect,但是並不是這個函數,而是disconnect[subprocess.disconnect()]。
因為長時間存活的服務連接可能會阻止工作進程斷開,可能發送一個消息比較有用,以便應用的特定動作可能會被用來關閉它們。殺死工作進程時,如果很長時間後都沒有發射』disconnect』事件,所以實現超時也可能有用。
if (cluster.isMaster) {const worker = cluster.fork();let timeout;worker.on(listening, (address) => {worker.send(shutdown);worker.disconnect();timeout = setTimeout(() => {worker.kill();}, 2000);});worker.on(disconnect, () => {clearTimeout(timeout);});} else if (cluster.isWorker) {const net = require(net);const server = net.createServer((socket) => {// connections never end});server.listen(8000);process.on(message, (msg) => {if (msg === shutdown) {// initiate graceful close of any connections to server}});}
worker.exitedAfterDisconnect
在v6.0.0添加
- <boolean>
調用.kill()或者.disconnect()時被設置。再次之前都是undefined。
worker.exitedAfterDisconnect這個布爾值可以區分是自願退出還是異常退出,主進程可以根據這個值來重新生成工作進程。
cluster.on(exit, (worker, code, signal) => {if (worker.exitedAfterDisconnect === true) {console.log(Oh, it was just voluntary – no need to worry);}});// kill workerworker.kill();
worker.id
在v0.8.0添加
- <number>
每一個工作進程都有一個唯一的ID,這個ID被存儲在**id**里。
當進程存活時,它是在cluster.workers中查找工作進程的索引。
worker.isConnected()
在v0.11.14添加
當工作進程是通過IPC通道連接的主進程,這個函數返回true,否則返回false。當工作進程被創建後,它被連接到主進程。當』disconnect』事件被發射後斷開連接。
worker.isDead()
在v0.11.14添加
當工作進程終止(不管是因為退出或者信號傳遞)後該函數返回true。否則,返回false。
worker.kill([signal=『SIGTERM』])
在v0.9.12添加
- signal <string> 發送給工作進程的kill信號量的名字。
這個方法將會殺死工作進程。在主進程中,通過關閉worker.process的連接來實現,一旦關閉連接,然後通過signal殺死。在工作進程中,通過關閉通道連接實現,然後以0退出碼退出。
導致.exitedAfterDisconnect被設置。
此方法通過別名worker.destroy()向後兼容。
注意,在工作進程中存在process.kill(),但是它不是這個功能,它的功能是kill [process.kill(pid[, signal])]。
worker.process
在v0.7.0添加
- <ChildProcess>
所有的工作進程都是使用child_process.fork()創建,這個函數返回的對象被存儲為.process。在子進程中,全局process已經被存儲。
參考: Child Process module
注意,如果process上發生』disconnect』事件並且.exitedAfterDisconnect不是true,工作進程會調用process.exit(0)。這可以防止意外斷開連接。
worker.send(message[,sendHandle][,callback])
歷史
v0.7.0 在v0.7.0添加。
v4.0.0 這個版本支持callback參數。
- message <Object>
- sendHandle <Handle>
- callback <Function>
- 返回:<boolean>
發送給工作進程或者主進程消息,可以選擇附帶句柄。
在主進程中發送消息給特定的工作進程。它跟ChildProcess.send()一樣。
工作進程發送消息給主進程。它跟process.send()一樣。
下面的例子會把主進程發送的消息全部回傳回去:
if (cluster.isMaster) {const worker = cluster.fork();worker.send(hi there);} else if (cluster.isWorker) {process.on(message, (msg) => {process.send(msg);});}
Event:』disconnect』
在v0.7.9添加
- worker <cluster.Worker>
在工作進程的IPC通道斷開連接後發射。當一個工作進程優雅退出後將會發生,比如被殺死或者手工斷開連接(例如:通過worker.disconnect())。
在』disconnect』和』exit』事件之間可能會有延時。這些事件可以被用作檢查進程是否在清理過程中被卡住,或者它們是否有長連接。
cluster.on(disconnect, (worker) => {console.log(`The worker #${worker.id} has disconnected`);});
Event: 『exit』
在v0.7.9添加
- worker <cluster.Worker>
- code <number> 正常退出時的退出碼
- signal <string> 導致進程被殺死的信號量的名字(例如:』SIGHUP』)
任何工作進程死掉cluster模塊都會發射』exit』事件。
通過再次調用.fork()重新啟用工作進程。
cluster.on(exit, (worker, code, signal) => {console.log(worker %d died (%s). restarting...,worker.process.pid, signal || code);cluster.fork();});
參考child_process event: 『exit』。
Event: 『fork』
在v0.7.0添加
- worker <cluster.Worker>
當工作進程被創建的時候cluster模塊將發射』fork』事件。這樣可以記錄工作進程的活動,並且創建一個自定義超時。
const timeouts = [];function errorMsg() {console.error(Something must be wrong with the connection ...);}cluster.on(fork, (worker) => {timeouts[worker.id] = setTimeout(errorMsg, 2000);});cluster.on(listening, (worker, address) => {clearTimeout(timeouts[worker.id]);});cluster.on(exit, (worker, code, signal) => {clearTimeout(timeouts[worker.id]);errorMsg();});
Event:』listening』
在v0.7.0添加
- worker <cluster.Worker>
- address <Object>
在一個工作進程調用listen()之後,當一個』listening』事件在服務端被發射時,在主進程的cluster中同樣發射』listening』事件。
事件句柄包括兩個參數,worker參數包含工作進程對象,address對象包含連接屬性:address、port和addressType。這對於工作進程監聽多個地址非常有用。
cluster.on(listening, (worker, address) => {console.log(`A worker is now connected to ${address.address}:${address.port}`);});
addressType是以下內容中的一個:
- 4(TCPv4)
- 6(TCPv6)
- -1(unix域套接字)
- 『udp4』或者』udp6』(UDP v4或者v6)
Event:』message』
歷史
v6.0.0 worker參數被傳遞;詳情參考下面。
v2.5.0 在v2.5.0添加
- worker <cluster.Worker>
- message <Object>
- handle <undefined> | <Object>
當集群主服務收到任何工作進程的消息的時候發射。
參考child_process event:』message』。
在Node.js v6.0之前,這個事件只發射消息和句柄,並不包括工作進程對象。
如果需要兼容老版本,但是並不需要工作進程,可以通過參數的長度來判斷:
cluster.on(message, (worker, message, handle) => {if (arguments.length === 2) {handle = message;message = worker;worker = undefined;}// ...});
Event:』online』
在v0.7.0添加
- worker <cluster.Worker>
在創建一個工作進程之後,工作進程應該回應一個在線消息。當主進程接收到一個在線消息時,它會發射這個事件。』fork』跟』online』的區別在於,』fork』是在主進程生成一個工作進程時發射,而』online』是在工作進程運行時發射。
cluster.on(online, (worker) => {console.log(Yay, the worker responded after it was forked);});
Event:』setup』
在v0.7.1添加
- settings <Object>
每次調用.setupMaster()時發射。
settings對象是在調用.setupMaster時設置的cluster.settings對象,並且這個對象只是建議性的,因為在一個周期內.setupMaster()可以被調用多次。
如果精確度非常重要,使用cluster.settings。
cluster.disconnect([callback])
在v0.7.7添加
- callback <Function> 當所有工作進程斷開連接和句柄被關閉時調用。
在每一個cluster.workers中的工作進程中調用.disconnect()。
當所有工作進程被斷開連接,它們內部的句柄也將會關閉,如果沒有事件在等待,現在允許主進程優雅退出。
這個方法的回調函數參數是可選的,它將會在結束後調用。
這個函數只能在主進程中被調用。
cluster.fork([env])
在v0.6.0添加
- env <Object> 添加到工作進程環境的鍵值對。
- 返回:<cluster.Worker>
產生一個新的工作進程。
這個只能在主進程中調用。
cluster.isMaster
在v0.8.1添加
- <boolean>
返回ture則進程是主進程。這是由process.env.NODE_UNIQUE_ID決定的。如果process.env.NODE_UNIQUE_ID是未定義的,則isMaster返回true。
cluster.isWorker
在v0.6.0添加
- <boolean>
當進程不是主進程時返回true。
cluster.schedulingPolicy
在v0.11.2添加
調度策略,cluster.SCHED_RR的循環或者cluster.SCHED_NONE的把調度交給操作系統。這是一個全局設置,並且當第一個工作進程產生或者cluster.setupMaster被調用的時候就被凍結,先到先得。
SCHED_ERR在除了Windows意外的平台上都是默認的。當libuv在分配IOCP句柄的時候不會引起大的性能問題,Windows會切換到SCHED_RR。
cluster.schedulingPolicy可以通過NODE_CLUSTER_SCHED_POLICY環境變數來設置。有效的值為』rr』和』none』。
cluster.settings
歷史
v9.5.0 添加cwd選項支持
v9.4.0 添加windowsHide選項支持
v8.2.0 添加inspectPort選項支持
v6.4.0 添加stdio選項支持
v0.7.1 在v0.7.1添加
- <Object>
- execArgv <string[]> 傳遞給Node.js可執行的字元串參數列表。默認:process.execArgv。
- exec <string> 工作文件的目錄。默認: process.argv[1]。
- args <string[]> 傳遞到子進程的字元串參數。默認:process.argv.slice(2)。
- cwd <string> 當前工作進程的工作目錄。默認:undefined(從父進程繼承)。
- silent <boolean> 是否把輸出發送到父進程的標準輸入輸出(stdio)。默認:false。
- stdio <Array> 配置生成的進程的標準輸入輸出(stdio)。因為集群模塊依賴IPC運行,所以配置必須包含一個』ipc』條目。當提供了這個選項,silent將會被覆蓋。
- uid <number> 設置進程的用戶唯一標識。(參考setuid(2))
- gid <number> 設置進程的組唯一標識。(參考setgid(2))
- inspectPort <number> | <Function> 設置工作進程的檢測埠。這可以是一個數字,或者一個沒有參數的函數,並且返回一個數字。默認每個工作進程獲得它自己的埠,通過增加主進程的process.debugPort產生。
- windowsHide <boolean> 隱藏通常在Windows系統上創建的進程式控制制台窗口。默認:false。
這個設置對象在調用.setupMaster()(或者.fork())之後將會包含這些設置,包括默認值。
這個對象不會被改變和手動設置。
cluster.setupMaster([settings])
歷史
v6.4.0 添加stdio選項支持
v0.7.1 在v0.7.1添加
- settings <Object> 參考cluster.settings
setupMaster用來改變默認的』fork』方式。一旦被調用,設置將會提現在cluster.settings。
注意這些:
- 任何設置改變隻影響將來通過.fork()創建的進程,原來的進程不受影響。
- env是唯一不能通過.setupMaster()來給.fork()設置的工作進程屬性。
- 上述的默認值只適用於第一次調用,以後的默認值是cluster.setupMaster()被調用時的值。
例子:
const cluster = require(cluster);cluster.setupMaster({exec: worker.js,args: [--use, https],silent: true});cluster.fork(); // https workercluster.setupMaster({exec: worker.js,args: [--use, http]});cluster.fork(); // http worker
這個只能在主進程中執行。
cluster.worker
在v0.7.0添加
- <Object>
當前工作進程對象的引用。在主進程中不可用。
const cluster = require(cluster);if (cluster.isMaster) {console.log(I am master);cluster.fork();cluster.fork();} else if (cluster.isWorker) {console.log(`I am worker #${cluster.worker.id}`);}
cluster.workers
在v0.7.0添加
- <Object>
一個存儲存活的工作對象的哈希表,鍵為id欄位。工作進程可以很容易的被循環。這個方法只在主進程中可見。
在工作進程斷開連接和退出後在cluster.workers中被移除。這兩個事件的順序不能被提前預測。然而,可以保證的是cluster.workers的移除動作發生在最後的『disconnect』或者』exit』事件被發射之前。
// Go through all workersfunction eachWorker(callback) {for (const id in cluster.workers) {callback(cluster.workers[id]);}}eachWorker((worker) => {worker.send(big announcement to all workers);});
使用工作進程的唯一id號是查找進程的最好方式。
socket.on(data, (id) => {const worker = cluster.workers[id];});
推薦閱讀:
※如何解決大規模機器學習的三大痛點?
※Docker 引擎的 Swarm 模式:入門教程
※【技術貼】如何部署Apache Kylin集群實現負載均衡?
※[轉]三大主流軟體負載均衡器對比(LVS、Nginx、HAproxy)