Node js 叢集(cluster)

DC_er發表於2019-05-22

概述

基本用法

Node.js預設單程式執行,對於32位系統最高可以使用512MB記憶體,對於64位最高可以使用1GB記憶體。對於多核CPU的計算機來說,這樣做效率很低,因為只有一個核在執行,其他核都在閒置。cluster模組就是為了解決這個問題而提出的。

cluster模組允許設立一個主程式和若干個worker程式,由主程式監控和協調worker程式的執行。worker之間採用程式間通訊交換訊息,cluster模組內建一個負載均衡器,採用Round-robin演算法協調各個worker程式之間的負載。執行時,所有新建立的連結都由主程式完成,然後主程式再把TCP連線分配給指定的worker程式。

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

複製程式碼

上面程式碼先判斷當前程式是否為主程式(cluster.isMaster),如果是的,就按照CPU的核數,新建若干個worker程式;如果不是,說明當前程式是worker程式,則在該程式啟動一個伺服器程式。

上面這段程式碼有一個缺點,就是一旦work程式掛了,主程式無法知道。為了解決這個問題,可以在主程式部署online事件和exit事件的監聽函式。

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

複製程式碼

上面程式碼中,主程式一旦監聽到worker程式的exit事件,就會重啟一個worker程式。worker程式一旦啟動成功,可以正常執行了,就會發出online事件。

worker物件

worker物件是cluster.fork()的返回值,代表一個worker程式。

它的屬性和方法如下。

(1)worker.id

worker.id返回當前worker的獨一無二的程式編號。這個編號也是cluster.workers中指向當前程式的索引值。

(2)worker.process

所有的worker程式都是用child_process.fork()生成的。child_process.fork()返回的物件,就被儲存在worker.process之中。通過這個屬性,可以獲取worker所在的程式物件。

(3)worker.send()

該方法用於在主程式中,向子程式傳送資訊。

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

複製程式碼

上面程式碼的作用是,worker程式對主程式發出的每個訊息,都做回聲。

在worker程式中,要向主程式傳送訊息,使用process.send(message);要監聽主程式發出的訊息,使用下面的程式碼。

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

複製程式碼

發出的訊息可以字串,也可以是JSON物件。下面是一個傳送JSON物件的例子。

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

複製程式碼

cluster.workers物件

該物件只有主程式才有,包含了所有worker程式。每個成員的鍵值就是一個worker程式物件,鍵名就是該worker程式的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程式。

當前socket的data事件,也可以用id屬性識別worker程式。

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

複製程式碼

cluster模組的屬性與方法

isMaster,isWorker

isMaster屬性返回一個布林值,表示當前程式是否為主程式。這個屬性由process.env.NODE_UNIQUE_ID決定,如果process.env.NODE_UNIQUE_ID為未定義,就表示該程式是主程式。

isWorker屬性返回一個布林值,表示當前程式是否為work程式。它與isMaster屬性的值正好相反。

fork()

fork方法用於新建一個worker程式,上下文都複製主程式。只有主程式才能呼叫這個方法。

該方法返回一個worker物件。

kill()

kill方法用於終止worker程式。它可以接受一個引數,表示系統訊號。

如果當前是主程式,就會終止與worker.process的聯絡,然後將系統訊號法發向worker程式。如果當前是worker程式,就會終止與主程式的通訊,然後退出,返回0。

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

listening事件

worker程式呼叫listening方法以後,“listening”事件就傳向該程式的伺服器,然後傳向主程式。

該事件的回撥函式接受兩個引數,一個是當前worker物件,另一個是地址物件,包含網址、埠、地址型別(IPv4、IPv6、Unix socket、UDP)等資訊。這對於那些服務多個網址的Node應用程式非常有用。

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

複製程式碼

不中斷地重啟Node服務

思路

重啟服務需要關閉後再啟動,利用cluster模組,可以做到先啟動一個worker程式,再把原有的所有work程式關閉。這樣就能實現不中斷地重啟Node服務。

首先,主程式向worker程式發出重啟訊號。

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

複製程式碼

worker程式監聽message事件,一旦發現內容是shutdown,就退出。

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

複製程式碼

下面是一個關閉所有worker程式的函式。

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

複製程式碼

例項

下面是一個完整的例項,先是主程式的程式碼master.js。

var cluster = require('cluster');

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

// 新建一個worker程式
cluster.fork();

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

複製程式碼

上面程式碼中,主程式監聽SIGHUP事件,如果發生該事件就關閉其他所有worker程式。之所以是SIGHUP事件,是因為nginx伺服器監聽到這個訊號,會創造一個新的worker程式,重新載入配置檔案。另外,關閉worker程式時,主程式傳送SIGTERM訊號,這是因為Node允許多個worker程式監聽同一個埠。

下面是worker程式的程式碼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');
});

複製程式碼

使用時程式碼如下。

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

複製程式碼

然後,向主程式連續發出兩次SIGHUP訊號。

$ kill -SIGHUP 10538
$ kill -SIGHUP 10538

複製程式碼

主程式會連續兩次新建一個worker程式,然後關閉所有其他worker程式,顯示如下。

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

複製程式碼

最後,向主程式發出SIGTERM訊號,關閉主程式。

$ kill 10538

複製程式碼

PM2模組

PM2模組是cluster模組的一個包裝層。它的作用是儘量將cluster模組抽象掉,讓使用者像使用單程式一樣,部署多程式Node應用。

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

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

複製程式碼

上面程式碼是標準的Node架設Web伺服器的方式,然後用PM2從命令列啟動這段程式碼。

$ pm2 start app.js -i 4

複製程式碼

上面程式碼的i引數告訴PM2,這段程式碼應該在cluster_mode啟動,且新建worker程式的數量是4個。如果i引數的值是0,那麼當前機器有幾個CPU核心,PM2就會啟動幾個worker程式。

如果一個worker程式由於某種原因掛掉了,會立刻重啟該worker程式。

# 重啟所有worker程式
$ pm2 reload all

複製程式碼

每個worker程式都有一個id,可以用下面的命令檢視單個worker程式的詳情。

$ pm2 show <worker id>

複製程式碼

正確情況下,PM2採用fork模式新建worker程式,即主程式fork自身,產生一個worker程式。pm2 reload命令則會用spawn方式啟動,即一個接一個啟動worker程式,一個新的worker啟動成功,再殺死一箇舊的worker程式。採用這種方式,重新部署新版本時,伺服器就不會中斷服務。

$ pm2 reload <指令碼檔名>

複製程式碼

關閉worker程式的時候,可以部署下面的程式碼,讓worker程式監聽shutdown訊息。一旦收到這個訊息,進行完畢收尾清理工作再關閉。

process.on('message', function(msg) {
  if (msg === 'shutdown') {
    close_all_connections();
    delete_logs();
    server.close();
    process.exit(0);
  }
});
複製程式碼

相關文章