詳解通過(guò)源碼解析Node.js中cluster模塊的主要功能實(shí)現(xiàn)
眾所周知,Node.js中的JavaScript代碼執(zhí)行在單線(xiàn)程中,非常脆弱,一旦出現(xiàn)了未捕獲的異常,那么整個(gè)應(yīng)用就會(huì)崩潰。這在許多場(chǎng)景下,尤其是web應(yīng)用中,是無(wú)法忍受的。通常的解決方案,便是使用Node.js中自帶的cluster模塊,以master-worker模式啟動(dòng)多個(gè)應(yīng)用實(shí)例。然而大家在享受cluster模塊帶來(lái)的福祉的同時(shí),不少人也開(kāi)始好奇:
- 為什么我的應(yīng)用代碼中明明有app.listen(port);,但cluter模塊在多次fork這份代碼時(shí),卻沒(méi)有報(bào)端口已被占用?
- Master是如何將接收的請(qǐng)求傳遞至worker中進(jìn)行處理然后響應(yīng)的?
讓我們從Node.js項(xiàng)目的lib/cluster.js中的代碼里,來(lái)一勘究竟。
問(wèn)題一
為了得到這個(gè)問(wèn)題的解答,我們先從worker進(jìn)程的初始化看起,master進(jìn)程在fork工作進(jìn)程時(shí),會(huì)為其附上環(huán)境變量NODE_UNIQUE_ID,是一個(gè)從零開(kāi)始的遞增數(shù):
// lib/cluster.js // ... function createWorkerProcess(id, env) { // ... workerEnv.NODE_UNIQUE_ID = '' + id; // ... return fork(cluster.settings.exec, cluster.settings.args, { env: workerEnv, silent: cluster.settings.silent, execArgv: execArgv, gid: cluster.settings.gid, uid: cluster.settings.uid }); }
隨后Node.js在初始化時(shí),會(huì)根據(jù)該環(huán)境變量,來(lái)判斷該進(jìn)程是否為cluster模塊fork出的工作進(jìn)程,若是,則執(zhí)行workerInit()函數(shù)來(lái)初始化環(huán)境,否則執(zhí)行masterInit()函數(shù)。
在workerInit()函數(shù)中,定義了cluster._getServer方法,這個(gè)方法在任何net.Server實(shí)例的listen方法中,會(huì)被調(diào)用:
// lib/net.js // ... function listen(self, address, port, addressType, backlog, fd, exclusive) { exclusive = !!exclusive; if (!cluster) cluster = require('cluster'); if (cluster.isMaster || exclusive) { self._listen2(address, port, addressType, backlog, fd); return; } cluster._getServer(self, { address: address, port: port, addressType: addressType, fd: fd, flags: 0 }, cb); function cb(err, handle) { // ... self._handle = handle; self._listen2(address, port, addressType, backlog, fd); } }
你可能已經(jīng)猜到,問(wèn)題一的答案,就在這個(gè)cluster._getServer函數(shù)的代碼中。它主要干了兩件事:
- 向master進(jìn)程注冊(cè)該worker,若master進(jìn)程是第一次接收到監(jiān)聽(tīng)此端口/描述符下的worker,則起一個(gè)內(nèi)部TCP服務(wù)器,來(lái)承擔(dān)監(jiān)聽(tīng)該端口/描述符的職責(zé),隨后在master中記錄下該worker。
- Hack掉worker進(jìn)程中的net.Server實(shí)例的listen方法里監(jiān)聽(tīng)端口/描述符的部分,使其不再承擔(dān)該職責(zé)。
對(duì)于第一件事,由于master在接收,傳遞請(qǐng)求給worker時(shí),會(huì)符合一定的負(fù)載均衡規(guī)則(在非Windows平臺(tái)下默認(rèn)為輪詢(xún)),這些邏輯被封裝在RoundRobinHandle類(lèi)中。故,初始化內(nèi)部TCP服務(wù)器等操作也在此處:
// lib/cluster.js // ... function RoundRobinHandle(key, address, port, addressType, backlog, fd) { // ... this.handles = []; this.handle = null; this.server = net.createServer(assert.fail); if (fd >= 0) this.server.listen({ fd: fd }); else if (port >= 0) this.server.listen(port, address); else this.server.listen(address); // UNIX socket path. /// ... }
對(duì)于第二件事,由于net.Server實(shí)例的listen方法,最終會(huì)調(diào)用自身_handle屬性下listen方法來(lái)完成監(jiān)聽(tīng)動(dòng)作,故在代碼中修改之:
// lib/cluster.js // ... function rr(message, cb) { // ... // 此處的listen函數(shù)不再做任何監(jiān)聽(tīng)動(dòng)作 function listen(backlog) { return 0; } function close() { // ... } function ref() {} function unref() {} var handle = { close: close, listen: listen, ref: ref, unref: unref, }; // ... handles[key] = handle; cb(0, handle); // 傳入這個(gè)cb中的handle將會(huì)被賦值給net.Server實(shí)例中的_handle屬性 } // lib/net.js // ... function listen(self, address, port, addressType, backlog, fd, exclusive) { // ... if (cluster.isMaster || exclusive) { self._listen2(address, port, addressType, backlog, fd); return; // 僅在worker環(huán)境下改變 } cluster._getServer(self, { address: address, port: port, addressType: addressType, fd: fd, flags: 0 }, cb); function cb(err, handle) { // ... self._handle = handle; // ... } }
至此,第一個(gè)問(wèn)題便已豁然開(kāi)朗了,總結(jié)下:
- 端口僅由master進(jìn)程中的內(nèi)部TCP服務(wù)器監(jiān)聽(tīng)了一次。
- 不會(huì)出現(xiàn)端口被重復(fù)監(jiān)聽(tīng)報(bào)錯(cuò),是由于,worker進(jìn)程中,最后執(zhí)行監(jiān)聽(tīng)端口操作的方法,已被cluster模塊主動(dòng)hack。
問(wèn)題二
解決了問(wèn)題一,問(wèn)題二的解決就明朗輕松許多了。通過(guò)問(wèn)題一我們已得知,監(jiān)聽(tīng)端口的是master進(jìn)程中創(chuàng)建的內(nèi)部TCP服務(wù)器,所以第二個(gè)問(wèn)題的解決,著手點(diǎn)就是該內(nèi)部TCP服務(wù)器接手連接時(shí),執(zhí)行的操作。Cluster模塊的做法是,監(jiān)聽(tīng)該內(nèi)部TCP服務(wù)器的connection事件,在監(jiān)聽(tīng)器函數(shù)里,有負(fù)載均衡地挑選出一個(gè)worker,向其發(fā)送newconn內(nèi)部消息(消息體對(duì)象中包含cmd: 'NODE_CLUSTER'屬性)以及一個(gè)客戶(hù)端句柄(即connection事件處理函數(shù)的第二個(gè)參數(shù)),相關(guān)代碼如下:
// lib/cluster.js // ... function RoundRobinHandle(key, address, port, addressType, backlog, fd) { // ... this.server = net.createServer(assert.fail); // ... var self = this; this.server.once('listening', function() { // ... self.handle.onconnection = self.distribute.bind(self); }); } RoundRobinHandle.prototype.distribute = function(err, handle) { this.handles.push(handle); var worker = this.free.shift(); if (worker) this.handoff(worker); }; RoundRobinHandle.prototype.handoff = function(worker) { // ... var message = { act: 'newconn', key: this.key }; var self = this; sendHelper(worker.process, message, handle, function(reply) { // ... }); };
Worker進(jìn)程在接收到了newconn內(nèi)部消息后,根據(jù)傳遞過(guò)來(lái)的句柄,調(diào)用實(shí)際的業(yè)務(wù)邏輯處理并返回:
// lib/cluster.js // ... // 該方法會(huì)在Node.js初始化時(shí)由 src/node.js 調(diào)用 cluster._setupWorker = function() { // ... process.on('internalMessage', internal(worker, onmessage)); // ... function onmessage(message, handle) { if (message.act === 'newconn') onconnection(message, handle); // ... } }; function onconnection(message, handle) { // ... var accepted = server !== undefined; // ... if (accepted) server.onconnection(0, handle); }
至此,問(wèn)題二也得到了解決,也總結(jié)一下:
- 所有請(qǐng)求先同一經(jīng)過(guò)內(nèi)部TCP服務(wù)器。
- 在內(nèi)部TCP服務(wù)器的請(qǐng)求處理邏輯中,有負(fù)載均衡地挑選出一個(gè)worker進(jìn)程,將其發(fā)送一個(gè)newconn內(nèi)部消息,隨消息發(fā)送客戶(hù)端句柄。
- Worker進(jìn)程接收到此內(nèi)部消息,根據(jù)客戶(hù)端句柄創(chuàng)建net.Socket實(shí)例,執(zhí)行具體業(yè)務(wù)邏輯,返回。
最后
Node.js中的cluster模塊除了上述提到的功能外,其實(shí)還提供了非常豐富的API供master和worker進(jìn)程之前通信,對(duì)于不同的操作系統(tǒng)平臺(tái),也提供了不同的默認(rèn)行為。本文僅挑選了一條功能線(xiàn)進(jìn)行了分析闡述。如果大家有閑,非常推薦完整領(lǐng)略一下cluster模塊的代碼實(shí)現(xiàn)。
參考:
https://github.com/nodejs/node/blob/master/lib/cluster.js
https://github.com/nodejs/node/blob/master/lib/net.js
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
NodeJS落地WebSocket實(shí)踐前端架構(gòu)師破局技術(shù)
這篇文章主要為大家介紹了NodeJS落地WebSocket實(shí)踐前端架構(gòu)師破局技術(shù),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06node.js中的console.timeEnd方法使用說(shuō)明
這篇文章主要介紹了node.js中的console.timeEnd方法使用說(shuō)明,本文介紹了console.timeEnd的方法說(shuō)明、語(yǔ)法、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12Node.js微信 access_token ( jsapi_ticket ) 存取與刷新的示例
本篇文章主要介紹了Node.js微信 access_token ( jsapi_ticket ) 存取與刷新的示例,具有一定的參考價(jià)值,有興趣的可以了解一一下‘2017-09-09nodejs中內(nèi)置模塊fs,path常見(jiàn)的用法說(shuō)明
這篇文章主要介紹了nodejs中內(nèi)置模塊fs,path常見(jiàn)的用法說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11nodejs中簡(jiǎn)單實(shí)現(xiàn)Javascript Promise機(jī)制的實(shí)例
這篇文章主要介紹了nodejs中簡(jiǎn)單實(shí)現(xiàn)Javascript Promise機(jī)制的實(shí)例,本文在nodejs中簡(jiǎn)單實(shí)現(xiàn)一個(gè)promise/A 規(guī)范,需要的朋友可以參考下2014-12-12node.js?express和koa中間件機(jī)制和錯(cuò)誤處理機(jī)制
這篇文章主要介紹了node.js?express和koa中間件機(jī)制和錯(cuò)誤處理機(jī)制,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-07-07