1. 程式人生 > 其它 >Nodejs cluster模組深入探究

Nodejs cluster模組深入探究

由表及裡

HTTP伺服器用於響應來自客戶端的請求,當客戶端請求數逐漸增大時服務端的處理機制有多種,如tomcat的多執行緒、nginx的事件迴圈等。而對於node而言,由於其也採用事件迴圈和非同步I/O機制,因此在高I/O併發的場景下效能非常好,但是由於單個node程式僅僅利用單核cpu,因此為了更好利用系統資源就需要fork多個node程序執行HTTP伺服器邏輯,所以node內建模組提供了child_process和cluster模組。利用child_process模組,我們可以執行shell命令,可以fork子程序執行程式碼,也可以直接執行二進位制檔案;利用cluster模組,使用node封裝好的API、IPC通道和排程機可以非常簡單的建立包括一個master程序下HTTP代理伺服器 + 多個worker程序多個HTTP應用伺服器

的架構,並提供兩種排程子程序演算法。本文主要針對cluster模組講述node是如何實現簡介高效的服務叢集建立和排程的。那麼就從程式碼進入本文的主題:

code1

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {

  let numReqs = 0;
  setInterval(() => {
    console.log(`numReqs = ${numReqs}`);
  }, 1000);

  function messageHandler(msg) {
    if (msg.cmd && msg.cmd === 'notifyRequest') {
      numReqs += 1;
    }
  }

  const numCPUs = require('os').cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  for (const id in cluster.workers) {
    cluster.workers[id].on('message', messageHandler);
  }

} else {

  // Worker processes have a http server.
  http.Server((req, res) => {
    res.writeHead(200);
    res.end('hello worldn');

    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);
}

主程序建立多個子程序,同時接受子程序傳來的訊息,迴圈輸出處理請求的數量;

子程序建立http伺服器,偵聽8000埠並返回響應。

泛泛的大道理誰都瞭解,可是這套程式碼如何執行在主程序和子程序中呢?父程序如何向子程序傳遞客戶端的請求?多個子程序共同偵聽8000埠,會不會造成埠reuse error?每個伺服器程序最大可有效支援多少併發量?主程序下的代理伺服器如何排程請求? 這些問題,如果不深入進去便永遠只停留在寫應用程式碼的層面,而且不瞭解cluster叢集建立的多程序與使用child_process建立的程序叢集的區別,也寫不出符合業務的最優程式碼,因此,深入cluster還是有必要的。

cluster與net

cluster模組與net模組息息相關,而net模組又和底層socket有聯絡,至於socket則涉及到了系統核心,這樣便由表及裡的瞭解了node對底層的一些優化配置,這是我們的思路。介紹前,筆者仔細研讀了node的js層模組實現,在基於自身理解的基礎上詮釋上節程式碼的實現流程,力圖做到清晰、易懂,如果有某些紕漏也歡迎讀者指出,只有在互相交流中才能收穫更多。

一套程式碼,多次執行

很多人對code1程式碼如何在主程序和子程序執行感到疑惑,怎樣通過cluster.isMaster判斷語句內的程式碼是在主程序執行,而其他程式碼在子程序執行呢?

其實只要你深入到了node原始碼層面,這個問題很容易作答。cluster模組的程式碼只有一句:

module.exports = ('NODE_UNIQUE_ID' in process.env) ?
                  require('internal/cluster/child') :
                  require('internal/cluster/master');

只需要判斷當前程序有沒有環境變數“NODE_UNIQUE_ID”就可知道當前程序是否是主程序;而變數“NODE_UNIQUE_ID”則是在主程序fork子程序時傳遞進去的引數,因此採用cluster.fork建立的子程序是一定包含“NODE_UNIQUE_ID”的。

這裡需要指出的是,必須通過cluster.fork建立的子程序才有NODE_UNIQUE_ID變數,如果通過child_process.fork的子程序,在不傳遞環境變數的情況下是沒有NODE_UNIQUE_ID的。因此,當你在child_process.fork的子程序中執行cluster.isMaster判斷時,返回 true。

主程序與伺服器

code1中,並沒有在cluster.isMaster的條件語句中建立伺服器,也沒有提供伺服器相關的路徑、埠和fd,那麼主程序中是否存在TCP伺服器,有的話到底是什麼時候怎麼建立的?

相信大家在學習nodejs時閱讀的各種書籍都介紹過在叢集模式下,主程序的伺服器會接受到請求然後傳送給子程序,那麼問題就來到主程序的伺服器到底是如何建立呢?主程序伺服器的建立離不開與子程序的互動,畢竟與建立伺服器相關的資訊全在子程序的程式碼中。

當子程序執行

http.Server((req, res) => {
    res.writeHead(200);
    res.end('hello worldn');

    process.send({ cmd: 'notifyRequest' });
  }).listen(8000);

時,http模組會呼叫net模組(確切的說,http.Server繼承net.Server),建立net.Server物件,同時偵聽埠。建立net.Server例項,呼叫建構函式返回。建立的net.Server例項呼叫listen(8000),等待accpet連線。那麼,子程序如何傳遞伺服器相關資訊給主程序呢?答案就在listen函式中。我保證,net.Server.prototype.listen函式絕沒有表面上看起來的那麼簡單,它涉及到了許多IPC通訊和相容性處理,可以說HTTP伺服器建立的所有邏輯都在listen函式中。

延伸下,在學習linux下的socket程式設計時,服務端的邏輯依次是執行socket(),bind(),listen()和accept(),在接收到客戶端連線時執行read(),write()呼叫完成TCP層的通訊。那麼,對應到node的net模組好像只有listen()階段,這是不是很難對應socket的四個階段呢?其實不然,node的net模組把“bind,listen”操作全部寫入了net.Server.prototype.listen中,清晰的對應底層socket和TCP三次握手,而向上層使用者只暴露簡單的listen介面。

code2

Server.prototype.listen = function() {

  ...

  // 根據引數建立 handle控制代碼
  options = options._handle || options.handle || options;
  // (handle[, backlog][, cb]) where handle is an object with a handle
  if (options instanceof TCP) {
    this._handle = options;
    this[async_id_symbol] = this._handle.getAsyncId();
    listenInCluster(this, null, -1, -1, backlogFromArgs);
    return this;
  }

  ...

  var backlog;
  if (typeof options.port === 'number' || typeof options.port === 'string') {
    if (!isLegalPort(options.port)) {
      throw new RangeError('"port" argument must be >= 0 and < 65536');
    }
    backlog = options.backlog || backlogFromArgs;
    // start TCP server listening on host:port
    if (options.host) {
      lookupAndListen(this, options.port | 0, options.host, backlog,
                      options.exclusive);
    } else { // Undefined host, listens on unspecified address
      // Default addressType 4 will be used to search for master server
      listenInCluster(this, null, options.port | 0, 4,
                      backlog, undefined, options.exclusive);
    }
    return this;
  }

  ...

  throw new Error('Invalid listen argument: ' + util.inspect(options));
};

由於本文只探究cluster模式下HTTP伺服器的相關內容,因此我們只關注有關TCP伺服器部分,其他的Pipe(domain socket)服務不考慮。

listen函式可以偵聽埠、路徑和指定的fd,因此在listen函式的實現中判斷各種引數的情況,我們最為關心的就是偵聽埠的情況,在成功進入條件語句後發現所有的情況最後都執行了listenInCluster函式而返回,因此有必要繼續探究。

code3

function listenInCluster(server, address, port, addressType,
                         backlog, fd, exclusive) {

  ...

  if (cluster.isMaster || exclusive) {
    server._listen2(address, port, addressType, backlog, fd);
    return;
  }

  // 後續程式碼為worker執行邏輯
  const serverQuery = {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  };

  ... 

  cluster._getServer(server, serverQuery, listenOnMasterHandle);
}

listenInCluster函式傳入了各種引數,如server例項、ip、port、ip型別(IPv6和IPv4)、backlog(底層服務端socket處理請求的最大佇列)、fd等,它們不是必須傳入,比如建立一個TCP伺服器,就僅僅需要一個port即可。

簡化後的listenInCluster函式很簡單,cluster模組判斷當前程序為主程序時,執行_listen2函式;否則,在子程序中執行cluster._getServer函式,同時像函式傳遞serverQuery物件,即建立伺服器需要的相關資訊。

因此,我們可以大膽假設,子程序在cluster._getServer函式中向主程序傳送了建立伺服器所需要的資料,即serverQuery。實際上也確實如此:

code4

cluster._getServer = function(obj, options, cb) {

  const message = util._extend({
    act: 'queryServer',
    index: indexes[indexesKey],
    data: null
  }, options);

  send(message, function modifyHandle(reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  });

};

子程序在該函式中向已建立的IPC通道傳送內部訊息message,該訊息包含之前提到的serverQuery資訊,同時包含act: 'queryServer'欄位,等待服務端響應後繼續執行回撥函式modifyHandle。

主程序接收到子程序傳送的內部訊息,會根據act: 'queryServer'執行對應queryServer方法,完成伺服器的建立,同時傳送回覆訊息給子程序,子程序執行回撥函式modifyHandle,繼續接下來的操作。

至此,針對主程序在cluster模式下如何建立伺服器的流程已完全走通,主要的邏輯是在子程序伺服器的listen過程中實現。

net模組與socket

上節提到了node中建立伺服器無法與socket建立對應的問題,本節就該問題做進一步解釋。在net.Server.prototype.listen函式中呼叫了listenInCluster函式,listenInCluster會在主程序或者子程序的回撥函式中呼叫_listen2函式,對應底層服務端socket建立階段的正是在這裡。

function setupListenHandle(address, port, addressType, backlog, fd) {

  // worker程序中,_handle為fake物件,無需建立
  if (this._handle) {
    debug('setupListenHandle: have a handle already');
  } else {
    debug('setupListenHandle: create a handle');

    if (rval === null)
      rval = createServerHandle(address, port, addressType, fd);

    this._handle = rval;
  }

  this[async_id_symbol] = getNewAsyncId(this._handle);

  this._handle.onconnection = onconnection;

  var err = this._handle.listen(backlog || 511);

}

通過createServerHandle函式建立控制代碼(控制代碼可理解為使用者空間的socket),同時給屬性onconnection賦值,最後偵聽埠,設定backlog。

那麼,socket處理請求過程“socket(),bind()”步驟就是在createServerHandle完成。

function createServerHandle(address, port, addressType, fd) {
  var handle;

  // 針對網路連線,繫結地址
  if (address || port || isTCP) {
    if (!address) {
      err = handle.bind6('::', port);
      if (err) {
        handle.close();
        return createServerHandle('0.0.0.0', port);
      }
    } else if (addressType === 6) {
      err = handle.bind6(address, port);
    } else {
      err = handle.bind(address, port);
    }
  }

  return handle;
}

在createServerHandle中,我們看到了如何建立socket(createServerHandle在底層利用node自己封裝的類庫建立TCP handle),也看到了bind繫結ip和地址,那麼node的net模組如何接收客戶端請求呢?

必須深入c++模組才能瞭解node是如何實現在c++層面呼叫js層設定的onconnection回撥屬性,v8引擎提供了c++和js層的型別轉換和介面透出,在c++的tcp_wrap中:

void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
  TCPWrap* wrap;
  ASSIGN_OR_RETURN_UNWRAP(&wrap,
                          args.Holder(),
                          args.GetReturnValue().Set(UV_EBADF));
  int backloxxg = args[0]->Int32Value();
  int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
                      backlog,
                      OnConnection);
  args.GetReturnValue().Set(err);
}

我們關注uv_listen函式,它是libuv封裝後的函式,傳入了handle_,backlog和OnConnection回撥函式,其中handle_為node呼叫libuv介面建立的socket封裝,OnConnection函式為socket接收客戶端連線時執行的操作。我們可能會猜測在js層設定的onconnction函式最終會在OnConnection中呼叫,於是進一步深入探查node的connection_wrap c++模組:

template <typename WrapType, typename UVType>
void ConnectionWrap<WrapType, UVType>::OnConnection(uv_stream_t* handle,
                                                    int status) {

  if (status == 0) {
    if (uv_accept(handle, client_handle))
      return;

    // Successful accept. Call the onconnection callback in JavaScript land.
    argv[1] = client_obj;
  }
  wrap_data->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
}

過濾掉多餘資訊便於分析。當新的客戶端連線到來時,libuv呼叫OnConnection,在該函式內執行uv_accept接收連線,最後將js層的回撥函式onconnection[通過env->onconnection_string()獲取js的回撥]和接收到的客戶端socket封裝傳入MakeCallback中。其中,argv陣列的第一項為錯誤資訊,第二項為已連線的clientSocket封裝,最後在MakeCallback中執行js層的onconnection函式,該函式的引數正是argv陣列傳入的資料,“錯誤程式碼和clientSocket封裝”。

js層的onconnection回撥

function onconnection(err, clientHandle) {
  var handle = this;

  if (err) {
    self.emit('error', errnoException(err, 'accept'));
    return;
  }

  var socket = new Socket({
    handle: clientHandle,
    allowHalfOpen: self.allowHalfOpen,
    pauseOnCreate: self.pauseOnConnect
  });
  socket.readable = socket.writable = true;

  self.emit('connection', socket);
}

這樣,node在C++層呼叫js層的onconnection函式,構建node層的socket物件,並觸發connection事件,完成底層socket與node net模組的連線與請求打通。

至此,我們打通了socket連線建立過程與net模組(js層)的流程的互動,這種封裝讓開發者在不需要查閱底層介面和資料結構的情況下,僅使用node提供的http模組就可以快速開發一個應用伺服器,將目光聚集在業務邏輯中。

backlog是已連線但未進行accept處理的socket佇列大小。在linux 2.2以前,backlog大小包括了半連線狀態和全連線狀態兩種佇列大小。linux 2.2以後,分離為兩個backlog來分別限制半連線SYN_RCVD狀態的未完成連線佇列大小跟全連線ESTABLISHED狀態的已完成連線佇列大小。這裡的半連線狀態,即在三次握手中,服務端接收到客戶端SYN報文後併發送SYN+ACK報文後的狀態,此時服務端等待客戶端的ACK,全連線狀態即服務端和客戶端完成三次握手後的狀態。backlog並非越大越好,當等待accept佇列過長,服務端無法及時處理排隊的socket,會造成客戶端或者前端伺服器如nignx的連線超時錯誤,出現“error: Broken Pipe”。因此,node預設在socket層設定backlog預設值為511,這是因為nginx和redis預設設定的backlog值也為此,儘量避免上述錯誤。

