Node.js--cluster原理分析


cluster,你真的弄明白了嗎?

上一篇文章中,我們已經了解到了cluster模塊的基本使用,cluster使用起來非常簡單,

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`工作進程 ${worker.process.pid} 已退出`);
  });
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);
}

上面的代碼就簡單的將cluster模塊加入到了node.js項目中。但是仔細分析一下這段代碼你可能會產生這些疑問:主進程僅僅fork出了子進程,並沒有創建httpserver,說好的主進程接收請求分發給子進程呢?每一個子進程都創建了一個httpserver,並偵聽同一個端口?是這樣的嗎?局面好像很尷尬。如果僅僅知道上面的這段代碼,似乎無法解決我們的疑惑。那就到源代碼中去瞧瞧,推薦一篇好文。如果你也感興趣,打開源碼過一遍,整個世界就明朗了。

cluster.js

'use strict';

const childOrMaster = 'NODE_UNIQUE_ID' in process.env ? 'child' : 'master';
module.exports = require(`internal/cluster/${childOrMaster}`);

上面的三行代碼就是cluster.js的全部內容,可以看出,子進程和主進程的區分是通過‘NODE_UNIQUE_ID’來判斷的。我們分析cluster.fork方法可以發現,在createworkprocess中都會對NODE_UNIQUE_ID進行賦值,而master進程中是沒有NODE_UNIQUE_ID的。所以再demo程序中可以分別在主進程和子進程中執行不同的內容。因此主進程執行完后,就僅僅fork出了子進程

主進程httpserver

主進程執行完畢后,子進程開始執行響應的代碼,子進程首先創建httpserver,然后監聽端口號,而正是這個listen方法,暗藏着問題的關鍵。http模塊http.server繼承了net模塊的net.server,那我們就來看看net.js中的Server.prototype.listen干了哪些事。

 

 

在listen中主要執行了listenInCluster方法,其輸入信息包含ip,端口號,地址類型,backlog和fd等,listenInCluster函數主要內容如上圖中所示,當當前進程是主進程時,直接創建服務監聽;如果是子進程,則執行_getserver函數,該函數位於lib/internal/cluster/child.js中,它會傳入創建httpserver所需要的端口等相關信息,並向主進程發送‘serverQuery’指令,主進程接收到‘serverQuery’指令后,會new出一個RoundRobinHandle的實例,在這個過程中,主進程服務被創建,端口被監聽,子進程被加入到調度度列中。這些完成后,子進程執行回調函數,繼續后續操作。

 子進程服務創建

在上面的圖中還有一個_listen2()方法,該函數對應執行的函數為setupListenHandle(),

function setupListenHandle(address, port, addressType, backlog, fd) {
      //...
    if (!address && typeof fd !== 'number') {
      rval = createServerHandle('::', port, 6, fd);
     //...
    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd);
    //...
    this[async_id_symbol] = getNewAsyncId(this._handle);
    this._handle.onconnection = onconnection;
    this._handle.owner = this;
    //...
}

通過createServerHandle函數創建句柄(句柄可理解為用戶空間的socket),同時給屬性onconnection賦值,最后偵聽端口,設定backlog。那么,socket處理請求過程“socket(),bind()”步驟就是在createServerHandle完成。

子進程是否也對端口進行了監聽?

我們在將實現回到child.js中的_getServer()函數,當子進程向主進程發送完消息后,執行回調函數。由於cluster默認的策略是round-robbin,所以會執行rr()函數:

function rr(message, indexesKey, cb) {
  if (message.errno)
    return cb(message.errno, null);

  var key = message.key;

  function listen(backlog) {
    // TODO(bnoordhuis) Send a message to the master that tells it to
    // update the backlog size. The actual backlog should probably be
    // the largest requested size by any worker.
    return 0;
  }
//...
const handle = { close, listen, ref: noop, unref: noop };
handles[key] = handle;
cb(0, handle);
}

從上面的代碼中可以看出,在listen()中直接返回,沒有做任何操作。因此子進程服務沒有創建對底層服務端socket的進行監聽,所以自然不會出現子進程端口復用的情況。最后,調用cb函數,將fake后的handle傳遞給上層net.Server,設置net.Server對底層的socket的引用。此后,子進程利用fake后的handle做端口偵聽(其實壓根啥都沒有做),執行成功后返回。

client通過tcp連接向主進程發送請求,那主進程又是如何將請求傳遞給子進程處理呢?

子進程TCP服務器沒有創建底層socket,它主要依賴IPC通道與主進程通信,既然主進程負責接受客戶端請求,那么理所應當由主進程分發客戶端請求給某個子進程,由子進程處理請求。具體分配給哪個子進程處理,是由round-robbine分發策略來決定的。由於子進程在server中設置了對底層的socket的引用,所以子進程接收到任務后,觸發connection事件開始執行業務邏輯。

對於該部分還需要持續關注,因為涉及底層libuv,需要結合C++代碼一起理解。比如:IPC通信方式有多種,node.js是如何決定使用哪種方式來通信?

總結

對於cluster的分析,得出以下結論:

1. cluster在創建子進程時,會在環境變量中增加標識,以此來區分主進程和子進程

2. listen函數在實現時對主進程和子進程進行了區分,在不同的進程中會執行不同操作

3. nodeJS封裝了進程間通信的方法,支持在進程間發送句柄的功能,句柄可以是一個socket對象,一個管道等等

4. 一個端口只能被一個進程監聽,但是該端口可以建立多個連接(accpet是產生的套接字),不同進程間可以共享這些套接字

5. 子進程的listen函數並沒有監聽端口,它在listen時將端口和地址等信息發送給主進程,由主進程進行監聽。

     主進程在收到accept事件時,產生連接socket,並把它發送給子進程。子進程直接通過該socket跟client端進行通信

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM