您好,登錄后才能下訂單哦!
這篇文章給大家分享的是有關(guān)Node中集群的示例分析的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。
Node 在 v0.8 時(shí)直接引入了 cluster 模塊,用以解決多核 CPU 的利用率問題,同時(shí)也提供了較完善的 API,用以處理進(jìn)程的健壯性問題。
cluster 模塊調(diào)用 fork 方法來創(chuàng)建子進(jìn)程,該方法與 child_process 中的 fork 是同一個(gè)方法。 cluster 模塊采用的是經(jīng)典的主從模型,cluster 會(huì)創(chuàng)建一個(gè) master,然后根據(jù)你指定的數(shù)量復(fù)制出多個(gè)子進(jìn)程,可以使用cluster.isMaster 屬性判斷當(dāng)前進(jìn)程是 master 還是 worker (工作進(jìn)程)。由 master 進(jìn)程來管理所有的子進(jìn)程,主進(jìn)程不負(fù)責(zé)具體的任務(wù)處理,主要工作是負(fù)責(zé)調(diào)度和管理。
cluster 模塊使用內(nèi)置的負(fù)載均衡來更好地處理線程之間的壓力,該負(fù)載均衡使用了 Round-robin 算法(也被稱之為循環(huán)算法)。當(dāng)使用 Round-robin 調(diào)度策略時(shí),master accepts() 所有傳入的連接請(qǐng)求,然后將相應(yīng)的TCP請(qǐng)求處理發(fā)送給選中的工作進(jìn)程(該方式仍然通過 IPC 來進(jìn)行通信)。 官方使用實(shí)例如下所示
const cluster = require('cluster'); const cpuNums = require('os').cpus().length; const http = require('http'); if (cluster.isMaster) { for (let i = 0; i < cpuNums; i++){ cluster.fork(); } // 子進(jìn)程退出監(jiān)聽 cluster.on('exit', (worker,code,signal) => { console.log('worker process died,id',worker.process.pid) }) } else { // 給子進(jìn)程標(biāo)注進(jìn)程名 process.title = `cluster 子進(jìn)程 ${process.pid}`; // Worker可以共享同一個(gè) TCP 連接,這里是一個(gè) http 服務(wù)器 http.createServer((req, res)=> { res.end(`response from worker ${process.pid}`); }).listen(3000); console.log(`Worker ${process.pid} started`); }
其實(shí),cluster 模塊由 child_process 和 net 模塊的組合應(yīng)用,cluster 啟動(dòng)時(shí),會(huì)在內(nèi)部啟動(dòng) TCP 服務(wù)器,在 cluster.fork() 子進(jìn)程時(shí),將這個(gè) TCP 服務(wù)器端 socket 的文件描述符發(fā)送給工作進(jìn)程。如果工作進(jìn)程是通過 cluster.fork() 復(fù)制出來的,那么它的環(huán)境變量里就存在 NODE_UNIQUE_ID,如果工作進(jìn)程中存在 listen() 偵聽網(wǎng)絡(luò)端口的調(diào)用,它將拿到文件描述符,通過 SO_REUSEADDR 端口重用,從而實(shí)現(xiàn)多個(gè)子進(jìn)程共享端口。
fork:復(fù)制一個(gè)工作進(jìn)程后觸發(fā)該事件;
online:復(fù)制好一個(gè)工作進(jìn)程后,工作進(jìn)程主動(dòng)發(fā)送一條 online 消息給主進(jìn)程,主進(jìn)程收到消息后,觸發(fā)該事件;
listening:工作進(jìn)程中調(diào)用 listen() (共享了服務(wù)器端 Socket)后,發(fā)送一條 listening 消息給主進(jìn)程,主進(jìn)程收到消息后,觸發(fā)該事件;
disconnect:主進(jìn)程和工作進(jìn)程之間 IPC 通道斷開后會(huì)觸發(fā)該事件;
exit:有工作進(jìn)程退出時(shí)會(huì)觸發(fā)該事件;
setup:cluster.setupMaster() 執(zhí)行完后觸發(fā)該事件;
這些事件大多跟 child_process 模塊的事件相關(guān),在進(jìn)程間消息傳遞的基礎(chǔ)上完成的封裝。
cluster.on('fork', ()=> { console.log('fork 事件... '); }) cluster.on('online', ()=> { console.log('online 事件... '); }) cluster.on('listening', ()=> { console.log('listening 事件... '); }) cluster.on('disconnect', ()=> { console.log('disconnect 事件... '); }) cluster.on('exit', ()=> { console.log('exit 事件... '); }) cluster.on('setup', ()=> { console.log('setup 事件... '); })
由以上可知,master 進(jìn)程通過 cluster.fork() 來創(chuàng)建 worker 進(jìn)程,其實(shí),cluster.fork() 內(nèi)部是通過 child_process.fork() 來創(chuàng)建子進(jìn)程。也就是說:master 與 worker 進(jìn)程是父、子進(jìn)程的關(guān)系;其跟 child_process 創(chuàng)建的父子進(jìn)程一樣是通過 IPC 通道進(jìn)行通信的。
IPC 的全稱是 Inter-Process Communication,即進(jìn)程間通信,進(jìn)程間通信的目的是為了讓不同的進(jìn)程能夠互相訪問資源并進(jìn)行協(xié)調(diào)工作。Node 中實(shí)現(xiàn) IPC 通道的是管道(pipe)技術(shù),具體實(shí)現(xiàn)由 libuv 提供,在 Windows 下由命名管道(named pipe)實(shí)現(xiàn),*nix 系統(tǒng)則采用 Unix Domain Socket 實(shí)現(xiàn)。其變現(xiàn)在應(yīng)用層上的進(jìn)程間通信只有簡(jiǎn)單的 message 事件和 send 方法,使用十分簡(jiǎn)單。
父進(jìn)程在實(shí)際創(chuàng)建子進(jìn)程之前,會(huì)創(chuàng)建 IPC 通道并監(jiān)聽它,然后才真正創(chuàng)建出子進(jìn)程,并通過環(huán)境變量(NODE_CHANNEL_FD)告訴子進(jìn)程這個(gè) IPC 通道的文件描述符。子進(jìn)程在啟動(dòng)過程中,根據(jù)文件描述符去連接這個(gè)已存在的 IPC 通道,從而完成父子進(jìn)程之間的連接。
建立連接之后的父子進(jìn)程就可以進(jìn)行自由通信了。由于 IPC 通道是用命名管道或 Domain Socket 創(chuàng)建的,它們與網(wǎng)絡(luò) socket 的行為比較類似,屬于雙向通信。不同的是它們?cè)谙到y(tǒng)內(nèi)核中就完成了進(jìn)程間的通信,而不用經(jīng)過實(shí)際的網(wǎng)絡(luò)層,非常高效。在 Node 中,IPC 通道被抽象為 Stream 對(duì)象,在調(diào)用 send 時(shí)發(fā)送數(shù)據(jù)(類似于 write ),接收到的消息會(huì)通過 message 事件(類似于 data)觸發(fā)給應(yīng)用層。
master 和 worker 進(jìn)程在 server 實(shí)例的創(chuàng)建過程中,是通過 IPC 通道進(jìn)行通信的,那會(huì)不會(huì)對(duì)我們的開發(fā)造成干擾呢?比如,收到一堆其實(shí)并不需要關(guān)心的消息?答案肯定是不會(huì)?那么是怎么做到的呢?
Node 引入進(jìn)程間發(fā)送句柄的功能,send 方法除了能通過 IPC 發(fā)送數(shù)據(jù)外,還能發(fā)送句柄,第二個(gè)參數(shù)為句柄,如下所示
child.send(meeage, [sendHandle])
句柄是一種可以用來標(biāo)識(shí)資源的引用,它的內(nèi)部包含了指向?qū)ο蟮奈募枋龇?。例如句柄可以用來?biāo)識(shí)一個(gè)服務(wù)器端 socket 對(duì)象、一個(gè)客戶端 socket 對(duì)象、一個(gè) UDP 套接字、一個(gè)管道等。 那么句柄發(fā)送跟我們直接將服務(wù)器對(duì)象發(fā)送給子進(jìn)程有沒有什么差別?它是否真的將服務(wù)器對(duì)象發(fā)送給子進(jìn)程?
其實(shí) send() 方法在將消息發(fā)送到 IPC 管道前,將消息組裝成兩個(gè)對(duì)象,一個(gè)參數(shù)是 handle,另一個(gè)是 message,message 參數(shù)如下所示
{ cmd: 'NODE_HANDLE', type: 'net.Server', msg: message }
發(fā)送到 IPC 管道中的實(shí)際上是要發(fā)送的句柄文件描述符,其為一個(gè)整數(shù)值。這個(gè) message 對(duì)象在寫入到 IPC 管道時(shí)會(huì)通過 JSON.stringify 進(jìn)行序列化,轉(zhuǎn)化為字符串。子進(jìn)程通過連接 IPC 通道讀取父進(jìn)程發(fā)送來的消息,將字符串通過 JSON.parse 解析還原為對(duì)象后,才觸發(fā) message 事件將消息體傳遞給應(yīng)用層使用。在這個(gè)過程中,消息對(duì)象還要被進(jìn)行過濾處理,message.cmd 的值如果以 NODE_ 為前綴,它將響應(yīng)一個(gè)內(nèi)部事件 internalMessage ,如果 message.cmd 值為 NODE_HANDLE,它將取出 message.type 值和得到的文件描述符一起還原出一個(gè)對(duì)應(yīng)的對(duì)象。這個(gè)過程的示意圖如下所示
在 cluster 中,以 worker 進(jìn)程通知 master 進(jìn)程創(chuàng)建 server 實(shí)例為例子。worker 偽代碼如下:
// woker進(jìn)程 const message = { cmd: 'NODE_CLUSTER', type: 'net.Server', msg: message }; process.send(message);
master 偽代碼如下:
worker.process.on('internalMessage', fn);
在前面的例子中,多個(gè) woker 中創(chuàng)建的 server 監(jiān)聽了同個(gè)端口 3000,通常來說,多個(gè)進(jìn)程監(jiān)聽同個(gè)端口,系統(tǒng)會(huì)報(bào) EADDRINUSE 異常。為什么 cluster 沒問題呢?
因?yàn)楠?dú)立啟動(dòng)的進(jìn)程中,TCP 服務(wù)器端 socket 套接字的文件描述符并不相同,導(dǎo)致監(jiān)聽到相同的端口時(shí)會(huì)拋出異常。但對(duì)于 send() 發(fā)送的句柄還原出來的服務(wù)而言,它們的文件描述符是相同的,所以監(jiān)聽相同端口不會(huì)引起異常。
這里需要注意的是,多個(gè)應(yīng)用監(jiān)聽相同端口時(shí),文件描述符同一時(shí)間只能被某個(gè)進(jìn)程所用,換言之就是網(wǎng)絡(luò)請(qǐng)求向服務(wù)器端發(fā)送時(shí),只有一個(gè)幸運(yùn)的進(jìn)程能夠搶到連接,也就是說只有它能為這個(gè)請(qǐng)求進(jìn)行服務(wù),這些進(jìn)程服務(wù)是搶占式的。
每當(dāng) worker 進(jìn)程創(chuàng)建 server 實(shí)例來監(jiān)聽請(qǐng)求,都會(huì)通過 IPC 通道,在 master 上進(jìn)行注冊(cè)。當(dāng)客戶端請(qǐng)求到達(dá),master 會(huì)負(fù)責(zé)將請(qǐng)求轉(zhuǎn)發(fā)給對(duì)應(yīng)的 worker;
具體轉(zhuǎn)發(fā)給哪個(gè) worker?這是由轉(zhuǎn)發(fā)策略決定的,可以通過環(huán)境變量 NODE_CLUSTER_SCHED_POLICY 設(shè)置,也可以在 cluster.setupMaster(options) 時(shí)傳入,默認(rèn)的轉(zhuǎn)發(fā)策略是輪詢(SCHED_RR);
當(dāng)有客戶請(qǐng)求到達(dá),master 會(huì)輪詢一遍 worker 列表,找到第一個(gè)空閑的 worker,然后將該請(qǐng)求轉(zhuǎn)發(fā)給該worker;
pm2 是 node 進(jìn)程管理工具,可以利用它來簡(jiǎn)化很多 node 應(yīng)用管理的繁瑣任務(wù),如性能監(jiān)控、自動(dòng)重啟、負(fù)載均衡等。
pm2 自身是基于 cluster 模塊進(jìn)行封裝的, 本節(jié)我們主要 pm2 的 Satan 進(jìn)程、God Daemon 守護(hù)進(jìn)程 以及兩者之間的進(jìn)程間遠(yuǎn)程調(diào)用 RPC。
撒旦(Satan),主要指《圣經(jīng)》中的墮天使(也稱墮天使撒旦),被看作與上帝的力量相對(duì)的邪惡、黑暗之源,是God 的對(duì)立面。
其中 Satan.js 提供程序的退出、殺死等方法,God.js 負(fù)責(zé)維持進(jìn)程的正常運(yùn)行,God 進(jìn)程啟動(dòng)后一直運(yùn)行,相當(dāng)于 cluster 中的 Master進(jìn)程,維持 worker 進(jìn)程的正常運(yùn)行。
RPC(Remote Procedure Call Protocol)是指遠(yuǎn)程過程調(diào)用,也就是說兩臺(tái)服務(wù)器A,B,一個(gè)應(yīng)用部署在A 服務(wù)器上,想要調(diào)用 B 服務(wù)器上應(yīng)用提供的函數(shù)/方法,由于不在一個(gè)內(nèi)存空間,不能直接調(diào)用,需要通過網(wǎng)絡(luò)來表達(dá)調(diào)用的語(yǔ)義和傳達(dá)調(diào)用的數(shù)據(jù)。同一機(jī)器不同進(jìn)程間的方法調(diào)用也屬于 rpc 的作用范疇。 執(zhí)行流程如下所示
每次命令行的輸入都會(huì)執(zhí)行一次 satan 程序,如果 God 進(jìn)程不在運(yùn)行,首先需要啟動(dòng) God 進(jìn)程。然后根據(jù)指令,Satan 通過 rpc 調(diào)用 God 中對(duì)應(yīng)的方法執(zhí)行相應(yīng)的邏輯。
以 pm2 start app.js -i 4
為例,God 在初次執(zhí)行時(shí)會(huì)配置 cluster,同時(shí)監(jiān)聽 cluster 中的事件:
// 配置cluster cluster.setupMaster({ exec : path.resolve(path.dirname(module.filename), 'ProcessContainer.js') }); // 監(jiān)聽cluster事件 (function initEngine() { cluster.on('online', function(clu) { // worker進(jìn)程在執(zhí)行 God.clusters_db[clu.pm_id].status = 'online'; }); // 命令行中 kill pid 會(huì)觸發(fā)exit事件,process.kill不會(huì)觸發(fā)exit cluster.on('exit', function(clu, code, signal) { // 重啟進(jìn)程 如果重啟次數(shù)過于頻繁直接標(biāo)注為stopped God.clusters_db[clu.pm_id].status = 'starting'; // 邏輯 // ... }); })();
在 God 啟動(dòng)后, 會(huì)建立 Satan 和 God 的rpc鏈接,然后調(diào)用 prepare 方法,prepare 方法會(huì)調(diào)用 cluster.fork 來完成集群的啟動(dòng)
God.prepare = function(opts, cb) { // ... return execute(opts, cb); }; function execute(env, cb) { // ... var clu = cluster.fork(env); // ... God.clusters_db[id] = clu; clu.once('online', function() { God.clusters_db[id].status = 'online'; if (cb) return cb(null, clu); return true; }); return clu; }
感謝各位的閱讀!關(guān)于“Node中集群的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。