818 NodeJS的Cluster模塊

NodeJS是單進程單線程<a name="1">[1]</a>結構识啦,適合編寫IO密集型的網絡應用箕憾。為了充分利用多核CPU的計算能力擂煞,最直接的想法是同時運行多個實例進程诬辈,但手動管理這些進程卻是個麻煩事酵使,不但要知道當前CPU的核心數(shù)以確定進程數(shù)量,還要為不同實例進程配置不同網絡監(jiān)聽端口(Listening Port)避免端口沖突<a name="2">[2]</a>焙糟,另外還要監(jiān)控進程運行狀態(tài)口渔,執(zhí)行Crash后重啟等操作,最后還得配合Load Balancer統(tǒng)一對外的服務端口:

手動管理實例

想想就好煩穿撮!幸好缺脉,NodeJS引入了Cluster模塊試圖簡化這些體力勞動。使用Cluster模塊可以運行并管理多個實例進程混巧,而且無須為每個進程單獨配置監(jiān)聽端口(當然如果你想的話也可以)枪向。下面是Cluster模塊的基本用法勤揩,一個子進程啟動器:

//cluster_launcher.js
let cluster = require('cluster');
if (cluster.isMaster) {
    // Here is in master process
    let cpus = require('os').cpus().length;
    console.log(`Master PID: ${process.pid}, CPUs: ${cpus}`); 
    // Fork workers.    
    for (var i = 0; i < cpus; i++) {
        cluster.fork();
    }    
    cluster.on('exit', (worker, code, signal) => {
        console.log(`worker ${worker.process.pid} died`); 
    });
} else { 
    // Here is in Worker process
    console.log(`Worker PID: ${process.pid}`);
    require('./tcpapp.js');
    //require('./udpapp.js'); //uncomment if you need a udp server
}

代碼很簡單咧党,運行后會產生一個Master進程及n個Worker子進程,n等于CPU核心數(shù)陨亡。

啟動器本身代碼(cluster_launcher.js)在Master和Worker子進程都會被執(zhí)行傍衡,依據(jù)cluster.isMaster的值來區(qū)分運行在Master和Worker上的代碼分支。Master進程的cluster對象上定義有fork方法负蠕,調用后操作系統(tǒng)會生成一個新的Worker子進程蛙埂。Worker子進程除了從Master進程繼承了環(huán)境變量和命令行等設置,另外還多了一個環(huán)境變量NODE_UNIQUE_ID來保存Worker進程的Id(由Master負責分配)遮糖。Cluster模塊內部通過判斷NODE_UNIQUE_ID的存在與否確定當前運行的進程是Master還是Worker:

cluster.isWorker = ('NODE_UNIQUE_ID' in process.env);
cluster.isMaster = (cluster.isWorker === false);

剛才提到使用Cluster模塊管理多進程Node應用绣的,可以不用單獨為每個進程指定監(jiān)聽端口,也就是從使用者角度看每個進程使用同一個端口監(jiān)聽網絡而不會發(fā)生端口沖突欲账。這是怎么做到的呢屡江?原來Node內部讓TCP和UDP模塊的對Cluster啟動的情況做了特殊處理,接下來對TCP和UDP兩種情況分別開8赛不。

首先是TCP惩嘉,按國際慣例,Hello World!踢故。

//tcpapp.js
let http = require('http');
http.createServer((req, res) => { 
   res.writeHead(200); 
   res.end('hello world\n'); 
}).listen(8000);

以上代碼實現(xiàn)了一個最簡單的HTTP服務器文黎,在8000端口監(jiān)聽請求并返回“hello world”字符串惹苗。TCP是面向連接的協(xié)議,操作系統(tǒng)層面每個監(jiān)聽端口都對應一個 Socket用來監(jiān)聽網絡上的TCP連接請求(Incoming Connection)耸峭,每當握手成功操作系統(tǒng)就會創(chuàng)建一個新的Socket代表這個已建立的連接(Established Connection)用做后續(xù)的IO操作桩蓉。單獨運行上面的服務器的話,這兩種Socket都屬于同一個進程劳闹,也就是監(jiān)聽TCP連接和處理HTTP請求都在一個進程完成触机。以下步驟幫助確認這種情況:

$ node tcpapp.js &

[1] 51647 //pid

打開瀏覽器訪問:http://localhost:8000 ,然后lsof查看進程socket的情況:

$ lsof -a -i tcp:8000 -P -l

COMMAND     PID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Google    10268      501   97u  IPv6 0xff13215c8e8c1ac7      0t0  TCP localhost:50807->localhost:8000 (ESTABLISHED)
node      38622      501   11u  IPv6 0xff13215c8e8c1567      0t0  TCP *:8000 (LISTEN)
node      38622      501   12u  IPv6 0xff13215c8e8c1007      0t0  TCP localhost:8000->localhost:50807 (ESTABLISHED)