多個子程序與埠複用

再回到關於cluster模組的主線中來。code1中,主程序與所有子程序通過訊息構建出偵聽8000埠的TCP伺服器,那麼子程序中有沒有也建立一個伺服器,同時偵聽8000埠呢?其實,在子程序中壓根就沒有這回事,如何理解呢?子程序中確實建立了net.Server物件,可是它沒有像主程序那樣在libuv層構建socket控制代碼,子程序的net.Server物件使用的是一個人為fake出的一個假控制代碼來“欺騙”使用者埠已偵聽,這樣做的目的是為了叢集的負載均衡,這又涉及到了cluster模組的均衡策略的話題上。

在本節有關cluster叢集埠偵聽以及請求處理的描述,都是基於cluster模式的預設策略RoundRobin之上討論的,關於排程策略的討論,我們放在下節進行。

主程序與伺服器這一章節最後,我們只瞭解到主程序是如何建立偵聽給定埠的TCP伺服器的,此時子程序還在等待主程序建立後傳送的訊息。當主程序傳送建立伺服器成功的訊息後,子程序會執行modifyHandle回撥函式。還記得這個函式嗎?主程序與伺服器這一章節最後已經貼出來它的原始碼:

function modifyHandle(reply, handle) => {
    if (typeof obj._setServerData === 'function')
      obj._setServerData(reply.data);

    if (handle)
      shared(reply, handle, indexesKey, cb);  // Shared listen socket.
    else
      rr(reply, indexesKey, cb);              // Round-robin.
  }

