【nodejs原理&源碼賞析(4)】深度剖析cluster模塊源碼與node.js多進(jìn)程(上)
示例代碼托管在:http://www.github.com/dashnowords/blogs
博客園地址:《大史住在大前端》原創(chuàng)博文目錄
華為云社區(qū)地址:【你要的前端打怪升級(jí)指南】
【nodejs原理&源碼賞析(4)】深度剖析cluster模塊源碼與node.js多進(jìn)程(上)一. 概述二. 線程與進(jìn)程三. cluster模塊源碼解析3.1 起步3.2 入口3.3 主進(jìn)程模塊master.js3.4 子進(jìn)程模塊child.js四. 小結(jié)
一. 概述
cluster模塊是node.js中用于實(shí)現(xiàn)和管理多進(jìn)程的模塊。常規(guī)的node.js應(yīng)用程序是單線程單進(jìn)程的,這也意味著它很難充分利用服務(wù)器多核CPU的性能,而cluster模塊就是為了解決這個(gè) 問題的,它使得node.js程序可以以多個(gè)實(shí)例并存的方式運(yùn)行在不同的進(jìn)程中,以求更大地榨取服務(wù)器的性能。node.js在官方示例代碼中使用worker實(shí)例來表示主進(jìn)程fork出的子進(jìn)程,使得前端開發(fā)者在學(xué)習(xí)過程中非常容易和瀏覽器環(huán)境中的worker實(shí)現(xiàn)的多線程混淆。為了容易區(qū)分,我們和node官方文檔使用一致的名稱,用集群中的master和worker來區(qū)分主進(jìn)程和工作進(jìn)程,用worker_threads來描述工作線程。
node.js的主從模型中,master主進(jìn)程相當(dāng)于一個(gè)包工頭,主管監(jiān)聽端口,而slave進(jìn)程被用于實(shí)際的任務(wù)執(zhí)行,當(dāng)任務(wù)請(qǐng)求到達(dá)后,它會(huì)根據(jù)某種方式將連接循環(huán)分發(fā)給worker進(jìn)程來處理。理論上,如果根據(jù)當(dāng)前各個(gè)worker進(jìn)程的負(fù)載狀況或者相關(guān)信息來挑選工作進(jìn)程,效率應(yīng)該比直接循環(huán)發(fā)放要更高,但node.js文檔中聲明這種方式由于受到操作系統(tǒng)調(diào)度機(jī)制的影響,會(huì)使得分發(fā)變得不穩(wěn)定,所以會(huì)將"循環(huán)法"作為默認(rèn)的分發(fā)策略。
關(guān)于cluster模塊的用法和API細(xì)節(jié),可以直接參考官方文檔《Node.js中文網(wǎng)V10.15.3/cluster》。
二. 線程與進(jìn)程
想要盡可能利用服務(wù)器性能,首先需要了解“線程”(thread)和“進(jìn)程”(process)這兩個(gè)概念。
計(jì)算機(jī)是由CPU來執(zhí)行計(jì)算任務(wù)的,如果你只有一個(gè)CPU,那么這臺(tái)機(jī)器上所有的任務(wù)都將由它來執(zhí)行。它既可以按照串聯(lián)執(zhí)行的原則一個(gè)接一個(gè)執(zhí)行任務(wù),也可以依據(jù)并聯(lián)原則同步執(zhí)行多個(gè)任務(wù),多個(gè)任務(wù)同步執(zhí)行時(shí),CPU會(huì)快速在多個(gè)線程之間進(jìn)行切換,切換線程的同時(shí)要切換對(duì)應(yīng)任務(wù)的上下文,這就會(huì)造成額外的CPU資源消耗,所以當(dāng)線程數(shù)量非常多時(shí),線程切換本身就會(huì)浪費(fèi)大量的CPU資源。如果在執(zhí)行一個(gè)任務(wù)的同時(shí),CPU和內(nèi)存都還有充足的剩余,就可以通過某種方式讓它們?nèi)?zhí)行其他任務(wù)。
你可以將“線程”看作是一種輕量級(jí)的“進(jìn)程”。
如果你在操作系統(tǒng)中打開任務(wù)管理器,在進(jìn)程標(biāo)簽下就可以看到如下圖的示例:
我們可以看到每一個(gè)程序至少開辟一個(gè)新的進(jìn)程(你可能瞬間就明白了chrome效率高的原因,我什么都沒說),它是一種粒度更大的資源隔離單元,進(jìn)程之間使用不同的內(nèi)存區(qū)域,無法直接共享數(shù)據(jù),只能通過跨進(jìn)程通訊機(jī)制來通訊,而且由于要使用新的內(nèi)存區(qū)域,它的創(chuàng)建銷毀和切換相對(duì)而言都更耗時(shí),它的好處就是進(jìn)程之間是互相隔離的,互不影響,所以你可以一邊聽音樂一邊玩游戲,而不會(huì)因?yàn)橐魳奋浖锿蝗环帕艘皇纵p音樂,結(jié)果你游戲里的角色攻擊力減半了。
再來看一下性能這個(gè)標(biāo)簽:
可以看到線程數(shù)是遠(yuǎn)大于進(jìn)程數(shù)的?!熬€程”通常用來在單個(gè)“進(jìn)程”中提高CPU的利用率,它是一種粒度更細(xì)的資源調(diào)度單位,它更容易創(chuàng)建和銷毀,在同一個(gè)進(jìn)程內(nèi)的線程共享分配給這個(gè)進(jìn)程的內(nèi)存,所以也就實(shí)現(xiàn)了共享數(shù)據(jù),多線程的編程要更加復(fù)雜,由于共享數(shù)據(jù),如果線程之間傳遞指針然后操作同一數(shù)據(jù)源,就必須考慮“原子操作”和“鎖”的問題,否則很容易就亂套了,如果傳遞數(shù)據(jù)的拷貝,又會(huì)造成內(nèi)存浪費(fèi),另外線程異常不會(huì)被隔離,而會(huì)導(dǎo)致整個(gè)進(jìn)程異常。
線程和進(jìn)程的相關(guān)知識(shí)涉及到底層操作系統(tǒng)的內(nèi)容,筆者涉獵有限,先分享這么多(會(huì)的都告訴你了,還要我怎樣)。
三. cluster模塊源碼解析
源碼中個(gè)別方法比較長(zhǎng),建議使用帶有代碼折疊的工具來看。
3.1 起步
cluster模塊的用法看起來并不復(fù)雜,官方給出的示例是這樣的:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`主進(jìn)程 ${process.pid} 正在運(yùn)行`);
// 衍生工作進(jìn)程。
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`工作進(jìn)程 ${worker.process.pid} 已退出`);
});
} else {
// 工作進(jìn)程可以共享任何 TCP 連接。
// 在本例子中,共享的是 HTTP 服務(wù)器。
http.createServer((req, res) => {
res.writeHead(200);
res.end('你好世界\n');
}).listen(8000);
console.log(`工作進(jìn)程 ${process.pid} 已啟動(dòng)`);
}
3.2 入口
cluster模塊的入口在/lib/cluster.js,這里的代碼很簡(jiǎn)單:
'use strict';
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);
可以看到,如果進(jìn)程對(duì)象的環(huán)境變量中有NODE_UNIQUE_ID這個(gè)變量,就透?jìng)鱥nternal/cluster/child.js模塊的輸出,否則就透?jìng)鱥nternal/cluster/master.js模塊的輸出。這是node的主進(jìn)程在進(jìn)行子進(jìn)程管理時(shí)的標(biāo)識(shí),后面的代碼中可以看到當(dāng)調(diào)用cluster.fork( )生成一個(gè)子進(jìn)程時(shí)會(huì)以一個(gè)自增ID的形式生成這個(gè)環(huán)境變量。
3.3 主進(jìn)程模塊master.js
首先運(yùn)行node程序的肯定是主線程,那么我們從master.js這個(gè)模塊開始,先用工具折疊一下代碼瀏覽一下:
可以看到除了模塊屬性外,cluster模塊對(duì)外暴露的方法只有下面3個(gè),其他的都是用來完成內(nèi)部功能的:
setupMaster(options )-修改fork時(shí)默認(rèn)設(shè)置
fork( )-生成子進(jìn)程
disconnect( )- 斷開和所有子進(jìn)程的連接
我們按照官方示例的邏輯路線來閱讀代碼cluster.fork( )方法定義在161-217行,一樣是用折疊工具來看全貌:
可以看到cluster.fork( )執(zhí)行時(shí)做了如下幾件事情:
1.設(shè)置主線程參數(shù)
2.傳入一個(gè)自增參數(shù)id(就是前文提到的NODE_UNIQUE_ID)和環(huán)境信息env來生成一個(gè)worker線程的process對(duì)象
3.將id和新的process對(duì)象傳入Worker構(gòu)造器生成新的worker進(jìn)程實(shí)例
4.在子進(jìn)程的process對(duì)象上添加了一些事件監(jiān)聽
5.在cluster.workers中以id為鍵添加對(duì)子進(jìn)程的引用
6.返回子進(jìn)程worker實(shí)例
接著看第一步setupMaster( ),在源碼中50-95行,著重看81-95行:
留意一下主線程在進(jìn)程層面監(jiān)聽的internalMessage事件非常關(guān)鍵,主進(jìn)程監(jiān)聽到這個(gè)事件后,首先判斷消息對(duì)象的cmd屬性是否為NODE_DEBUGE_ENABLED,并以此為條件判斷后續(xù)語句是否執(zhí)行,后續(xù)的邏輯是遍歷每一個(gè)worker進(jìn)程實(shí)例,如果子進(jìn)程的狀態(tài)是online或listening就將子進(jìn)程pid作為參數(shù)調(diào)用主進(jìn)程的_debugProcess( )方法,否則改為在worker進(jìn)程實(shí)例首次上線時(shí)調(diào)用。
process._debugProcess的定義在src/node_process_methods.cc里,看名字推測(cè)大致的意思就是為了啟用對(duì)子進(jìn)程的調(diào)試功能。這是一個(gè)重載方法,在windows和linux下有不同的實(shí)現(xiàn)。linux下的代碼較短,基本可以看懂(不秀一下怎么對(duì)得住自己看1周的C++):
#ifdef __POSIX__
static void DebugProcess(const FunctionCallbackInfo
//這里的常量參數(shù)是通過地址引用的worker.process.pid
Environment* env = Environment::GetCurrent(args);
//用pid做參數(shù)獲取當(dāng)前激活的環(huán)境變量,這一步應(yīng)該是在獲取上下文
if (args.Length() != 1) {//不合法調(diào)用時(shí)報(bào)錯(cuò),沒什么可說的
return env->ThrowError("Invalid number of arguments.");
}
CHECK(args[0]->IsNumber());//檢測(cè)參數(shù)
pid_t pid = args[0].As
int r = kill(pid, SIGUSR1);//發(fā)送SIGUSR1信號(hào),終止了這個(gè)子進(jìn)程
if (r != 0) {//exit code為0時(shí)是正常退出,子進(jìn)程未能正常中止時(shí)報(bào)錯(cuò)
return env->ThrowErrnoException(errno, "kill");
}
}
win32平臺(tái)中對(duì)應(yīng)的代碼比較長(zhǎng),看不懂??偨Y(jié)一下這里就是,在沒有收到cmd屬性等于NODE_DEBUG_ENABLED的內(nèi)部消息之前,什么都不做,如果收到這個(gè)消息,就終止所有的子進(jìn)程,或者通過事件在子進(jìn)程第一次處于online狀態(tài)就終止它。
按照?qǐng)?zhí)行順序接下來是101-140行的createWorkerProcess(id,env)方法,看名字就知道是生成子進(jìn)程process對(duì)象的,前半部分合并和處理環(huán)境參數(shù),然后判斷運(yùn)行參數(shù)中是否包含啟用--inspect功能的參數(shù)并進(jìn)行一些處理,最后傳入一堆參數(shù)調(diào)用了fork方法,這個(gè)方法就是child_process.fork( ),它就是用來生成子進(jìn)程的,返回值就是子進(jìn)程實(shí)例,你可以先簡(jiǎn)單瀏覽一下API【官方文檔child_process.fork功能】,或者知道這里生成了子進(jìn)程就好。
回到cluster.fork方法繼續(xù)執(zhí)行,下一步使用新生成的子進(jìn)程process對(duì)象和唯一id作為參數(shù)傳入Worker構(gòu)造函數(shù),生成worker實(shí)例,Worker的定義就在當(dāng)前文件夾的worker.js中,它首先繼承了EventEmitter的消息的發(fā)布訂閱能力,然后把子進(jìn)程的process對(duì)象掛在在自己的process屬性上,接著為子進(jìn)程添加error和 message事件的監(jiān)聽,最后暴露了一些更語義化的針對(duì)進(jìn)程實(shí)例的管理方法(更詳細(xì)的分析可以參考本系列前一篇博文)。生成了worker進(jìn)程實(shí)例后,添加了對(duì)于message事件的響應(yīng),并在子進(jìn)程process對(duì)象上監(jiān)聽進(jìn)程的exit,disconnect,internalMessage事件,最后將worker實(shí)例和自己的id以鍵值對(duì)的形式添加到cluster.workers中記錄,并通過return返回給外界,至此master模塊的初始化流程就告一段落,先mark一下,后面還會(huì)講這里。
3.4 子進(jìn)程模塊child.js
子進(jìn)程模塊是從master.js調(diào)用child_process時(shí)啟動(dòng)的,它和主進(jìn)程是并行執(zhí)行的。老規(guī)矩,代碼折疊看一下:
看出什么了嗎?child.js的代碼里只有引用和定義,_setupWorker是在nodejs工作進(jìn)程初始化時(shí)執(zhí)行的,它在自己的獨(dú)立進(jìn)程中初始化了一個(gè)進(jìn)程管理實(shí)例,并執(zhí)行了下述邏輯:
1.實(shí)例化進(jìn)程管理對(duì)象worker
2.全局添加`disconnect`事件響應(yīng)
3.全局添加`internalMessage`事件響應(yīng),主要是分發(fā)`act:newconn`和`act:disconnect`事件
4.用send方法發(fā)送`online`事件,通知主線程自己已上線。
注意,這個(gè)process對(duì)象就是IPC(Inter Process Communication,也稱為跨進(jìn)程通訊)能夠?qū)崿F(xiàn)的關(guān)鍵,很明顯它繼承了EventEmitter的消息收發(fā)能力,在子進(jìn)程內(nèi)部進(jìn)行消息收發(fā)不存在任何問題,還記得master.js中fork方法嗎?這個(gè)process就是調(diào)用child_process啟動(dòng)子進(jìn)程時(shí)返回給主進(jìn)程的那個(gè)process對(duì)象,當(dāng)你在主進(jìn)程中獲取它后,就可以共享worker進(jìn)程的消息能力,從而在資源隔離的條件下實(shí)現(xiàn)master和worker進(jìn)程的跨進(jìn)程通訊。_getServer( )方法是在建立server實(shí)例時(shí)調(diào)用的,等到驅(qū)動(dòng)事件信息到達(dá)child.js時(shí)再看,可以留意一下最后兩個(gè)添加在Worker原型方法上的方法,它們只在子進(jìn)程中有效。
四. 小結(jié)
至此,你已經(jīng)看到node是如何通過cluster模塊實(shí)現(xiàn)多實(shí)例并初始化跨進(jìn)程通訊了。但是跨進(jìn)程通訊的底層實(shí)現(xiàn)以及服務(wù)器的建立,以及如何在進(jìn)程間協(xié)調(diào)網(wǎng)絡(luò)請(qǐng)求的處理,還依賴于net和http的一些內(nèi)容,只好等研究完了再繼續(xù),硬剛反正我是吃不消的。
版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。