可以看到同一個node進程上打開了兩個Socket(DEVICE列的值不同)玷或,一個負責監(jiān)聽端口儡首,一個負責已建立連接上的IO。一般來說TCP連接握手由操作系統(tǒng)在內核空間完成偏友,不會形成性能瓶頸蔬胯,單進程node應用的瓶頸在于應用邏輯,即使業(yè)務邏輯以IO為主位他,CPU消耗仍然比內核操作大得多氛濒。因此單進程node應用的瓶頸會在業(yè)務邏輯處理量增加到單CPU核心飽和時出現(xiàn)。

下面看看用cluster_launcher.js啟動的情況鹅髓,運行下面的命令:

$ node cluster_launcher.js &

[1] 28153
Master PID: 28153, CPUs: 4
Worker PID: 28155
Worker PID: 28156
Worker PID: 28154
Worker PID: 28157

可以看到一個Master進程啟動了四個Worker子進程:

$ pstree 28153

-+- 28153 /usr/local/bin/node /tmp/demo/cluster_launcher.js
 |--- 28154 /usr/local/bin/node /tmp/demo/cluster_launcher.js
 |--- 28155 /usr/local/bin/node /tmp/demo/cluster_launcher.js
 |--- 28156 /usr/local/bin/node /tmp/demo/cluster_launcher.js
 \--- 28157 /usr/local/bin/node /tmp/demo/cluster_launcher.js

打開瀏覽器訪問http://localhost:8000舞竿,然后lsof下進程的socket的情況:

$ lsof -a -i tcp:8000 -P -R -l

COMMAND     PID  PPID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
Google    10268     1      501    3u  IPv6 0xff13215c8e8c1007      0t0  TCP localhost:50504->localhost:8000 (ESTABLISHED)
Google    10268     1      501    5u  IPv6 0xff13215c8e8bffe7      0t0  TCP localhost:50547->localhost:8000 (ESTABLISHED)
Google    10268     1      501   10u  IPv6 0xff13215c8e8c0547      0t0  TCP localhost:50548->localhost:8000 (ESTABLISHED)
node      28153 12710      501   17u  IPv6 0xff13215c8e8c1567      0t0  TCP *:8000 (LISTEN)
node      28154 28153      501   14u  IPv6 0xff13215c8e8c0aa7      0t0  TCP localhost:8000->localhost:50548 (ESTABLISHED)
node      28155 28153      501   14u  IPv6 0xff13215c8e8bfa87      0t0  TCP localhost:8000->localhost:50547 (ESTABLISHED)
node      28156 28153      501   14u  IPv6 0xff13215c8e8c1ac7      0t0  TCP localhost:8000->localhost:50504 (ESTABLISHED)

可以看到分配給Master和Worker進程的DEVICE(對應Protocol Control Block的內核地址)的值都不一樣,說明各有各的Socket窿冯。Master進程只有一個處在Listening狀態(tài)的Socket負責監(jiān)聽8000端口骗奖,Worker進程的Socket都是Established的,說明Worker進程只負責處理連接上的IO醒串。同時也可以看到执桌,三個Established狀態(tài)的TCP連接<a name="3">[3]</a>被分配給了三個Worker進程,也就是說芜赌,Cluster模塊可以利用多進程并行處理同一端口的TCP連接:

Cluster下的TCP連接處理

剩下的就要看每個Worker進程的負載是否均衡了仰挣。上圖所示是Cluster模式下Established TCP連接的默認調度方式(除Windows以外),調度由Master進程負責缠沈,以Round Robin的方式將Established狀態(tài)的連接IPC給Worker進程做進一步處理膘壶,這樣看來各Worker的負載是平均的。

默認的調度策略(Round Robin)大多數(shù)時候可以工作的很好洲愤,連接按建立的順序依次被分配到各Worker進程颓芭,每個CPU內核都可以得到充分利用。但是禽篱,這也意味著這種調度方式不能保證“同源(來自同一個IP)的連接”被同一個Worker進程處理畜伐,帶來上層應用會話狀態(tài)的管理問題。一般情況下可以使用redis等全局session store保存應用會話狀態(tài)躺率,對所有進程可見玛界,然而不是所有的應用層狀態(tài)都受業(yè)務代碼掌控万矾,能放入全局store,典型的例子是Socket.io 在建立WebSocket連接過程中的握手狀態(tài)是保存在本地進程內存中的慎框,而且目前沒有提供接口控制保存策略良狈,那么當通過Cluster模塊啟動Socket.io服務器時,同源的連接可能會被分配給不同Worker進程笨枯,出現(xiàn)握手失敗的狀況薪丁。解決的方法并不復雜,Master進程只需把同源IP的連接分配給同一個worker進程就可以了(這種調度方式有時被稱作IP Hash)馅精,可惜Cluster模塊目前并沒提供這個選項严嗜,只能借助第三方插件了<a name="4">[4]</a>。

除了默認的調度策略洲敢,還可以讓OS的Process Scheduler來負責worker進程的調度(詳見SCHED_NONE策略)漫玄,這也是Windows上的默認策略,但在Linux下效果并不理想压彭,這里不再贅述睦优。

接下來是UDP的情況:

