Node.js 集群(cluster)

概述

基本用法

Node.js默認(rèn)單進(jìn)程運(yùn)行,對(duì)于32位系統(tǒng)最高可以使用512MB內(nèi)存空民,對(duì)于64位最高可以使用1GB內(nèi)存烦却。對(duì)于多核CPU的計(jì)算機(jī)來說豌鹤,這樣做效率很低,因?yàn)橹挥幸粋€(gè)核在運(yùn)行,其他核都在閑置凤薛。cluster模塊就是為了解決這個(gè)問題而提出的郊霎。

cluster模塊允許設(shè)立一個(gè)主進(jìn)程和若干個(gè)worker進(jìn)程槐沼,由主進(jìn)程監(jiān)控和協(xié)調(diào)worker進(jìn)程的運(yùn)行阶冈。worker之間采用進(jìn)程間通信交換消息,cluster模塊內(nèi)置一個(gè)負(fù)載均衡器葫慎,采用Round-robin算法協(xié)調(diào)各個(gè)worker進(jìn)程之間的負(fù)載衔彻。運(yùn)行時(shí),所有新建立的鏈接都由主進(jìn)程完成偷办,然后主進(jìn)程再把TCP連接分配給指定的worker進(jìn)程艰额。

var cluster = require('cluster');
var os = require('os');

if (cluster.isMaster){
  for (var i = 0, n = os.cpus().length; i < n; i += 1){
    cluster.fork();
  }
} else {
  http.createServer(function(req, res) {
    res.writeHead(200);
    res.end("hello world\n");
  }).listen(8000);
}

上面代碼先判斷當(dāng)前進(jìn)程是否為主進(jìn)程(cluster.isMaster),如果是的椒涯,就按照CPU的核數(shù)柄沮,新建若干個(gè)worker進(jìn)程;如果不是废岂,說明當(dāng)前進(jìn)程是worker進(jìn)程祖搓,則在該進(jìn)程啟動(dòng)一個(gè)服務(wù)器程序。

上面這段代碼有一個(gè)缺點(diǎn)湖苞,就是一旦work進(jìn)程掛了拯欧,主進(jìn)程無法知道。為了解決這個(gè)問題财骨,可以在主進(jìn)程部署online事件和exit事件的監(jiān)聽函數(shù)镐作。

var cluster = require('cluster');

if(cluster.isMaster) {
  var numWorkers = require('os').cpus().length;
  console.log('Master cluster setting up ' + numWorkers + ' workers...');

  for(var i = 0; i < numWorkers; i++) {
    cluster.fork();
  }

  cluster.on('online', function(worker) {
    console.log('Worker ' + worker.process.pid + ' is online');
  });

  cluster.on('exit', function(worker, code, signal) {
    console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
    console.log('Starting a new worker');
    cluster.fork();
  });
}

上面代碼中,主進(jìn)程一旦監(jiān)聽到worker進(jìn)程的exit事件隆箩,就會(huì)重啟一個(gè)worker進(jìn)程滑肉。worker進(jìn)程一旦啟動(dòng)成功,可以正常運(yùn)行了摘仅,就會(huì)發(fā)出online事件。

worker對(duì)象

worker對(duì)象是cluster.fork()的返回值问畅,代表一個(gè)worker進(jìn)程娃属。

它的屬性和方法如下六荒。

(1)worker.id

worker.id返回當(dāng)前worker的獨(dú)一無二的進(jìn)程編號(hào)。這個(gè)編號(hào)也是cluster.workers中指向當(dāng)前進(jìn)程的索引值矾端。

(2)worker.process

所有的worker進(jìn)程都是用child_process.fork()生成的掏击。child_process.fork()返回的對(duì)象,就被保存在worker.process之中秩铆。通過這個(gè)屬性砚亭,可以獲取worker所在的進(jìn)程對(duì)象。

(3)worker.send()

該方法用于在主進(jìn)程中殴玛,向子進(jìn)程發(fā)送信息捅膘。

if (cluster.isMaster) {
  var worker = cluster.fork();
  worker.send('hi there');
} else if (cluster.isWorker) {
  process.on('message', function(msg) {
    process.send(msg);
  });
}

上面代碼的作用是,worker進(jìn)程對(duì)主進(jìn)程發(fā)出的每個(gè)消息滚粟,都做回聲寻仗。