它會根據主程序是否返回handle控制代碼(即libuv對socket的封裝)來選擇執行函式。由於cluter預設採用RoundRobin排程策略,因此主程序返回的handle為null,執行函式rr。在該函式中,做了上文提到的hack操作,作者fake了一個假的handle物件,“欺騙”上層呼叫者:

function listen(backlog) {
    return 0;
  }

  const handle = { close, listen, ref: noop, unref: noop };

  handles[key] = handle;
  cb(0, handle);

看到了嗎?fake出的handle.listen並沒有呼叫libuv層的Listen方法,它直接返回了。這意味著什麼??子程序壓根沒有建立底層的服務端socket做偵聽,所以在子程序建立的HTTP伺服器偵聽的埠根本不會出現埠複用的情況。 最後,呼叫cb函式,將fake後的handle傳遞給上層net.Server,設定net.Server對底層的socket的引用。此後,子程序利用fake後的handle做埠偵聽(其實壓根啥都沒有做),執行成功後返回。

那麼子程序TCP伺服器沒有建立底層socket,如何接受請求和傳送響應呢?這就要依賴IPC通道了。既然主程序負責接受客戶端請求,那麼理所應當由主程序分發客戶端請求給某個子程序,由子程序處理請求。實際上也確實是這樣做的,主程序的伺服器中會建立RoundRobinHandle決定分發請求給哪一個子程序,篩選出子程序後傳送newconn訊息給對應子程序:

  const message = { act: 'newconn', key: this.key };

  sendHelper(worker.process, message, handle, (reply) => {
    if (reply.accepted)
      handle.close();
    else
      this.distribute(0, handle);  // Worker is shutting down. Send to another.

    this.handoff(worker);
  });

子程序接收到newconn訊息後,會呼叫內部的onconnection函式,先向主程序傳送開始處理請求的訊息,然後執行業務處理函式handle.onconnection。還記得這個handle.onconnection嗎?它正是上節提到的node在c++層執行的js層回撥函式,在handle.onconnection中構造了net.Socket物件標識已連線的socket,最後觸發connection事件呼叫開發者的業務處理函式(此時的資料處理對應在網路模型的第四層傳輸層中,node的http模組會從socket中獲取資料做應用層的封裝,解析出請求頭、請求體並構造響應體),這樣便從核心socket->libuv->js依次執行到開發者的業務邏輯中。

到此為止,相信讀者已經明白node是如何處理客戶端的請求了,那麼下一步繼續探究node是如何分發客戶端的請求給子程序的。

請求分發策略

上節提到cluster模組預設採用RoundRobin排程策略,那麼還有其他策略可以選擇嗎?答案是肯定的,在windows機器中,cluster模組採用的是共享服務端socket方式,通俗點說就是由作業系統進行排程客戶端的請求,而不是由node程式排程。其實在node v0.8以前,預設的叢集模式就是採用作業系統排程方式進行,直到cluster模組的加入才有了改變。

那麼,RoundRobin排程策略到底是怎樣的呢?

