Nodejs中cluster模塊的作用是什么

這期內(nèi)容當(dāng)中小編將會(huì)給大家?guī)碛嘘P(guān)Nodejs中cluster模塊的作用是什么,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。

專注于為中小企業(yè)提供網(wǎng)站制作、網(wǎng)站設(shè)計(jì)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)德安免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了上1000家企業(yè)的穩(wěn)健成長(zhǎng),幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。

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

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

主進(jìn)程創(chuàng)建多個(gè)子進(jìn)程同時(shí)接受子進(jìn)程傳來的消息循環(huán)輸出處理請(qǐng)求的數(shù)量

子進(jìn)程創(chuàng)建http服務(wù)器偵聽8000端口并返回響應(yīng)。

泛泛的大道理誰(shuí)都了解可是這套代碼如何運(yùn)行在主進(jìn)程和子進(jìn)程中呢父進(jìn)程如何向子進(jìn)程傳遞客戶端的請(qǐng)求多個(gè)子進(jìn)程共同偵聽8000端口會(huì)不會(huì)造成端口reuse error每個(gè)服務(wù)器進(jìn)程最大可有效支持多少并發(fā)量主進(jìn)程下的代理服務(wù)器如何調(diào)度請(qǐng)求 這些問題如果不深入進(jìn)去便永遠(yuǎn)只停留在寫應(yīng)用代碼的層面而且不了解cluster集群創(chuàng)建的多進(jìn)程與使用child_process創(chuàng)建的進(jìn)程集群的區(qū)別也寫不出符合業(yè)務(wù)的最優(yōu)代碼因此深入cluster還是有必要的。

cluster與net

cluster模塊與net模塊息息相關(guān)而net模塊又和底層socket有聯(lián)系至于socket則涉及到了系統(tǒng)內(nèi)核這樣便由表及里的了解了node對(duì)底層的一些優(yōu)化配置這是我們的思路。介紹前筆者仔細(xì)研讀了node的js層模塊實(shí)現(xiàn)在基于自身理解的基礎(chǔ)上詮釋上節(jié)代碼的實(shí)現(xiàn)流程力圖做到清晰、易懂如果有某些紕漏也歡迎讀者指出只有在互相交流中才能收獲更多。

一套代碼多次執(zhí)行

很多人對(duì)code1代碼如何在主進(jìn)程和子進(jìn)程執(zhí)行感到疑惑怎樣通過cluster.isMaster判斷語(yǔ)句內(nèi)的代碼是在主進(jìn)程執(zhí)行而其他代碼在子進(jìn)程執(zhí)行呢

其實(shí)只要你深入到了node源碼層面這個(gè)問題很容易作答。cluster模塊的代碼只有一句

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

只需要判斷當(dāng)前進(jìn)程有沒有環(huán)境變量“NODE_UNIQUE_ID”就可知道當(dāng)前進(jìn)程是否是主進(jìn)程而變量“NODE_UNIQUE_ID”則是在主進(jìn)程fork子進(jìn)程時(shí)傳遞進(jìn)去的參數(shù)因此采用cluster.fork創(chuàng)建的子進(jìn)程是一定包含“NODE_UNIQUE_ID”的。

這里需要指出的是必須通過cluster.fork創(chuàng)建的子進(jìn)程才有NODE_UNIQUE_ID變量如果通過child_process.fork的子進(jìn)程在不傳遞環(huán)境變量的情況下是沒有NODE_UNIQUE_ID的。因此當(dāng)你在child_process.fork的子進(jìn)程中執(zhí)行cluster.isMaster判斷時(shí)返回 true。

主進(jìn)程與服務(wù)器

code1中并沒有在cluster.isMaster的條件語(yǔ)句中創(chuàng)建服務(wù)器也沒有提供服務(wù)器相關(guān)的路徑、端口和fd那么主進(jìn)程中是否存在TCP服務(wù)器有的話到底是什么時(shí)候怎么創(chuàng)建的

相信大家在學(xué)習(xí)nodejs時(shí)閱讀的各種書籍都介紹過在集群模式下主進(jìn)程的服務(wù)器會(huì)接受到請(qǐng)求然后發(fā)送給子進(jìn)程那么問題就來到主進(jìn)程的服務(wù)器到底是如何創(chuàng)建呢主進(jìn)程服務(wù)器的創(chuàng)建離不開與子進(jìn)程的交互畢竟與創(chuàng)建服務(wù)器相關(guān)的信息全在子進(jìn)程的代碼中。

當(dāng)子進(jìn)程執(zhí)行

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

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

時(shí)http模塊會(huì)調(diào)用net模塊(確切的說http.Server繼承net.Server)創(chuàng)建net.Server對(duì)象同時(shí)偵聽端口。創(chuàng)建net.Server實(shí)例調(diào)用構(gòu)造函數(shù)返回。創(chuàng)建的net.Server實(shí)例調(diào)用listen(8000)等待accpet連接。那么子進(jìn)程如何傳遞服務(wù)器相關(guān)信息給主進(jìn)程呢答案就在listen函數(shù)中。我保證net.Server.prototype.listen函數(shù)絕沒有表面上看起來的那么簡(jiǎn)單它涉及到了許多IPC通信和兼容性處理可以說HTTP服務(wù)器創(chuàng)建的所有邏輯都在listen函數(shù)中。

延伸下在學(xué)習(xí)linux下的socket編程時(shí)服務(wù)端的邏輯依次是執(zhí)行socket(),bind(),listen()和accept()在接收到客戶端連接時(shí)執(zhí)行read(),write()調(diào)用完成TCP層的通信。那么對(duì)應(yīng)到node的net模塊好像只有listen()階段這是不是很難對(duì)應(yīng)socket的四個(gè)階段呢其實(shí)不然node的net模塊把“bindlisten”操作全部寫入了net.Server.prototype.listen中清晰的對(duì)應(yīng)底層socket和TCP三次握手而向上層使用者只暴露簡(jiǎn)單的listen接口。

code2

Server.prototype.listen = function() {

  ...  // 根據(jù)參數(shù)創(chuàng)建 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服務(wù)器的相關(guān)內(nèi)容因此我們只關(guān)注有關(guān)TCP服務(wù)器部分其他的Pipedomain socket服務(wù)不考慮。

listen函數(shù)可以偵聽端口、路徑和指定的fd因此在listen函數(shù)的實(shí)現(xiàn)中判斷各種參數(shù)的情況我們最為關(guān)心的就是偵聽端口的情況在成功進(jìn)入條件語(yǔ)句后發(fā)現(xiàn)所有的情況最后都執(zhí)行了listenInCluster函數(shù)而返回因此有必要繼續(xù)探究。

code3

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

  ...  if (cluster.isMaster || exclusive) {
    server._listen2(address, port, addressType, backlog, fd);    return;
  }  // 后續(xù)代碼為worker執(zhí)行邏輯
  const serverQuery = {
    address: address,
    port: port,
    addressType: addressType,
    fd: fd,
    flags: 0
  };

  ... 

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

listenInCluster函數(shù)傳入了各種參數(shù)如server實(shí)例、ip、port、ip類型IPv6和IPv4、backlog底層服務(wù)端socket處理請(qǐng)求的最大隊(duì)列、fd等它們不是必須傳入比如創(chuàng)建一個(gè)TCP服務(wù)器就僅僅需要一個(gè)port即可。

簡(jiǎn)化后的listenInCluster函數(shù)很簡(jiǎn)單cluster模塊判斷當(dāng)前進(jìn)程為主進(jìn)程時(shí)執(zhí)行_listen2函數(shù)否則在子進(jìn)程中執(zhí)行cluster._getServer函數(shù)同時(shí)像函數(shù)傳遞serverQuery對(duì)象即創(chuàng)建服務(wù)器需要的相關(guān)信息。

因此我們可以大膽假設(shè)子進(jìn)程在cluster._getServer函數(shù)中向主進(jìn)程發(fā)送了創(chuàng)建服務(wù)器所需要的數(shù)據(jù)即serverQuery。實(shí)際上也確實(shí)如此

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.
  });

};

子進(jìn)程在該函數(shù)中向已建立的IPC通道發(fā)送內(nèi)部消息message該消息包含之前提到的serverQuery信息同時(shí)包含act: 'queryServer'字段等待服務(wù)端響應(yīng)后繼續(xù)執(zhí)行回調(diào)函數(shù)modifyHandle。

主進(jìn)程接收到子進(jìn)程發(fā)送的內(nèi)部消息會(huì)根據(jù)act: 'queryServer'執(zhí)行對(duì)應(yīng)queryServer方法完成服務(wù)器的創(chuàng)建同時(shí)發(fā)送回復(fù)消息給子進(jìn)程子進(jìn)程執(zhí)行回調(diào)函數(shù)modifyHandle繼續(xù)接下來的操作。

