示例代碼託管在:https://www.github.com/dashnowords/blogs前端
博客園地址:《大史住在大前端》原創博文目錄node
華爲雲社區地址:【你要的前端打怪升級指南】linux
cluster
模塊是node.js
中用於實現和管理多進程的模塊。常規的node.js
應用程序是單線程單進程的,這也意味着它很難充分利用服務器多核CPU的性能,而cluster
模塊就是爲了解決這個 問題的,它使得node.js
程序能夠以多個實例並存的方式運行在不一樣的進程中,以求更大地榨取服務器的性能。node.js
在官方示例代碼中使用worker
實例來表示主進程fork出的子進程,使得前端開發者在學習過程當中很是容易和瀏覽器環境中的worker
實現的多線程混淆。爲了容易區分,咱們和node
官方文檔使用一致的名稱,用集羣中的master
和worker
來區分主進程和工做進程,用worker_threads
來描述工做線程。git
node.js
的主從模型中,master
主進程至關於一個包工頭,主管監聽端口,而slave
進程被用於實際的任務執行,當任務請求到達後,它會根據某種方式將鏈接循環分發給worker
進程來處理。理論上,若是根據當前各個worker
進程的負載情況或者相關信息來挑選工做進程,效率應該比直接循環發放要更高,但node.js
文檔中聲明這種方式因爲受到操做系統調度機制的影響,會使得分發變得不穩定,因此會將"循環法"做爲默認的分發策略。github
關於cluster
模塊的用法和API細節,能夠直接參考官方文檔《Node.js中文網V10.15.3/cluster》。chrome
想要儘量利用服務器性能,首先須要瞭解「線程」(thread)和「進程」(process)這兩個概念。編程
計算機是由CPU來執行計算任務的,若是你只有一個CPU,那麼這臺機器上全部的任務都將由它來執行。它既能夠按照串聯執行的原則一個接一個執行任務,也能夠依據並聯原則同步執行多個任務,多個任務同步執行時,CPU會快速在多個線程之間進行切換,切換線程的同時要切換對應任務的上下文,這就會形成額外的CPU資源消耗,因此當線程數量很是多時,線程切換自己就會浪費大量的CPU資源。若是在執行一個任務的同時,CPU和內存都還有充足的剩餘,就能夠經過某種方式讓它們去執行其餘任務。windows
你能夠將「線程」看做是一種輕量級的「進程」。api
若是你在操做系統中打開任務管理器,在進程
標籤下就能夠看到以下圖的示例:
咱們能夠看到每個程序至少開闢一個新的進程(你可能瞬間就明白了chrome效率高的緣由,我什麼都沒說),它是一種粒度更大的資源隔離單元,進程之間使用不一樣的內存區域,沒法直接共享數據,只能經過跨進程通信機制來通信,並且因爲要使用新的內存區域,它的建立銷燬和切換相對而言都更耗時,它的好處就是進程之間是互相隔離的,互不影響,因此你能夠一邊聽音樂一邊玩遊戲,而不會由於音樂軟件裏忽然放了一首輕音樂,結果你遊戲裏的角色攻擊力減半了。
再來看一下性能
這個標籤:
能夠看到線程數是遠大於進程數的。「線程」一般用來在單個「進程」中提升CPU的利用率,它是一種粒度更細的資源調度單位,它更容易建立和銷燬,在同一個進程內的線程共享分配給這個進程的內存,因此也就實現了共享數據,多線程的編程要更加複雜,因爲共享數據,若是線程之間傳遞指針而後操做同一數據源,就必須考慮「原子操做」和「鎖」的問題,不然很容易就亂套了,若是傳遞數據的拷貝,又會形成內存浪費,另外線程異常不會被隔離,而會致使整個進程異常。
線程和進程的相關知識涉及到底層操做系統的內容,筆者涉獵有限,先分享這麼多(會的都告訴你了,還要我怎樣)。
源碼中個別方法比較長,建議使用帶有代碼摺疊的工具來看。
cluster
模塊的用法看起來並不複雜,官方給出的示例是這樣的:
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; if (cluster.isMaster) { console.log(`主進程 ${process.pid} 正在運行`); // 衍生工做進程。 for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`工做進程 ${worker.process.pid} 已退出`); }); } else { // 工做進程能夠共享任何 TCP 鏈接。 // 在本例子中,共享的是 HTTP 服務器。 http.createServer((req, res) => { res.writeHead(200); res.end('你好世界\n'); }).listen(8000); console.log(`工做進程 ${process.pid} 已啓動`); }
cluster
模塊的入口在/lib/cluster.js
,這裏的代碼很簡單:
'use strict'; const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master'; module.exports = require(`internal/cluster/${childOrMaster}`);
能夠看到,若是進程對象的環境變量中有NODE_UNIQUE_ID
這個變量,就透傳internal/cluster/child.js
模塊的輸出,不然就透傳internal/cluster/master.js
模塊的輸出。這是node
的主進程在進行子進程管理時的標識,後面的代碼中能夠看到當調用cluster.fork( )
生成一個子進程時會以一個自增ID的形式生成這個環境變量。
首先運行node
程序的確定是主線程,那麼咱們從master.js
這個模塊開始,先用工具摺疊一下代碼瀏覽一下:
能夠看到除了模塊屬性外,cluster模塊對外暴露的方法只有下面3個,其餘的都是用來完成內部功能的:
setupMaster(options )
-修改fork
時默認設置fork( )
-生成子進程disconnect( )
- 斷開和全部子進程的鏈接咱們按照官方示例的邏輯路線來閱讀代碼cluster.fork( )
方法定義在161-217行,同樣是用摺疊工具來看全貌:
能夠看到cluster.fork( )
執行時作了以下幾件事情:
1.設置主線程參數 2.傳入一個自增參數id(就是前文提到的NODE_UNIQUE_ID)和環境信息env來生成一個worker線程的process對象 3.將id和新的process對象傳入Worker構造器生成新的worker進程實例 4.在子進程的process對象上添加了一些事件監聽 5.在cluster.workers中以id爲鍵添加對子進程的引用 6.返回子進程worker實例
接着看第一步setupMaster( )
,在源碼中50-95行,着重看81-95行:
留意一下主線程在進程層面監聽的internalMessage
事件很是關鍵,主進程監聽到這個事件後,首先判斷消息對象的cmd屬性是否爲NODE_DEBUGE_ENABLED
,並以此爲條件判斷後續語句是否執行,後續的邏輯是遍歷每個worker
進程實例,若是子進程的狀態是online
或listening
就將子進程pid做爲參數調用主進程的_debugProcess( )
方法,不然改成在worker
進程實例首次上線時調用。
process._debugProcess
的定義在src/node_process_methods.cc
裏,看名字推測大體的意思就是爲了啓用對子進程的調試功能。這是一個重載方法,在windows和linux下有不一樣的實現。linux
下的代碼較短,基本能夠看懂(不秀一下怎麼對得住本身看1周的C++):
#ifdef __POSIX__ static void DebugProcess(const FunctionCallbackInfo<Value>& args) { //這裏的常量參數是經過地址引用的worker.process.pid Environment* env = Environment::GetCurrent(args); //用pid作參數獲取當前激活的環境變量,這一步應該是在獲取上下文 if (args.Length() != 1) {//不合法調用時報錯,沒什麼可說的 return env->ThrowError("Invalid number of arguments."); } CHECK(args[0]->IsNumber());//檢測參數 pid_t pid = args[0].As<Integer>()->Value(); int r = kill(pid, SIGUSR1);//發送SIGUSR1信號,終止了這個子進程 if (r != 0) {//exit code爲0時是正常退出,子進程未能正常停止時報錯 return env->ThrowErrnoException(errno, "kill"); } }
win32平臺中對應的代碼比較長,看不懂。總結一下這裏就是,在沒有收到cmd
屬性等於NODE_DEBUG_ENABLED
的內部消息以前,什麼都不作,若是收到這個消息,就終止全部的子進程,或者經過事件在子進程第一次處於online狀態就終止它。
按照執行順序接下來是101-140行的createWorkerProcess(id,env)
方法,看名字就知道是生成子進程process對象的,前半部分合並和處理環境參數,而後判斷運行參數中是否包含啓用--inspect
功能的參數並進行一些處理,最後傳入一堆參數調用了fork
方法,這個方法就是child_process.fork( )
,它就是用來生成子進程的,返回值就是子進程實例,你能夠先簡單瀏覽一下API【官方文檔child_process.fork功能】,或者知道這裏生成了子進程就好。
回到cluster.fork
方法繼續執行,下一步使用新生成的子進程process對象和惟一id做爲參數傳入Worker構造函數,生成worker
實例,Worker
的定義就在當前文件夾的worker.js
中,它首先繼承了EventEmitter
的消息的發佈訂閱能力,而後把子進程的process對象掛在在本身的process屬性上,接着爲子進程添加error
和 message
事件的監聽,最後暴露了一些更語義化的針對進程實例的管理方法(更詳細的分析能夠參考本系列前一篇博文)。生成了worker
進程實例後,添加了對於message
事件的響應,並在子進程process
對象上監聽進程的exit
,disconnect
,internalMessage
事件,最後將worker實例和本身的id以鍵值對的形式添加到cluster.workers
中記錄,並經過return
返回給外界,至此master
模塊的初始化流程就告一段落,先mark一下,後面還會講這裏。
子進程模塊是從master.js
調用child_process
時啓動的,它和主進程是並行執行的。老規矩,代碼摺疊看一下:
看出什麼了嗎?child.js
的代碼裏只有引用和定義,_setupWorker
是在nodejs
工做進程初始化時執行的,它在本身的獨立進程中初始化了一個進程管理實例,並執行了下述邏輯:
1.實例化進程管理對象worker 2.全局添加`disconnect`事件響應 3.全局添加`internalMessage`事件響應,主要是分發`act:newconn`和`act:disconnect`事件 4.用send方法發送`online`事件,通知主線程本身已上線。
注意,這個process
對象就是IPC(Inter Process Communication,也稱爲跨進程通信)可以實現的關鍵,很明顯它繼承了EventEmitter
的消息收發能力,在子進程內部進行消息收發不存在任何問題,還記得master.js
中fork
方法嗎?這個process就是調用child_process
啓動子進程時返回給主進程的那個process對象,當你在主進程中獲取它後,就能夠共享worker進程的消息能力,從而在資源隔離的條件下實現master
和worker
進程的跨進程通信。_getServer( )
方法是在創建server實例時調用的,等到驅動事件信息到達child.js
時再看,能夠留意一下最後兩個添加在Worker
原型方法上的方法,它們只在子進程中有效。
至此,你已經看到node是如何經過cluster模塊實現多實例並初始化跨進程通信了。可是跨進程通信的底層實現以及服務器的創建,以及如何在進程間協調網絡請求的處理,還依賴於net
和http
的一些內容,只好等研究完了再繼續,硬剛反正我是吃不消的。