Nodejs cluster模塊深刻探究

能夠收藏個人博客html

由表及裏

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是如何實現簡介高效的服務集羣建立和調度的。那麼就從代碼進入本文的主題:前端

code1node

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 world\n');

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

主進程建立多個子進程,同時接受子進程傳來的消息,循環輸出處理請求的數量;linux

子進程建立http服務器,偵聽8000端口並返回響應。nginx

泛泛的大道理誰都瞭解,但是這套代碼如何運行在主進程和子進程中呢?父進程如何向子進程傳遞客戶端的請求?多個子進程共同偵聽8000端口,會不會形成端口reuse error?每一個服務器進程最大可有效支持多少併發量?主進程下的代理服務器如何調度請求? 這些問題,若是不深刻進去便永遠只停留在寫應用代碼的層面,並且不瞭解cluster集羣建立的多進程與使用child_process建立的進程集羣的區別,也寫不出符合業務的最優代碼,所以,深刻cluster仍是有必要的。c++

cluster與net

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

一套代碼,屢次執行

不少人對code1代碼如何在主進程和子進程執行感到疑惑,怎樣經過cluster.isMaster判斷語句內的代碼是在主進程執行,而其餘代碼在子進程執行呢?算法

其實只要你深刻到了node源碼層面,這個問題很容易做答。cluster模塊的代碼只有一句:shell

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 world\n');

    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的異步特性和事件循環可讓單進程處理多請求)。按照這樣的策略,主進程每fork一個子進程,都會調用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 %s\n' % 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參數

相關文章
相關標籤/搜索