至此針對(duì)主進(jìn)程在cluster模式下如何創(chuàng)建服務(wù)器的流程已完全走通主要的邏輯是在子進(jìn)程服務(wù)器的listen過程中實(shí)現(xiàn)。

net模塊與socket

上節(jié)提到了node中創(chuàng)建服務(wù)器無法與socket創(chuàng)建對(duì)應(yīng)的問題本節(jié)就該問題做進(jìn)一步解釋。在net.Server.prototype.listen函數(shù)中調(diào)用了listenInCluster函數(shù)listenInCluster會(huì)在主進(jìn)程或者子進(jìn)程的回調(diào)函數(shù)中調(diào)用_listen2函數(shù)對(duì)應(yīng)底層服務(wù)端socket建立階段的正是在這里。

function setupListenHandle(address, port, addressType, backlog, fd) {  // worker進(jìn)程中_handle為fake對(duì)象無需創(chuàng)建
  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函數(shù)創(chuàng)建句柄句柄可理解為用戶空間的socket同時(shí)給屬性onconnection賦值最后偵聽端口設(shè)定backlog。

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

function createServerHandle(address, port, addressType, fd) {  var handle;  // 針對(duì)網(wǎng)絡(luò)連接綁定地址
  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中我們看到了如何創(chuàng)建socketcreateServerHandle在底層利用node自己封裝的類庫(kù)創(chuàng)建TCP handle也看到了bind綁定ip和地址那么node的net模塊如何接收客戶端請(qǐng)求呢

必須深入c++模塊才能了解node是如何實(shí)現(xiàn)在c++層面調(diào)用js層設(shè)置的onconnection回調(diào)屬性v8引擎提供了c++和js層的類型轉(zhuǎn)換和接口透出在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);
}

我們關(guān)注uv_listen函數(shù)它是libuv封裝后的函數(shù)傳入了handle_,backlog和OnConnection回調(diào)函數(shù)其中handle_為node調(diào)用libuv接口創(chuàng)建的socket封裝OnConnection函數(shù)為socket接收客戶端連接時(shí)執(zhí)行的操作。我們可能會(huì)猜測(cè)在js層設(shè)置的onconnction函數(shù)最終會(huì)在OnConnection中調(diào)用于是進(jìn)一步深入探查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);
}

過濾掉多余信息便于分析。當(dāng)新的客戶端連接到來時(shí)libuv調(diào)用OnConnection在該函數(shù)內(nèi)執(zhí)行uv_accept接收連接最后將js層的回調(diào)函數(shù)onconnection[通過env->onconnection_string()獲取js的回調(diào)]和接收到的客戶端socket封裝傳入MakeCallback中。其中argv數(shù)組的第一項(xiàng)為錯(cuò)誤信息第二項(xiàng)為已連接的clientSocket封裝最后在MakeCallback中執(zhí)行js層的onconnection函數(shù)該函數(shù)的參數(shù)正是argv數(shù)組傳入的數(shù)據(jù)“錯(cuò)誤代碼和clientSocket封裝”。

js層的onconnection回調(diào)

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++層調(diào)用js層的onconnection函數(shù)構(gòu)建node層的socket對(duì)象并觸發(fā)connection事件完成底層socket與node net模塊的連接與請(qǐng)求打通。

至此我們打通了socket連接建立過程與net模塊js層的流程的交互這種封裝讓開發(fā)者在不需要查閱底層接口和數(shù)據(jù)結(jié)構(gòu)的情況下僅使用node提供的http模塊就可以快速開發(fā)一個(gè)應(yīng)用服務(wù)器將目光聚集在業(yè)務(wù)邏輯中。

