熟悉 js 的朋友都知道,js 是單線程
的,在 Node 中,采用的是 多進(jìn)程單線程 的模型。由于javascript單線程的限制,在多核服務(wù)器上,我們往往需要啟動多個進(jìn)程才能最大化服務(wù)器性能。
Node.js 進(jìn)程集群可用于運(yùn)行多個 Node.js 實(shí)例,這些實(shí)例可以在其應(yīng)用程序線程之間分配工作負(fù)載。 當(dāng)不需要進(jìn)程隔離時,請改用 worker_threads
模塊,它允許在單個 Node.js 實(shí)例中運(yùn)行多個應(yīng)用程序線程。
零、NodeJS多進(jìn)程
- 進(jìn)程總數(shù),其中一個主進(jìn)程,cpu 個數(shù) x cpu 核數(shù) 個 子進(jìn)程
- 無論 child_process 還是 cluster,都不是多線程模型,而是多進(jìn)程模型
- 應(yīng)對單線程問題,通常使用多進(jìn)程的方式來模擬多線程
一、核心模塊cluster集群
Node 在 V0.8 版本之后引入了 cluster模塊,通過一個主進(jìn)程 (master) 管理多個子進(jìn)程 (worker) 的方式實(shí)現(xiàn)集群
。
集群模塊可以輕松創(chuàng)建共享服務(wù)器端口的子進(jìn)程。
cluster 底層是 child_process 模塊,除了可以發(fā)送普通消息,還可以發(fā)送底層對象
TCP
、UDP
等,cluster
模塊是child_process
模塊和net
模塊的組合應(yīng)用。 cluster 啟動時,內(nèi)部會啟動 TCP 服務(wù)器,將這個 TCP 服務(wù)器端 socket 的文件描述符發(fā)給工作進(jìn)程。
在 cluster
模塊應(yīng)用中,一個主進(jìn)程只能管理一組工作進(jìn)程
,其運(yùn)作模式?jīng)]有 child_process
模塊那么靈活,但是更加穩(wěn)定:
1.cluster配置詳情
1.1 引入cluster
const cluster = require('cluster')復(fù)
1.2 cluster常用屬性
.isMaster
標(biāo)識主進(jìn)程, Node<16.isPrimary
標(biāo)識主進(jìn)程, Node>16.isWorker
標(biāo)識子進(jìn)程.worker
對當(dāng)前工作進(jìn)程對象的引用【子進(jìn)程中】.workers
存儲活動工作進(jìn)程對象的哈希,以id
字段為鍵。 這樣可以很容易地遍歷所有工作進(jìn)程。 它僅在主進(jìn)程中可用。cluster.wokers[id] === worker
【主進(jìn)程中】.settings
只讀, cluster配置項(xiàng)。在調(diào)用 .setupPrimary()或.fork()方法之后,此設(shè)置對象將包含設(shè)置,包括默認(rèn)值。之前為空對象。此對象不應(yīng)手動更改或設(shè)置。
cluster.settings
配置項(xiàng)詳情:
- `execArgv` <string[]>傳給 Node.js 可執(zhí)行文件的字符串參數(shù)列表。 **默認(rèn)值:** `process.execArgv`。 - `exec` <string> 工作進(jìn)程文件的文件路徑。 **默認(rèn)值:** `process.argv[1]`。 - `args` <string[]> 傳給工作進(jìn)程的字符串參數(shù)。 **默認(rèn)值:**`process.argv.slice(2)`。 - `cwd` <string>工作進(jìn)程的當(dāng)前工作目錄。 **默認(rèn)值:** `undefined` (從父進(jìn)程繼承)。 - `serialization` <string>指定用于在進(jìn)程之間發(fā)送消息的序列化類型。 可能的值為 `'json'` 和 `'advanced'`。 **默認(rèn)值:** `false`。 - `silent` <boolean>是否將輸出發(fā)送到父進(jìn)程的標(biāo)準(zhǔn)輸入輸出。 **默認(rèn)值:** `false`。 - `stdio` <Array>配置衍生進(jìn)程的標(biāo)準(zhǔn)輸入輸出。 由于集群模塊依賴 IPC 來運(yùn)行,因此此配置必須包含 `'ipc'` 條目。 提供此選項(xiàng)時,它會覆蓋 `silent`。 - `uid` <number>設(shè)置進(jìn)程的用戶標(biāo)識。 - `gid` <number>設(shè)置進(jìn)程的群組標(biāo)識。 - `inspectPort` <number> | <Function> 設(shè)置工作進(jìn)程的檢查器端口。 這可以是數(shù)字,也可以是不帶參數(shù)并返回?cái)?shù)字的函數(shù)。 默認(rèn)情況下,每個工作進(jìn)程都有自己的端口,從主進(jìn)程的 `process.debugPort` 開始遞增。 - `windowsHide` <boolean> 隱藏通常在 Windows 系統(tǒng)上創(chuàng)建的衍生進(jìn)程控制臺窗口。 **默認(rèn)值:** `false`。
1.3 cluster常用方法
.fork([env])
衍生新的工作進(jìn)程【主進(jìn)程中】.setupPrimary([settings])
Node>16.setupMaster([settings])
用于更改默認(rèn)的 'fork' 行為,用后設(shè)置將出現(xiàn)在cluster.settings
中。任何設(shè)置更改只會影響未來對.fork()
的調(diào)用,而不會影響已經(jīng)運(yùn)行的工作進(jìn)程。上述默認(rèn)值僅適用于第一次調(diào)用。Node 小于 16【主進(jìn)程中】.disconnect([callback])
當(dāng)所有工作進(jìn)程斷開連接并關(guān)閉句柄時調(diào)用【主進(jìn)程中】
1.4 cluster常用事件
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 當(dāng)集群主進(jìn)程接收到來自任何工作進(jìn)程的消息時觸發(fā)。'exit'
事件, 當(dāng)任何工作進(jìn)程死亡時,則集群模塊將觸發(fā)'exit'
事件。
cluster.on('exit', (worker, code, signal) => { console.log('worker %d died (%s). restarting...', worker.process.pid, signal || code); cluster.fork(); });
'listening'
事件,從工作進(jìn)程調(diào)用listen()
后,當(dāng)服務(wù)器上觸發(fā)'listening'
事件時,則主進(jìn)程中的cluster
也將觸發(fā)'listening'
事件。
cluster.on('listening', (worker, address) => { console.log( `A worker is now connected to ${address.address}:${address.port}`); });
'fork'
事件,當(dāng)新的工作進(jìn)程被衍生時,則集群模塊將觸發(fā)'fork'
事件。
cluster.on('fork', (worker) => { timeouts[worker.id] = setTimeout(errorMsg, 2000); });
'setup'
事件,每次調(diào)用.setupPrimary()
時觸發(fā)。disconnect
事件,在工作進(jìn)程 IPC 通道斷開連接后觸發(fā)。 當(dāng)工作進(jìn)程正常退出、被殺死、或手動斷開連接時
cluster.on('disconnect', (worker) => { console.log(`The worker #${worker.id} has disconnected`); });
1.5 Worker類
Worker
對象包含了工作進(jìn)程的所有公共的信息和方法。 在主進(jìn)程中,可以使用 cluster.workers
來獲取它。 在工作進(jìn)程中,可以使用 cluster.worker
來獲取它。
1.5.1 worker常用屬性
.id
工作進(jìn)程標(biāo)識,每個新的工作進(jìn)程都被賦予了自己唯一的 id,此 id 存儲在id
。當(dāng)工作進(jìn)程存活時,這是在cluster.workers
中索引它的鍵。.process
所有工作進(jìn)程都是使用child_process.fork()
創(chuàng)建,此函數(shù)返回的對象存儲為.process
。 在工作進(jìn)程中,存儲了全局的process
。
1.5.2 worker常用方法
.send(message[, sendHandle[, options]][, callback])
向工作進(jìn)程或主進(jìn)程發(fā)送消息,可選擇使用句柄。在主進(jìn)程中,這會向特定的工作進(jìn)程發(fā)送消息。 它與ChildProcess.send()
相同。在工作進(jìn)程中,這會向主進(jìn)程發(fā)送消息。 它與process.send()
相同。.destroy()
.kill([signal])
此函數(shù)會殺死工作進(jìn)程。kill()
函數(shù)在不等待正常斷開連接的情況下殺死工作進(jìn)程,它與worker.process.kill()
具有相同的行為。為了向后兼容,此方法別名為worker.destroy()
。.disconnect([callback])
發(fā)送給工作進(jìn)程,使其調(diào)用自身的.disconnect()
將關(guān)閉所有服務(wù)器,等待那些服務(wù)器上的'close'
事件,然后斷開 IPC 通道。.isConnect()
如果工作進(jìn)程通過其 IPC 通道連接到其主進(jìn)程,則此函數(shù)返回true
,否則返回false
。 工作進(jìn)程在創(chuàng)建后連接到其主進(jìn)程。.isDead()
如果工作進(jìn)程已終止(由于退出或收到信號),則此函數(shù)返回true
。 否則,它返回false
。
1.5.3 worker常用事件
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 在工作進(jìn)程中。
cluster.workers[id].on('message', messageHandler);
'exit'
事件, 當(dāng)任何工作進(jìn)程死亡時,則當(dāng)前worker工作進(jìn)程
對象將觸發(fā)'exit'
事件。
if (cluster.isPrimary) { 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!'); } }); }
'listening'
事件,從工作進(jìn)程調(diào)用listen()
,對當(dāng)前工作進(jìn)程進(jìn)行監(jiān)聽。
cluster.fork().on('listening', (address) => { // 工作進(jìn)程正在監(jiān)聽 });
disconnect
事件,在工作進(jìn)程 IPC 通道斷開連接后觸發(fā)。 當(dāng)工作進(jìn)程正常退出、被殺死、或手動斷開連接時
cluster.fork().on('disconnect', () => { //限定于當(dāng)前worker對象觸發(fā) });
2. 進(jìn)程通信
Node中主進(jìn)程和子進(jìn)程之間通過進(jìn)程間通信 (IPC) 實(shí)現(xiàn)進(jìn)程間的通信,進(jìn)程間通過 .send()
(a.send表示向a發(fā)送)方法發(fā)送消息,監(jiān)聽 message
事件收取信息,這是 cluster模塊
通過集成 EventEmitter
實(shí)現(xiàn)的。還是一個簡單的官網(wǎng)的進(jìn)程間通信例子
- 子進(jìn)程:
process.on('message')
、process.send()
- 父進(jìn)程:
child.on('message')
、child.send()
# cluster.isMaster # cluster.fork() # cluster.workers # cluster.workers[id].on('message', messageHandler); # cluster.workers[id].send(); # process.on('message', messageHandler); # process.send(); const cluster = require('cluster'); const http = require('http'); # 主進(jìn)程 if (cluster.isMaster) { // Keep track of http requests console.log(`Primary ${process.pid} is running`); let numReqs = 0; // Count requests function messageHandler(msg) { if (msg.cmd && msg.cmd === 'notifyRequest') { numReqs += 1; } } // Start workers and listen for messages containing notifyRequest // 開啟多進(jìn)程(cpu核心數(shù)) // 衍生工作進(jìn)程。 const numCPUs = require('os').cpus().length; for (let i = 0; i < numCPUs; i++) { console.log(i) cluster.fork(); } // cluster worker 主進(jìn)程與子進(jìn)程通信 for (const id in cluster.workers) { // ***監(jiān)聽來自子進(jìn)程的事件 cluster.workers[id].on('message', messageHandler); // ***向子進(jìn)程發(fā)送 cluster.workers[id].send({ type: 'masterToWorker', from: 'master', data: { number: Math.floor(Math.random() * 50) } }); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { # 子進(jìn)程 // 工作進(jìn)程可以共享任何 TCP 連接 // 在本示例中,其是 HTTP 服務(wù)器 // Worker processes have a http server. http.Server((req, res) => { res.writeHead(200); res.end('hello worldn'); //****** ?。。?!Notify master about the request ?。。。。?!******* //****** 向process發(fā)送 process.send({ cmd: 'notifyRequest' }); //****** 監(jiān)聽從process來的 process.on('message', function(message) { // xxxxxxx }) }).listen(8000); console.log(`Worker ${process.pid} started`); }
2.1 句柄發(fā)送與還原
NodeJS 進(jìn)程之間通信只有消息傳遞,不會真正的傳遞對象。
send()
方法在發(fā)送消息前,會將消息組裝成 handle 和 message,這個 message 會經(jīng)過 JSON.stringify
序列化,也就是說,傳遞句柄的時候,不會將整個對象傳遞過去,在 IPC 通道傳輸?shù)亩际亲址?,傳輸后通過 JSON.parse
還原成對象。
2.2 監(jiān)聽共同端口
代碼里有 app.listen(port)
在進(jìn)行 fork 時,為什么多個進(jìn)程可以監(jiān)聽同一個端口呢?
原因是主進(jìn)程通過 send() 方法向多個子進(jìn)程發(fā)送屬于該主進(jìn)程的一個服務(wù)對象的句柄,所以對于每一個子進(jìn)程而言,它們在還原句柄之后,得到的服務(wù)對象是一樣的,當(dāng)網(wǎng)絡(luò)請求向服務(wù)端發(fā)起時,進(jìn)程服務(wù)是搶占式的,所以監(jiān)聽相同端口時不會引起異常。
- 看下端口被占用的情況:
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
# worker.js const http = require('http'); http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); }).listen(3000);
以上代碼示例,控制臺執(zhí)行
node master.js
只有一個 worker 可以監(jiān)聽到 3000 端口,其余將會拋出Error: listen EADDRINUSE :::3000
錯誤。
- 那么多進(jìn)程模式下怎么實(shí)現(xiàn)多進(jìn)程端口監(jiān)聽呢?答案還是有的,通過句柄傳遞 Node.js v0.5.9 版本之后支持進(jìn)程間可
發(fā)送句柄
功能
/** * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback * message * sendHandle */ subprocess.send(message, sendHandle)
當(dāng)父子進(jìn)程之間建立 IPC 通道之后,通過子進(jìn)程對象的 send 方法發(fā)送消息,第二個參數(shù) sendHandle 就是句柄,可以是 TCP套接字、TCP服務(wù)器、UDP套接字等
,為了解決上面多進(jìn)程端口占用問題,我們將主進(jìn)程的 socket 傳遞到子進(jìn)程。
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); const server = require('net').createServer(); server.listen(3000); process.title = 'node-master' for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); # 句柄傳遞 worker.send('server', server); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
// worker.js let worker; process.title = 'node-worker' process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function (socket) { console.log('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid) }); } });
驗(yàn)證一番,控制臺執(zhí)行 node master.js
2.3 進(jìn)程負(fù)載均衡
了解 cluster
的話會知道,子進(jìn)程是通過 cluster.fork()
創(chuàng)建的。在 linux 中,系統(tǒng)原生提供了 fork
方法,那么為什么 Node 選擇自己實(shí)現(xiàn) cluster模塊
,而不是直接使用系統(tǒng)原生的方法?主要的原因是以下兩點(diǎn):
-
fork的進(jìn)程監(jiān)聽同一端口會導(dǎo)致端口占用錯誤
-
fork的進(jìn)程之間沒有負(fù)載均衡,容易導(dǎo)致驚群現(xiàn)象
在 cluster模塊
中,針對第一個問題,通過判斷當(dāng)前進(jìn)程是否為 master進(jìn)程
,若是,則監(jiān)聽端口,若不是則表示為 fork 的 worker進(jìn)程
,不監(jiān)聽端口。
針對第二個問題,cluster模塊
內(nèi)置了負(fù)載均衡功能, master進(jìn)程
負(fù)責(zé)監(jiān)聽端口接收請求,然后通過調(diào)度算法(默認(rèn)為 Round-Robin,可以通過環(huán)境變量 NODE_CLUSTER_SCHED_POLICY
修改調(diào)度算法)分配給對應(yīng)的 worker進(jìn)程
。
3. 異常捕獲
3.1 未捕獲異常
當(dāng)代碼拋出了異常沒有被捕獲到時,進(jìn)程將會退出,此時 Node.js 提供了 process.on('uncaughtException', handler)
接口來捕獲它,但是當(dāng)一個 Worker 進(jìn)程遇到未捕獲的異常時,它已經(jīng)處于一個不確定狀態(tài),此時我們應(yīng)該讓這個進(jìn)程優(yōu)雅退出:
- 關(guān)閉異常 Worker 進(jìn)程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。
- Master 立刻 fork 一個新的 Worker 進(jìn)程,保證在線的『工人』總數(shù)不變。
- 異常 Worker 等待一段時間,處理完已經(jīng)接受的請求后退出。
+---------+ +---------+ | Worker | | Master | +---------+ +----+----+ | uncaughtException | +------------+ | | | | +---------+ | <----------+ | | Worker | | | +----+----+ | disconnect | fork a new worker | +-------------------------> + ---------------------> | | wait... | | | exit | | +-------------------------> | | | | | die | | | | | |
3.2 OOM、系統(tǒng)異常
當(dāng)一個進(jìn)程出現(xiàn)異常導(dǎo)致 crash 或者 OOM 被系統(tǒng)殺死時,不像未捕獲異常發(fā)生時我們還有機(jī)會讓進(jìn)程繼續(xù)執(zhí)行,只能夠讓當(dāng)前進(jìn)程直接退出,Master 立刻 fork 一個新的 Worker。
二、子進(jìn)程
1. child_process模塊
child_process 模塊提供了衍生子進(jìn)程的能力, 簡單來說就是執(zhí)行cmd命令的能力
。 默認(rèn)情況下, stdin、 stdout 和 stderr 的管道會在父 Node.js 進(jìn)程和衍生的子進(jìn)程之間建立
。 這些管道具有有限的(且平臺特定的)容量。 如果子進(jìn)程寫入 stdout 時超出該限制且沒有捕獲輸出,則子進(jìn)程會阻塞并等待管道緩沖區(qū)接受