【nodejs原理&源碼賞析(4)】深度剖析cluster模塊源碼與node.js多進程(上)

示例代碼託管在:https://www.github.com/dashnowords/blogs前端

博客園地址:《大史住在大前端》原創博文目錄node

華爲雲社區地址:【你要的前端打怪升級指南】linux

一. 概述

cluster模塊是node.js中用於實現和管理多進程的模塊。常規的node.js應用程序是單線程單進程的,這也意味着它很難充分利用服務器多核CPU的性能,而cluster模塊就是爲了解決這個 問題的,它使得node.js程序能夠以多個實例並存的方式運行在不一樣的進程中,以求更大地榨取服務器的性能。node.js在官方示例代碼中使用worker實例來表示主進程fork出的子進程,使得前端開發者在學習過程當中很是容易和瀏覽器環境中的worker實現的多線程混淆。爲了容易區分,咱們和node官方文檔使用一致的名稱,用集羣中的masterworker來區分主進程和工做進程,用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模塊源碼解析

源碼中個別方法比較長,建議使用帶有代碼摺疊的工具來看。

3.1 起步

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} 已啓動`);
}

3.2 入口

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的形式生成這個環境變量。

3.3 主進程模塊master.js

首先運行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進程實例,若是子進程的狀態是onlinelistening就將子進程pid做爲參數調用主進程的_debugProcess( )方法,不然改成在worker進程實例首次上線時調用。

process._debugProcess的定義在src/node_process_methods.cc裏,看名字推測大體的意思就是爲了啓用對子進程的調試功能。這是一個重載方法,在windowslinux下有不一樣的實現。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屬性上,接着爲子進程添加errormessage事件的監聽,最後暴露了一些更語義化的針對進程實例的管理方法(更詳細的分析能夠參考本系列前一篇博文)。生成了worker進程實例後,添加了對於message事件的響應,並在子進程process對象上監聽進程的exit,disconnect,internalMessage事件,最後將worker實例和本身的id以鍵值對的形式添加到cluster.workers中記錄,並經過return返回給外界,至此master模塊的初始化流程就告一段落,先mark一下,後面還會講這裏。

3.4 子進程模塊child.js

子進程模塊是從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.jsfork方法嗎?這個process就是調用child_process啓動子進程時返回給主進程的那個process對象,當你在主進程中獲取它後,就能夠共享worker進程的消息能力,從而在資源隔離的條件下實現masterworker進程的跨進程通信。_getServer( )方法是在創建server實例時調用的,等到驅動事件信息到達child.js時再看,能夠留意一下最後兩個添加在Worker原型方法上的方法,它們只在子進程中有效。

四. 小結

至此,你已經看到node是如何經過cluster模塊實現多實例並初始化跨進程通信了。可是跨進程通信的底層實現以及服務器的創建,以及如何在進程間協調網絡請求的處理,還依賴於nethttp的一些內容,只好等研究完了再繼續,硬剛反正我是吃不消的。

相關文章
相關標籤/搜索