backlog是已連接但未進(jìn)行accept處理的socket隊(duì)列大小。在linux 2.2以前backlog大小包括了半連接狀態(tài)和全連接狀態(tài)兩種隊(duì)列大小。linux 2.2以后分離為兩個(gè)backlog來分別限制半連接SYN_RCVD狀態(tài)的未完成連接隊(duì)列大小跟全連接ESTABLISHED狀態(tài)的已完成連接隊(duì)列大小。這里的半連接狀態(tài)即在三次握手中服務(wù)端接收到客戶端SYN報(bào)文后并發(fā)送SYN+ACK報(bào)文后的狀態(tài)此時(shí)服務(wù)端等待客戶端的ACK全連接狀態(tài)即服務(wù)端和客戶端完成三次握手后的狀態(tài)。backlog并非越大越好當(dāng)?shù)却齛ccept隊(duì)列過長(zhǎng)服務(wù)端無法及時(shí)處理排隊(duì)的socket會(huì)造成客戶端或者前端服務(wù)器如nignx的連接超時(shí)錯(cuò)誤出現(xiàn)“error: Broken Pipe”。因此node默認(rèn)在socket層設(shè)置backlog默認(rèn)值為511這是因?yàn)閚ginx和redis默認(rèn)設(shè)置的backlog值也為此盡量避免上述錯(cuò)誤。

多個(gè)子進(jìn)程與端口復(fù)用

再回到關(guān)于cluster模塊的主線中來。code1中主進(jìn)程與所有子進(jìn)程通過消息構(gòu)建出偵聽8000端口的TCP服務(wù)器那么子進(jìn)程中有沒有也創(chuàng)建一個(gè)服務(wù)器同時(shí)偵聽8000端口呢其實(shí)在子進(jìn)程中壓根就沒有這回事如何理解呢子進(jìn)程中確實(shí)創(chuàng)建了net.Server對(duì)象可是它沒有像主進(jìn)程那樣在libuv層構(gòu)建socket句柄子進(jìn)程的net.Server對(duì)象使用的是一個(gè)人為fake出的一個(gè)假句柄來“欺騙”使用者端口已偵聽這樣做的目的是為了集群的負(fù)載均衡這又涉及到了cluster模塊的均衡策略的話題上。

在本節(jié)有關(guān)cluster集群端口偵聽以及請(qǐng)求處理的描述都是基于cluster模式的默認(rèn)策略RoundRobin之上討論的關(guān)于調(diào)度策略的討論我們放在下節(jié)進(jìn)行。

主進(jìn)程與服務(wù)器這一章節(jié)最后我們只了解到主進(jìn)程是如何創(chuàng)建偵聽給定端口的TCP服務(wù)器的此時(shí)子進(jìn)程還在等待主進(jìn)程創(chuàng)建后發(fā)送的消息。當(dāng)主進(jìn)程發(fā)送創(chuàng)建服務(wù)器成功的消息后子進(jìn)程會(huì)執(zhí)行modifyHandle回調(diào)函數(shù)。還記得這個(gè)函數(shù)嗎主進(jìn)程與服務(wù)器這一章節(jié)最后已經(jīng)貼出來它的源碼

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.
  }

它會(huì)根據(jù)主進(jìn)程是否返回handle句柄即libuv對(duì)socket的封裝來選擇執(zhí)行函數(shù)。由于cluter默認(rèn)采用RoundRobin調(diào)度策略因此主進(jìn)程返回的handle為null執(zhí)行函數(shù)rr。在該函數(shù)中做了上文提到的hack操作作者fake了一個(gè)假的handle對(duì)象“欺騙”上層調(diào)用者

function listen(backlog) {    return 0;
  }  const handle = { close, listen, ref: noop, unref: noop };

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

看到了嗎fake出的handle.listen并沒有調(diào)用libuv層的Listen方法它直接返回了。這意味著什么子進(jìn)程壓根沒有創(chuàng)建底層的服務(wù)端socket做偵聽所以在子進(jìn)程創(chuàng)建的HTTP服務(wù)器偵聽的端口根本不會(huì)出現(xiàn)端口復(fù)用的情況。 最后調(diào)用cb函數(shù)將fake后的handle傳遞給上層net.Server設(shè)置net.Server對(duì)底層的socket的引用。此后子進(jìn)程利用fake后的handle做端口偵聽其實(shí)壓根啥都沒有做執(zhí)行成功后返回。

那么子進(jìn)程TCP服務(wù)器沒有創(chuàng)建底層socket如何接受請(qǐng)求和發(fā)送響應(yīng)呢這就要依賴IPC通道了。既然主進(jìn)程負(fù)責(zé)接受客戶端請(qǐng)求那么理所應(yīng)當(dāng)由主進(jìn)程分發(fā)客戶端請(qǐng)求給某個(gè)子進(jìn)程由子進(jìn)程處理請(qǐng)求。實(shí)際上也確實(shí)是這樣做的主進(jìn)程的服務(wù)器中會(huì)創(chuàng)建RoundRobinHandle決定分發(fā)請(qǐng)求給哪一個(gè)子進(jìn)程篩選出子進(jìn)程后發(fā)送newconn消息給對(duì)應(yīng)子進(jìn)程

  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);
  });