在worker進(jìn)程中,要向主進(jìn)程發(fā)送消息凡壤,使用process.send(message)署尤;要監(jiān)聽主進(jìn)程發(fā)出的消息,使用下面的代碼亚侠。

process.on('message', function(message) {
  console.log(message);
});

發(fā)出的消息可以字符串曹体,也可以是JSON對(duì)象。下面是一個(gè)發(fā)送JSON對(duì)象的例子硝烂。

worker.send({
  type: 'task 1',
  from: 'master',
  data: {
    // the data that you want to transfer
  }
});

cluster.workers對(duì)象

該對(duì)象只有主進(jìn)程才有箕别,包含了所有worker進(jìn)程。每個(gè)成員的鍵值就是一個(gè)worker進(jìn)程對(duì)象钢坦,鍵名就是該worker進(jìn)程的worker.id屬性究孕。

function eachWorker(callback) {
  for (var id in cluster.workers) {
    callback(cluster.workers[id]);
  }
}
eachWorker(function(worker) {
  worker.send('big announcement to all workers');
});

上面代碼用來遍歷所有worker進(jìn)程。

當(dāng)前socket的data事件爹凹,也可以用id屬性識(shí)別worker進(jìn)程厨诸。

socket.on('data', function(id) {
  var worker = cluster.workers[id];
});

cluster模塊的屬性與方法

isMaster,isWorker

isMaster屬性返回一個(gè)布爾值禾酱,表示當(dāng)前進(jìn)程是否為主進(jìn)程微酬。這個(gè)屬性由process.env.NODE_UNIQUE_ID決定,如果process.env.NODE_UNIQUE_ID為未定義颤陶,就表示該進(jìn)程是主進(jìn)程颗管。

isWorker屬性返回一個(gè)布爾值,表示當(dāng)前進(jìn)程是否為work進(jìn)程滓走。它與isMaster屬性的值正好相反垦江。

fork()

fork方法用于新建一個(gè)worker進(jìn)程,上下文都復(fù)制主進(jìn)程搅方。只有主進(jìn)程才能調(diào)用這個(gè)方法比吭。

該方法返回一個(gè)worker對(duì)象绽族。

kill()

kill方法用于終止worker進(jìn)程。它可以接受一個(gè)參數(shù)衩藤,表示系統(tǒng)信號(hào)吧慢。

如果當(dāng)前是主進(jìn)程,就會(huì)終止與worker.process的聯(lián)絡(luò)赏表,然后將系統(tǒng)信號(hào)法發(fā)向worker進(jìn)程检诗。如果當(dāng)前是worker進(jìn)程,就會(huì)終止與主進(jìn)程的通信瓢剿,然后退出逢慌,返回0。

在以前的版本中跋选,該方法也叫做 worker.destroy() 涕癣。

listening事件

worker進(jìn)程調(diào)用listening方法以后,“l(fā)istening”事件就傳向該進(jìn)程的服務(wù)器前标,然后傳向主進(jìn)程坠韩。

該事件的回調(diào)函數(shù)接受兩個(gè)參數(shù),一個(gè)是當(dāng)前worker對(duì)象炼列,另一個(gè)是地址對(duì)象只搁,包含網(wǎng)址、端口俭尖、地址類型(IPv4氢惋、IPv6、Unix socket稽犁、UDP)等信息焰望。這對(duì)于那些服務(wù)多個(gè)網(wǎng)址的Node應(yīng)用程序非常有用。

cluster.on('listening', function (worker, address) {
  console.log("A worker is now connected to " + address.address + ":" + address.port);
});

不中斷地重啟Node服務(wù)

思路

重啟服務(wù)需要關(guān)閉后再啟動(dòng)已亥,利用cluster模塊熊赖,可以做到先啟動(dòng)一個(gè)worker進(jìn)程,再把原有的所有work進(jìn)程關(guān)閉虑椎。這樣就能實(shí)現(xiàn)不中斷地重啟Node服務(wù)震鹉。

首先,主進(jìn)程向worker進(jìn)程發(fā)出重啟信號(hào)捆姜。

workers[wid].send({type: 'shutdown', from: 'master'});

worker進(jìn)程監(jiān)聽message事件传趾,一旦發(fā)現(xiàn)內(nèi)容是shutdown,就退出泥技。

process.on('message', function(message) {
  if(message.type === 'shutdown') {
    process.exit(0);
  }
});