RoundRobinHandle.prototype.distribute = function(err, handle) {
  this.handles.push(handle);
  const worker = this.free.shift();

  if (worker)
    this.handoff(worker);
};

// 傳送訊息和handle給對應worker程序,處理業務邏輯
RoundRobinHandle.prototype.handoff = function(worker) {
  if (worker.id in this.all === false) {
    return;  // Worker is closing (or has closed) the server.
  }

  const handle = this.handles.shift();

  if (handle === undefined) {
    this.free.push(worker);  // Add to ready queue again.
    return;
  }

  const message = { act: 'newconn', key: this.key };

  sendHelper(worker.process, message, handle, (reply) => {
    if (reply.accepted)
      handle.close();
    else
      this.distribute(0, handle);  // Worker is shutting down. Send to another.

    this.handoff(worker);
  });
};

核心程式碼就是這兩個函式,濃縮的是精華。distribute函式負責篩選出處理請求的子程序,this.free陣列儲存空閒的子程序,this.handles陣列存放待處理的使用者請求。handoff函式獲取排隊中的客戶端請求,並通過IPC傳送控制代碼handle和newconn訊息,等待子程序返回。當子程序返回正在處理請求訊息時,在此執行handoff函式,繼續分配請求給該子程序,不管該子程序上次請求是否處理完成(node的非同步特性和事件迴圈可以讓單程序處理多請求)。按照這樣的策略,主程序的伺服器每接受一個req請求,執行修改後的onconnection回撥,執行distribute方法,在其內部呼叫handoff函式,進入該子程序的處理迴圈中。一旦主程序沒有快取的客戶端請求時(this.handles為空),便會將當前子程序加入free空閒佇列,等待主程序的下一步排程。這就是cluster模式的RoundRobin排程策略,每個子程序的處理邏輯都是一個閉環,直到主程序快取的客戶端請求處理完畢時,該子程序的處理閉環才被開啟。

這麼簡單的實現帶來的效果卻是不小,經過全世界這麼多使用者的嘗試,主程序分發請求還是很平均的,如果RoundRobin的排程需求不滿足你業務中的要求,你可以嘗試仿照RoundRobin模組寫一個另類的排程演算法。

那麼cluster模組在windows系統中採用的shared socket策略(後文簡稱SS策略)是什麼呢?採用SS策略排程演算法,子程序的伺服器工作邏輯完全不同於上文中所講的那樣,子程序建立的TCP伺服器會在底層偵聽埠並處理響應,這是如何實現的呢?SS策略的核心在於IPC傳輸控制代碼的檔案描述符,並且在C++層設定埠的SO_REUSEADDR選項,最後根據傳輸的檔案描述符還原出handle(net.TCP),處理請求。這正是shared socket名稱由來,共享檔案描述符。

子程序繼承父程序fd,處理請求

import socket
import os

def main():
    serversocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    serversocket.bind(("127.0.0.1", 8888))
    serversocket.listen(0)

    # Child Process
    if os.fork() == 0:
        accept_conn("child", serversocket)

    accept_conn("parent", serversocket)

def accept_conn(message, s):
    while True:
        c, addr = s.accept()
        print 'Got connection from in %s' % message
        c.send('Thank you for your connecting to %sn' % message)
        c.close()

if __name__ == "__main__":
    main()

需要指出的是,在子程序中根據檔案描述符還原出的handle,不能再進行bind(ip,port)和listen(backlog)操作,只有主程序建立的handle可以呼叫這些函式。子程序中只能選擇accept、read和write操作。