子進(jìn)程接收到newconn消息后會(huì)調(diào)用內(nèi)部的onconnection函數(shù)先向主進(jìn)程發(fā)送開始處理請(qǐng)求的消息然后執(zhí)行業(yè)務(wù)處理函數(shù)handle.onconnection。還記得這個(gè)handle.onconnection嗎它正是上節(jié)提到的node在c++層執(zhí)行的js層回調(diào)函數(shù)在handle.onconnection中構(gòu)造了net.Socket對(duì)象標(biāo)識(shí)已連接的socket最后觸發(fā)connection事件調(diào)用開發(fā)者的業(yè)務(wù)處理函數(shù)此時(shí)的數(shù)據(jù)處理對(duì)應(yīng)在網(wǎng)絡(luò)模型的第四層傳輸層中node的http模塊會(huì)從socket中獲取數(shù)據(jù)做應(yīng)用層的封裝解析出請(qǐng)求頭、請(qǐng)求體并構(gòu)造響應(yīng)體這樣便從內(nèi)核socket->libuv->js依次執(zhí)行到開發(fā)者的業(yè)務(wù)邏輯中。

到此為止相信讀者已經(jīng)明白node是如何處理客戶端的請(qǐng)求了那么下一步繼續(xù)探究node是如何分發(fā)客戶端的請(qǐng)求給子進(jìn)程的。

請(qǐng)求分發(fā)策略

上節(jié)提到cluster模塊默認(rèn)采用RoundRobin調(diào)度策略那么還有其他策略可以選擇嗎答案是肯定的在windows機(jī)器中cluster模塊采用的是共享服務(wù)端socket方式通俗點(diǎn)說就是由操作系統(tǒng)進(jìn)行調(diào)度客戶端的請(qǐng)求而不是由node程序調(diào)度。其實(shí)在node v0.8以前默認(rèn)的集群模式就是采用操作系統(tǒng)調(diào)度方式進(jìn)行直到cluster模塊的加入才有了改變。

那么RoundRobin調(diào)度策略到底是怎樣的呢

RoundRobinHandle.prototype.distribute = function(err, handle) {  this.handles.push(handle);  const worker = this.free.shift();  if (worker)    this.handoff(worker);
};// 發(fā)送消息和handle給對(duì)應(yīng)worker進(jìn)程處理業(yè)務(wù)邏輯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);
  });
};

核心代碼就是這兩個(gè)函數(shù)濃縮的是精華。distribute函數(shù)負(fù)責(zé)篩選出處理請(qǐng)求的子進(jìn)程this.free數(shù)組存儲(chǔ)空閑的子進(jìn)程this.handles數(shù)組存放待處理的用戶請(qǐng)求。handoff函數(shù)獲取排隊(duì)中的客戶端請(qǐng)求并通過IPC發(fā)送句柄handle和newconn消息等待子進(jìn)程返回。當(dāng)子進(jìn)程返回正在處理請(qǐng)求消息時(shí)在此執(zhí)行handoff函數(shù)繼續(xù)分配請(qǐng)求給該子進(jìn)程不管該子進(jìn)程上次請(qǐng)求是否處理完成node的異步特性和事件循環(huán)可以讓單進(jìn)程處理多請(qǐng)求。按照這樣的策略主進(jìn)程每fork一個(gè)子進(jìn)程都會(huì)調(diào)用handoff函數(shù)進(jìn)入該子進(jìn)程的處理循環(huán)中。一旦主進(jìn)程沒有緩存的客戶端請(qǐng)求時(shí)this.handles為空便會(huì)將當(dāng)前子進(jìn)程加入free空閑隊(duì)列等待主進(jìn)程的下一步調(diào)度。這就是cluster模式的RoundRobin調(diào)度策略每個(gè)子進(jìn)程的處理邏輯都是一個(gè)閉環(huán)直到主進(jìn)程緩存的客戶端請(qǐng)求處理完畢時(shí)該子進(jìn)程的處理閉環(huán)才被打開。