下面是一個(gè)關(guān)閉所有worker進(jìn)程的函數(shù)浆兰。

function restartWorkers() {
  var wid, workerIds = [];
  for(wid in cluster.workers) {
    workerIds.push(wid);
  }

  workerIds.forEach(function(wid) {
    cluster.workers[wid].send({
      text: 'shutdown',
      from: 'master'
     });
    setTimeout(function() {
      if(cluster.workers[wid]) {
        cluster.workers[wid].kill('SIGKILL');
      }
    }, 5000);
  });
};

實(shí)例

下面是一個(gè)完整的實(shí)例,先是主進(jìn)程的代碼master.js。

var cluster = require('cluster');

console.log('started master with ' + process.pid);

// 新建一個(gè)worker進(jìn)程
cluster.fork();

process.on('SIGHUP', function () {
  console.log('Reloading...');
  var new_worker = cluster.fork();
  new_worker.once('listening', function () {
    // 關(guān)閉所有其他worker進(jìn)程
    for(var id in cluster.workers) {
      if (id === new_worker.id.toString()) continue;
      cluster.workers[id].kill('SIGTERM');
    }
  });
});

上面代碼中镊讼,主進(jìn)程監(jiān)聽SIGHUP事件宽涌,如果發(fā)生該事件就關(guān)閉其他所有worker進(jìn)程。之所以是SIGHUP事件蝶棋,是因?yàn)閚ginx服務(wù)器監(jiān)聽到這個(gè)信號(hào),會(huì)創(chuàng)造一個(gè)新的worker進(jìn)程忽妒,重新加載配置文件玩裙。另外,關(guān)閉worker進(jìn)程時(shí)段直,主進(jìn)程發(fā)送SIGTERM信號(hào)吃溅,這是因?yàn)镹ode允許多個(gè)worker進(jìn)程監(jiān)聽同一個(gè)端口。

下面是worker進(jìn)程的代碼server.js鸯檬。

var cluster = require('cluster');

if (cluster.isMaster) {
  require('./master');
  return;
}

var express = require('express');
var http = require('http');
var app = express();

app.get('/', function (req, res) {
  res.send('ha fsdgfds gfds gfd!');
});

http.createServer(app).listen(8080, function () {
  console.log('http://localhost:8080');
});

使用時(shí)代碼如下决侈。

$ node server.js
started master with 10538
http://localhost:8080

然后,向主進(jìn)程連續(xù)發(fā)出兩次SIGHUP信號(hào)喧务。

$ kill -SIGHUP 10538
$ kill -SIGHUP 10538

主進(jìn)程會(huì)連續(xù)兩次新建一個(gè)worker進(jìn)程赖歌,然后關(guān)閉所有其他worker進(jìn)程,顯示如下功茴。

Reloading...
http://localhost:8080
Reloading...
http://localhost:8080

最后庐冯,向主進(jìn)程發(fā)出SIGTERM信號(hào),關(guān)閉主進(jìn)程坎穿。

$ kill 10538

PM2模塊

PM2模塊是cluster模塊的一個(gè)包裝層展父。它的作用是盡量將cluster模塊抽象掉,讓用戶像使用單進(jìn)程一樣玲昧,部署多進(jìn)程N(yùn)ode應(yīng)用栖茉。

// app.js
var http = require('http');

http.createServer(function(req, res) {
  res.writeHead(200);
  res.end("hello world");
}).listen(8080);

上面代碼是標(biāo)準(zhǔn)的Node架設(shè)Web服務(wù)器的方式,然后用PM2從命令行啟動(dòng)這段代碼孵延。

$ pm2 start app.js -i 4

上面代碼的i參數(shù)告訴PM2吕漂,這段代碼應(yīng)該在cluster_mode啟動(dòng),且新建worker進(jìn)程的數(shù)量是4個(gè)隙袁。如果i參數(shù)的值是0痰娱,那么當(dāng)前機(jī)器有幾個(gè)CPU內(nèi)核,PM2就會(huì)啟動(dòng)幾個(gè)worker進(jìn)程菩收。

如果一個(gè)worker進(jìn)程由于某種原因掛掉了梨睁,會(huì)立刻重啟該worker進(jìn)程。

# 重啟所有worker進(jìn)程
$ pm2 reload all

每個(gè)worker進(jìn)程都有一個(gè)id娜饵,可以用下面的命令查看單個(gè)worker進(jìn)程的詳情坡贺。

