您好,登錄后才能下訂單哦!
Node.js中如何使用Cluster模塊,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
1.為什么我的應(yīng)用代碼中明明有app.listen(port);,但cluter模塊在多次fork這份代碼時,卻沒有報端口已被占用?
2.Master是如何將接收的請求傳遞至worker中進行處理然后響應(yīng)的?
帶著這些疑問我們開始往下看
TIPS:
本文編寫于2019年12月8日,是最新版本的Node.js源碼
Cluster源碼解析:
入口 :
const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master'; module.exports = require(`internal/cluster/${childOrMaster}`);
分析
會根據(jù)一個當(dāng)前的Node_UNIQUE_ID(后面會講)是否在環(huán)境變量中判斷是子進程還是主進程,然后引用不同的js代碼
NODE_UNIQUE_ID是一個唯一標(biāo)示,Node.js的Cluster多進程模式,采用默認的調(diào)度算法是round-robin,其實就是輪詢.官方解釋是實踐效率非常高,穩(wěn)定
之前的問題一: 為什么我的應(yīng)用代碼中明明有app.listen(port);,但cluter模塊在多次fork這份代碼時,卻沒有報端口已被占用?
我在Node.js的官網(wǎng)找到了答案:
原來所有的net.Socket都被設(shè)置了SO_REUSEADDR
這個SO_REUSEADDR到底是什么呢?
為什么需要 SO_REUSEADDR 參數(shù)?
服務(wù)端主動斷開連接以后,需要等 2 個 MSL 以后才最終釋放這個連接,重啟以后要綁定同一個端口,默認情況下,操作系統(tǒng)的實現(xiàn)都會阻止新的監(jiān)聽套接字綁定到這個端口上。
我們都知道 TCP 連接由四元組唯一確定。形式如下
{local-ip-address:local-port , foreign-ip-address:foreign-port}
一個典型的例子如下圖
TCP 要求這樣的四元組必須是唯一的,但大多數(shù)操作系統(tǒng)的實現(xiàn)要求更加嚴(yán)格,只要還有連接在使用這個本地端口,則本地端口不能被重用(bind 調(diào)用失?。?/strong>
啟用 SO_REUSEADDR 套接字選項可以解除這個限制,默認情況下這個值都為 0,表示關(guān)閉。在 Java 中,reuseAddress 不同的 JVM 有不同的實現(xiàn),在我本機上,這個值默認為 1 允許端口重用。但是為了保險起見,寫 TCP、HTTP 服務(wù)一定要主動設(shè)置這個參數(shù)為 1。
目前常見的網(wǎng)絡(luò)編程模型就是多進程或多線程,根據(jù)accpet的位置,分為如下場景
2種場景
(1) 單進程或線程創(chuàng)建socket,并進行l(wèi)isten和accept,接收到連接后創(chuàng)建進程和線程處理連接
(2) 單進程或線程創(chuàng)建socket,并進行l(wèi)isten,預(yù)先創(chuàng)建好多個工作進程或線程accept()在同一個服務(wù)器套接字
這兩種模型解充分發(fā)揮了多核CPU的優(yōu)勢,雖然可以做到線程和CPU核綁定,但都會存在:
1.單一listener工作進程或線程在高速的連接接入處理時會成為瓶頸
2.多個線程之間競爭獲取服務(wù)套接字
3.緩存行跳躍
4.很難做到CPU之間的負載均衡
5.隨著核數(shù)的擴展,性能并沒有隨著提升
6.SO_REUSEPORT解決了什么問題
7.SO_REUSEPORT支持多個進程或者線程綁定到同一端口,提高服務(wù)器程序的性能
解決的問題:
1.允許多個套接字 bind()/listen() 同一個TCP/UDP端口
2.每一個線程擁有自己的服務(wù)器套接字
3.在服務(wù)器套接字上沒有了鎖的競爭
4.內(nèi)核層面實現(xiàn)負載均衡
5.安全層面,監(jiān)聽同一個端口的套接字只能位于同一個用戶下面
其核心的實現(xiàn)主要有三點:
1.擴展 socket option,增加 SO_REUSEPORT 選項,用來設(shè)置 reuseport
2.修改 bind 系統(tǒng)調(diào)用實現(xiàn),以便支持可以綁定到相同的 IP 和端口
3.修改處理新建連接的實現(xiàn),查找 listener 的時候,能夠支持在監(jiān)聽相同 IP 4.和端口的多個 sock 之間均衡選擇。
5.有了SO_RESUEPORT后,每個進程可以自己創(chuàng)建socket、bind、listen、accept相同的地址和端口,各自是獨立平等的
讓多進程監(jiān)聽同一個端口,各個進程中accept socket fd不一樣,有新連接建立時,內(nèi)核只會喚醒一個進程來accept,并且保證喚醒的均衡性。
總結(jié):原來端口被復(fù)用是因為設(shè)置了SO_REUSEADDR,當(dāng)然不止這一點,下面會繼續(xù)描述
回到源碼第一行
NODE_UNIQUE_ID是什么?
下面給出介紹:
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 }); }
原來,創(chuàng)建子進程的時候,給了每個進程一個唯一的自增標(biāo)示ID
隨后Node.js在初始化時,會根據(jù)該環(huán)境變量,來判斷該進程是否為cluster模塊fork出的工作進程,若是,則執(zhí)行workerInit()函數(shù)來初始化環(huán)境,否則執(zhí)行masterInit()函數(shù)
就是這行入口的代碼~
module.exports = require(`internal/cluster/${childOrMaster}`);
接下來我們需要看一下net模塊的listen函數(shù)源碼:
// 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); } }
仔細一看,原來listen函數(shù)會根據(jù)是不是主進程做不同的操作!
上面有提到SO_REUSEADDR選項,在主進程調(diào)用的_listen2中就有設(shè)置。
子進程初始化的每個workerinit函數(shù)中,也有cluster._getServer這個方法,
你可能已經(jīng)猜到,問題一的答案,就在這個cluster._getServer函數(shù)的代碼中。它主要干了兩件事:
向master進程注冊該worker,若master進程是第一次接收到監(jiān)聽此端口/描述符下的worker,則起一個內(nèi)部TCP服務(wù)器,來承擔(dān)監(jiān)聽該端口/描述符的職責(zé),隨后在master中記錄下該worker。
Hack掉worker進程中的net.Server實例的listen方法里監(jiān)聽端口/描述符的部分,使其不再承擔(dān)該職責(zé)。
對于第一件事,由于master在接收,傳遞請求給worker時,會符合一定的負載均衡規(guī)則(在非Windows平臺下默認為輪詢),這些邏輯被封裝在RoundRobinHandle類中。故,初始化內(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. /// ... }
在子進程中:
function listen(backlog) { return 0; } function close() { // ... } function ref() {} function unref() {} var handle = { close: close, listen: listen, ref: ref, unref: unref, }
由于net.Server實例的listen方法,最終會調(diào)用自身_handle屬性下listen方法來完成監(jiān)聽動作,故在代碼中修改之:此時的listen方法已經(jīng)被hack ,每次調(diào)用只能發(fā)揮return 0 ,并不會監(jiān)聽端口
// 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; // ... } }
這里可以看到,傳入的回調(diào)函數(shù)中的handle,已經(jīng)把listen方法重新定義,返回0,那么等子進程調(diào)用listen方法時候,也是返回0,并不會去監(jiān)聽端口,至此,煥然大悟,原來是這樣,真正監(jiān)聽端口的始終只有主進程!
上面通過將近3000字講解,把端口復(fù)用這個問題講清楚了,下面把負載均衡這塊也講清楚。然后再講PM2的原理實現(xiàn),其實不過是對cluster模式進行了封裝,多了很多功能而已~
首先畫了一個流程圖
核心實現(xiàn)源碼:
function RoundRobinHandle(key, address, port, addressType, backlog, fd) { // ... this.server = net.createServer(assert.fail); // ... var self = this; this.server.once('listening', function() { // ... selfself.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) { // ... });
解析
定義好handle對象中的onconnection方法
觸發(fā)事件時,取出一個子進程通知,傳入句柄
子進程接受到消息和句柄后,做相應(yīng)的業(yè)務(wù)處理:
var accepted = server !== undefined; // ... if (accepted) server.onconnection(0, handle);// lib/cluster.js // ... // 該方法會在Node.js初始化時由 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) { // ... }
總結(jié)下來,負載均衡大概流程:
1.所有請求先同一經(jīng)過內(nèi)部TCP服務(wù)器,真正監(jiān)聽端口的只有主進程。
2.在內(nèi)部TCP服務(wù)器的請求處理邏輯中,有負載均衡地挑選出一個worker進程,將其發(fā)送一個newconn內(nèi)部消息,隨消息發(fā)送客戶端句柄。
3.Worker進程接收到此內(nèi)部消息,根據(jù)客戶端句柄創(chuàng)建net.Socket實例,執(zhí)行具體業(yè)務(wù)邏輯,返回。
至此,Cluster多進程模式,負載均衡講解完畢,下面講PM2的實現(xiàn)原理,它是基于Cluster模式的封裝
PM2的使用:
npm i pm2 -g pm2 start app.js pm2 ls
這樣就可以啟動你的Node.js服務(wù),并且根據(jù)你的電腦CPU個數(shù)去啟動相應(yīng)的進程數(shù),監(jiān)聽到錯誤事件,自帶重啟子進程,即使更新了代碼,需要熱更新,也會逐個替換,號稱永動機。
它的功能:
1.內(nèi)建負載均衡(使用Node cluster 集群模塊)
2.后臺運行
3.0秒停機重載,我理解大概意思是維護升級的時候不需要停機.
4.具有Ubuntu和CentOS 的啟動腳本
5.停止不穩(wěn)定的進程(避免無限循環(huán))
6.控制臺檢測
7.提供 HTTP API
8.遠程控制和實時的接口API ( Nodejs 模塊,允許和PM2進程管理器交互 )
先來一張PM2的架構(gòu)圖:
pm2包括 Satan進程、God Deamon守護進程、進程間的遠程調(diào)用rpc、cluster等幾個概念
如果不知道點西方文化,還真搞不清他的文件名為啥是 Satan 和 God:
撒旦(Satan),主要指《圣經(jīng)》中的墮天使(也稱墮天使撒旦),被看作與上帝的力量相對的邪惡、黑暗之源,是God的對立面。
1.Satan.js提供了程序的退出、殺死等方法,因此它是魔鬼;God.js 負責(zé)維護進程的正常運行,當(dāng)有異常退出時能保證重啟,所以它是上帝。作者這么命名,我只能說一句:oh my god。
God進程啟動后一直運行,它相當(dāng)于cluster中的Master進程,守護者worker進程的正常運行。
2.rpc(Remote Procedure Call Protocol)是指遠程過程調(diào)用,也就是說兩臺服務(wù)器A,B,一個應(yīng)用部署在A服務(wù)器上,想要調(diào)用B服務(wù)器上應(yīng)用提供的函數(shù)/方法,由于不在一個內(nèi)存空間,不能直接調(diào)用,需要通過網(wǎng)絡(luò)來表達調(diào)用的語義和傳達調(diào)用的數(shù)據(jù)。同一機器不同進程間的方法調(diào)用也屬于rpc的作用范疇。
3.代碼中采用了axon-rpc 和 axon 兩個庫,基本原理是提供服務(wù)的server綁定到一個域名和端口下,調(diào)用服務(wù)的client連接端口實現(xiàn)rpc連接。后續(xù)新版本采用了pm2-axon-rpc 和 pm2-axon兩個庫,綁定的方法也由端口變成.sock文件,因為采用port可能會和現(xiàn)有進程的端口產(chǎn)生沖突。
執(zhí)行流程
程序的執(zhí)行流程圖如下:
每次命令行的輸入都會執(zhí)行一次satan程序。如果God進程不在運行,首先需要啟動God進程。然后根據(jù)指令,satan通過rpc調(diào)用God中對應(yīng)的方法執(zhí)行相應(yīng)的邏輯。
以 pm2 start app.js -i 4為例,God在初次執(zhí)行時會配置cluster,同時監(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進程在執(zhí)行 God.clusters_db[clu.pm_id].status = 'online'; }); // 命令行中 kill pid 會觸發(fā)exit事件,process.kill不會觸發(fā)exit cluster.on('exit', function(clu, code, signal) { // 重啟進程 如果重啟次數(shù)過于頻繁直接標(biāo)注為stopped God.clusters_db[clu.pm_id].status = 'starting'; // 邏輯 ... }); })();
在God啟動后, 會建立Satan和God的rpc鏈接,然后調(diào)用prepare方法。prepare方法會調(diào)用cluster.fork,完成集群的啟動
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; }
PM2的功能目前已經(jīng)特別多了,源碼閱讀非常耗時,但是可以猜測到一些功能的實現(xiàn):
例如
如何檢測子進程是否處于正?;钴S狀態(tài)?
采用心跳檢測
每隔數(shù)秒向子進程發(fā)送心跳包,子進程如果不回復(fù),那么調(diào)用kill殺死這個進程 然后再重新cluster.fork()一個新的進程
子進程發(fā)出異常報錯,如何保證一直有一定數(shù)量子進程?
子進程可以監(jiān)聽到錯誤事件,這時候可以發(fā)送消息給主進程,請求殺死自己 并且主進程此時重新調(diào)用cluster.fork一個新的子進程
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。