既然SS策略傳遞的是master程序的服務端socket的檔案描述符,子程序偵聽該描述符,那麼由誰來排程哪個子程序處理請求呢?這就是由作業系統核心來進行排程。可是核心排程往往出現意想不到的效果,在linux下導致請求往往集中在某幾個子程序中處理。這從核心的排程策略也可以推算一二,核心的程序排程離不開上下文切換,上下文切換的代價很高,不僅需要儲存當前程序的程式碼、資料和堆疊等使用者空間資料,還需要儲存各種暫存器,如PC,ESP,最後還需要恢復被排程程序的上下文狀態,仍然包括程式碼、資料和各種暫存器,因此代價非常大。而linux核心在排程這些子程序時往往傾向於喚醒最近被阻塞的子程序,上下文切換的代價相對較小。而且核心的排程策略往往受到當前系統的執行任務數量和資源使用情況,對專注於業務開發的http伺服器影響較大,因此會造成某些子程序的負載嚴重不均衡的狀況。那麼為什麼cluster模組預設會在windows機器中採用SS策略排程子程序呢?原因是node在windows平臺採用的IOCP來最大化效能,它使得傳遞連線的控制代碼到其他程序的成本很高,因此採用預設的依靠作業系統排程的SS策略。

SS排程策略非常簡單,主程序直接通過IPC通道傳送handle給子程序即可,此處就不針對程式碼進行分析了。此處,筆者利用node的child_process模組實現了一個簡易的SS排程策略的服務叢集,讀者可以更好的理解:

master程式碼

var net = require('net');
var cp = require('child_process');
var w1 = cp.fork('./singletest/worker.js');
var w2 = cp.fork('./singletest/worker.js');
var w3 = cp.fork('./singletest/worker.js');
var w4 = cp.fork('./singletest/worker.js');

var server = net.createServer();

server.listen(8000,function(){
  // 傳遞控制代碼
  w1.send({type: 'handle'},server);
  w2.send({type: 'handle'},server);
  w3.send({type: 'handle'},server);
  w4.send({type: 'handle'},server);
  server.close();
});

child程式碼

var server = require('http').createServer(function(req,res){
  res.write(cluster.isMaster + '');
  res.end(process.pid+'')
})

var cluster = require('cluster');
process.on('message',(data,handle)=>{
  if(data.type !== 'handle')
    return;

  handle.on('connection',function(socket){
    server.emit('connection',socket)
  });
});

這種方式便是SS策略的典型實現,不推薦使用者嘗試。

結尾

開篇提到的一些問題至此都已經解答完畢,關於cluster模組的一些具體實現本文不做詳細描述,有興趣感受node原始碼的同學可以在閱讀本文的基礎上再翻閱,這樣事半功倍。本文是在node原始碼和筆者的計算機網路基礎之上混合後的產物,起因於筆者研究PM2的cluster模式下God程序的具體實現。在嘗試幾天仔細研讀node cluster相關模組後有感於其良好的封裝性,故產生將其內部實現原理和技巧向日常開發者所展示的想法,最後有了這篇文章。

那麼,閱讀了這篇文章,熟悉了cluster模式的具體實現原理,對於日常開發者有什麼促進作用呢?首先,能不停留在使用層面,深入到具體實現原理中去,這便是比大多數人強了;在理解實現機制的階段下,如果能反哺業務開發就更有意義了。比如,根據業務設計出更匹配的負載均衡邏輯;根據服務的日常QPS設定合理的backlog值等;最後,在探究實現的過程中,我們又回顧了許多離應用層開發人員難以接觸到的底層網路程式設計和作業系統知識,這同時也是學習深入的過程。

接下來,筆者可能會抽時間針對node的其他常用模組做一次細緻的解讀。其實,node較為重要的Stream模組筆者已經分析過了,node中的Stream深入node之Transform,經過深入探究之後在日常開發node應用中有著很大的提升作用,讀者們可以嘗試下。既然提到了Stream模組,那麼結合本文的net模組解析,我們就非常容易理解node http模組的實現了,因為http模組正是基於net和Stream模組實現的。那麼下一篇文章就針對http模組做深入解析吧!

參考文章

Node.js v0.12的新特性 -- Cluster模式採用Round-Robin負載均衡 TCP SOCKET中backlog引數