這么簡(jiǎn)單的實(shí)現(xiàn)帶來的效果卻是不小經(jīng)過全世界這么多使用者的嘗試主進(jìn)程分發(fā)請(qǐng)求還是很平均的如果RoundRobin的調(diào)度需求不滿足你業(yè)務(wù)中的要求你可以嘗試仿照RoundRobin模塊寫一個(gè)另類的調(diào)度算法。

那么cluster模塊在windows系統(tǒng)中采用的shared socket策略后文簡(jiǎn)稱SS策略是什么呢采用SS策略調(diào)度算法子進(jìn)程的服務(wù)器工作邏輯完全不同于上文中所講的那樣子進(jìn)程創(chuàng)建的TCP服務(wù)器會(huì)在底層偵聽端口并處理響應(yīng)這是如何實(shí)現(xiàn)的呢SS策略的核心在于IPC傳輸句柄的文件描述符并且在C++層設(shè)置端口的SO_REUSEADDR選項(xiàng)最后根據(jù)傳輸?shù)奈募枋龇€原出handle(net.TCP)處理請(qǐng)求。這正是shared socket名稱由來共享文件描述符。

子進(jìn)程繼承父進(jìn)程fd處理請(qǐng)求

import socketimport osdef 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()

需要指出的是在子進(jìn)程中根據(jù)文件描述符還原出的handle不能再進(jìn)行bind(ip,port)和listen(backlog)操作只有主進(jìn)程創(chuàng)建的handle可以調(diào)用這些函數(shù)。子進(jìn)程中只能選擇accept、read和write操作。

既然SS策略傳遞的是master進(jìn)程的服務(wù)端socket的文件描述符子進(jìn)程偵聽該描述符那么由誰(shuí)來調(diào)度哪個(gè)子進(jìn)程處理請(qǐng)求呢這就是由操作系統(tǒng)內(nèi)核來進(jìn)行調(diào)度??墒莾?nèi)核調(diào)度往往出現(xiàn)意想不到的效果在linux下導(dǎo)致請(qǐng)求往往集中在某幾個(gè)子進(jìn)程中處理。這從內(nèi)核的調(diào)度策略也可以推算一二內(nèi)核的進(jìn)程調(diào)度離不開上下文切換上下文切換的代價(jià)很高不僅需要保存當(dāng)前進(jìn)程的代碼、數(shù)據(jù)和堆棧等用戶空間數(shù)據(jù)還需要保存各種寄存器如PCESP最后還需要恢復(fù)被調(diào)度進(jìn)程的上下文狀態(tài)仍然包括代碼、數(shù)據(jù)和各種寄存器因此代價(jià)非常大。而linux內(nèi)核在調(diào)度這些子進(jìn)程時(shí)往往傾向于喚醒最近被阻塞的子進(jìn)程上下文切換的代價(jià)相對(duì)較小。而且內(nèi)核的調(diào)度策略往往受到當(dāng)前系統(tǒng)的運(yùn)行任務(wù)數(shù)量和資源使用情況對(duì)專注于業(yè)務(wù)開發(fā)的http服務(wù)器影響較大因此會(huì)造成某些子進(jìn)程的負(fù)載嚴(yán)重不均衡的狀況。那么為什么cluster模塊默認(rèn)會(huì)在windows機(jī)器中采用SS策略調(diào)度子進(jìn)程呢原因是node在windows平臺(tái)采用的IOCP來最大化性能它使得傳遞連接的句柄到其他進(jìn)程的成本很高因此采用默認(rèn)的依靠操作系統(tǒng)調(diào)度的SS策略。

SS調(diào)度策略非常簡(jiǎn)單主進(jìn)程直接通過IPC通道發(fā)送handle給子進(jìn)程即可此處就不針對(duì)代碼進(jìn)行分析了。此處筆者利用node的child_process模塊實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的SS調(diào)度策略的服務(wù)集群讀者可以更好的理解

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)
  });
});

上述就是小編為大家分享的Nodejs中cluster模塊的作用是什么了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

分享名稱:Nodejs中cluster模塊的作用是什么
本文網(wǎng)址:http://bm7419.com/article42/ipdeec.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、網(wǎng)站維護(hù)、面包屑導(dǎo)航、用戶體驗(yàn)、服務(wù)器托管企業(yè)網(wǎng)站制作

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

商城網(wǎng)站建設(shè)