//udpapp.js
let dgram = require('dgram');
let server = dgram.createSocket('udp4');
server.on('error', (err) => {   
  console.log(`server error:\n${err.stack}`);
  server.close();
});
server.on('message', (msg, rinfo) => {    
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});
server.on('listening', () => {    
  var address = server.address();    
  console.log(`server listening ${address.address}:${address.port}`);
});
server.bind(9000);

上面的代碼啟動UDP服務器,在9000端口監(jiān)聽UDP packet壮不。用cluster_launcher.js啟動并lsof查看socket結果如下:

$ node cluster_launcher.js &

Master PID: 23263, CPUs: 4
Worker PID: 23266
Worker PID: 23265
Worker PID: 23267
Worker PID: 23264
server listening 0.0.0.0:9000
server listening 0.0.0.0:9000
server listening 0.0.0.0:9000
server listening 0.0.0.0:9000

$ lsof -a -i udp:9000 -P -R -l

COMMAND   PID  PPID     USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
node    23263 12710      501   17u  IPv4 0xff13215c8a237c97      0t0  UDP *:9000
node    23264 23263      501   14u  IPv4 0xff13215c8a237c97      0t0  UDP *:9000
node    23265 23263      501   14u  IPv4 0xff13215c8a237c97      0t0  UDP *:9000
node    23266 23263      501   14u  IPv4 0xff13215c8a237c97      0t0  UDP *:9000
node    23267 23263      501   14u  IPv4 0xff13215c8a237c97      0t0  UDP *:9000

可以看到Master和Worker進程的實際上共享同一個UDP Socket(DEVICE指向地址相同)汗盘,但區(qū)別是Worker子進程調用了Bind方法而Master進程沒有,Master進程在這里的作用僅僅是管理Worker進程“聲明”使用到的UDP Socket:每當Worker調用Bind方法監(jiān)聽某UDP端口時询一,內部會通過IPC詢問Master是否有可重用的UDP Socket隐孽,Master收到詢問后會在本地Socket緩存中查找,沒有則新創(chuàng)建一個并緩存起來家凯,之后把相應的UDP Socket IPC給Worker缓醋,Worker收到后在其上完成真正的Bind操作。這樣處理結果就是Cluster模塊把UDP packet的分發(fā)任務交給OS的Process Scheduler負責:當9000端口收到一個UDP packet時绊诲,Process Scheduler就會隨機分配給一個Worker做進一步處理:


Cluster下的UDP連接處理

以上是對Cluster模塊在處理TCP和UDP時內部機理的一些分析發(fā)掘,希望能對各位使用好Cluster模塊有所幫助褪贵,如有紕漏敬請指出掂之。

<a name="1ref">[1]</a>這里指用戶的編程模型是單進程單線程的,NodeJS進程本身是多線程的脆丁,例如世舰,NodeJS的底層庫libuv用線程池將文件系統(tǒng)的同步操作轉化成異步操作,只不過這一切對用戶透明槽卫。
<a name="2ref">[2]</a>Linux Kernel 3.9之后支持了SO_REUSEPORT選項跟压,可以讓多個進程共享同一個端口,但libuv目前沒有采用歼培。
<a name="3ref">[3]</a>訪問服務器時震蒋,瀏覽器通常會同時打開多個TCP連接發(fā)送HTTP請求茸塞,加快頁面的加載速度。
<a name="4ref">[4]</a>indutny/sticky-session可以解決Socket.io的問題查剖。

本文原創(chuàng)钾虐,歡迎轉載,但請注明出處

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末笋庄,一起剝皮案震驚了整個濱河市效扫,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌直砂,老刑警劉巖菌仁,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異静暂,居然都是意外死亡掘托,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門籍嘹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闪盔,“玉大人,你說我怎么就攤上這事辱士±嵯疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵颂碘,是天一觀的道長异赫。 經常有香客問我,道長头岔,這世上最難降的妖魔是什么塔拳? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮峡竣,結果婚禮上靠抑,老公的妹妹穿的比我還像新娘。我一直安慰自己适掰,他們只是感情好颂碧,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著类浪,像睡著了一般载城。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上费就,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天诉瓦,我揣著相機與錄音,去河邊找鬼。 笑死睬澡,一個胖子當著我的面吹牛固额,可吹牛的內容都是我干的。 我是一名探鬼主播猴贰,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼对雪,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了米绕?” 一聲冷哼從身側響起瑟捣,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎栅干,沒想到半個月后迈套,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡碱鳞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年桑李,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片窿给。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡贵白,死狀恐怖,靈堂內的尸體忽然破棺而出崩泡,到底是詐尸還是另有隱情禁荒,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布角撞,位于F島的核電站呛伴,受9級特大地震影響,放射性物質發(fā)生泄漏谒所。R本人自食惡果不足惜热康,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望劣领。 院中可真熱鬧姐军,春花似錦、人聲如沸剖踊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽德澈。三九已至,卻和暖如春固惯,著一層夾襖步出監(jiān)牢的瞬間梆造,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镇辉,地道東北人屡穗。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像忽肛,于是被迫代替她去往敵國和親村砂。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內容