$ pm2 show <worker id>

正確情況下,PM2采用fork模式新建worker進(jìn)程,即主進(jìn)程fork自身遍坟,產(chǎn)生一個(gè)worker進(jìn)程拳亿。pm2 reload命令則會(huì)用spawn方式啟動(dòng),即一個(gè)接一個(gè)啟動(dòng)worker進(jìn)程愿伴,一個(gè)新的worker啟動(dòng)成功肺魁,再殺死一個(gè)舊的worker進(jìn)程。采用這種方式隔节,重新部署新版本時(shí)鹅经,服務(wù)器就不會(huì)中斷服務(wù)。

$ pm2 reload <腳本文件名>

關(guān)閉worker進(jìn)程的時(shí)候怎诫,可以部署下面的代碼瘾晃,讓worker進(jìn)程監(jiān)聽shutdown消息。一旦收到這個(gè)消息幻妓,進(jìn)行完畢收尾清理工作再關(guān)閉蹦误。

process.on('message', function(msg) {
  if (msg === 'shutdown') {
    close_all_connections();
    delete_logs();
    server.close();
    process.exit(0);
  }
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市肉津,隨后出現(xiàn)的幾起案子强胰,更是在濱河造成了極大的恐慌,老刑警劉巖阀圾,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哪廓,死亡現(xiàn)場離奇詭異,居然都是意外死亡初烘,警方通過查閱死者的電腦和手機(jī)涡真,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來肾筐,“玉大人哆料,你說我怎么就攤上這事÷痤恚” “怎么了东亦?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長唬渗。 經(jīng)常有香客問我典阵,道長,這世上最難降的妖魔是什么镊逝? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任壮啊,我火速辦了婚禮,結(jié)果婚禮上撑蒜,老公的妹妹穿的比我還像新娘歹啼。我一直安慰自己玄渗,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布狸眼。 她就那樣靜靜地躺著藤树,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拓萌。 梳的紋絲不亂的頭發(fā)上岁钓,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音微王,去河邊找鬼甜紫。 笑死,一個(gè)胖子當(dāng)著我的面吹牛骂远,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播腰根,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼激才,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了额嘿?” 一聲冷哼從身側(cè)響起瘸恼,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎册养,沒想到半個(gè)月后东帅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡球拦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年靠闭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片坎炼。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愧膀,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出谣光,到底是詐尸還是另有隱情檩淋,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布萄金,位于F島的核電站蟀悦,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏氧敢。R本人自食惡果不足惜日戈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望福稳。 院中可真熱鬧涎拉,春花似錦瑞侮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至季俩,卻和暖如春钮糖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酌住。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國打工店归, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酪我。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓消痛,卻偏偏與公主長得像,于是被迫代替她去往敵國和親都哭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子秩伞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容

  • 很多Node.js初學(xué)者都會(huì)有這樣的疑惑,Node.js到底是單線程的還是多線程的欺矫?通過本章的學(xué)習(xí)纱新,能夠讓讀者較為...
    越努力越幸運(yùn)_952c閱讀 3,644評(píng)論 4 36
  • # 模塊機(jī)制 node采用模塊化結(jié)構(gòu),按照CommonJS規(guī)范定義和使用模塊穆趴,模塊與文件是一一對(duì)應(yīng)關(guān)系脸爱,即加載一個(gè)...
    RichRand閱讀 2,502評(píng)論 0 3
  • 文:正龍(滬江網(wǎng)校Web前端工程師) 本文原創(chuàng),轉(zhuǎn)載請(qǐng)注明作者及出處 之前的文章“走進(jìn)Node.js之HTTP實(shí)現(xiàn)...
    iKcamp閱讀 893評(píng)論 1 3
  • 前言 通過前邊的學(xué)習(xí)未妹,大家應(yīng)該已經(jīng)充分理解了node的單線程只不過是js層面的單線程簿废,是基于V8引擎的單線程,因?yàn)?..
    白昔月閱讀 4,551評(píng)論 3 13
  • 原文地址在我的博客教寂,轉(zhuǎn)載請(qǐng)注明出處捏鱼,謝謝! node 模塊是node 完成強(qiáng)大功能的實(shí)現(xiàn)者酪耕。node 的核心模塊包...
    莫凡_Tcg閱讀 606評(píng)論 0 1