Node.js の Cluster モジュールを使って Express サーバを並列化する
Node.js はシングルプロセスで処理するため、マルチコアを活かして並列処理するにはひと手間準備が必要になる。
今回は、Node.js 組み込みの cluster
モジュールを使って Express サーバを並列化してみる。
目次
元となる Express サーバ
今回元にする Express サーバの実装は以下のとおり。
/*! index.js */
const express = require('express');
express()
.get('/', (req, res) => {
console.log(`Request`);
res.send('Hello World');
})
.listen(8080, () => {
console.log(`Server Started`);
});
http://localhost:8080/
にアクセスすると「Hello World」と答えるだけのサーバだ。
cluster
モジュールを組み込む
このようなサーバプロセスを CPU のコア数分だけ Fork して上手く管理してもらうために、Node.js 組み込みの cluster
モジュールを組み込んでみる。
/*! index.js */
const cluster = require('cluster');
const os = require('os');
const express = require('express');
// CPU のコア (スレッド) 数を調べる
const numCPUs = os.cpus().length;
if(cluster.isMaster) {
console.log('Master');
// Worker を生成する
for(let i = 0; i < numCPUs; i++) {
console.log(`Master : Cluster Fork ${i}`);
cluster.fork();
}
// Worker がクラッシュしたら再生成する
cluster.on('exit', (worker, code, signal) => {
console.warn(`[${worker.id}] Worker died : [PID ${worker.process.pid}] [Signal ${signal}] [Code ${code}]`);
cluster.fork();
});
}
else {
console.log(`[${cluster.worker.id}] [PID ${cluster.worker.process.pid}] Worker`);
// Express サーバの実装は元のまま変更なし (コンソール出力の内容だけ加工)
express()
.get('/', (req, res) => {
console.log(`[${cluster.worker.id}] [PID ${cluster.worker.process.pid}] Request`);
res.send('Hello World');
})
.listen(8080, () => {
console.log(`[${cluster.worker.id}] [PID ${cluster.worker.process.pid}] Server Started`);
});
}
cluster.isMaster
で条件分岐し、マスターコードはプロセスの Fork のみ行い、ワーカコードはこれまでどおり Express サーバの起動のみを行っている。分かりやすくするためコンソール出力の内容だけ加工したものの、Express サーバの実装は何も変更していない。
動作を見てみる
このように実装した index.js
を起動して、どのように動くか見てみよう。
$ npm start
> express-cluster-practice@ start /Users/Neo/express-cluster-practice
> node index.js
Master
Master : Cluster Fork 0
Master : Cluster Fork 1
Master : Cluster Fork 2
Master : Cluster Fork 3
Master : Cluster Fork 4
Master : Cluster Fork 5
Master : Cluster Fork 6
Master : Cluster Fork 7
[1] [PID 58925] Worker
[1] [PID 58925] Server Started
[2] [PID 58926] Worker
[3] [PID 58927] Worker
[4] [PID 58928] Worker
[2] [PID 58926] Server Started
[4] [PID 58928] Server Started
[5] [PID 58929] Worker
[3] [PID 58927] Server Started
[8] [PID 58932] Worker
[6] [PID 58930] Worker
[7] [PID 58931] Worker
[5] [PID 58929] Server Started
[8] [PID 58932] Server Started
[6] [PID 58930] Server Started
[7] [PID 58931] Server Started
起動直後はこんな感じ。Master コードからプロセスを8つ Fork している。今回検証に使用した MacBookPro の CPU は Intel Core i7-7820HQ というモデルで、このモデルは4コア8スレッドだった。だから8つのプロセスが Fork できたのだろう。
$ node -e "console.log( require('os').cpus() )"
[ { model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 22561050, nice: 0, sys: 10492570, idle: 187783350, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 1412060, nice: 0, sys: 894360, idle: 218515020, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 19931150, nice: 0, sys: 5917620, idle: 194972820, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 1255140, nice: 0, sys: 730460, idle: 218835770, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 20051370, nice: 0, sys: 5997140, idle: 194773040, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 1252750, nice: 0, sys: 733980, idle: 218834600, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 19638190, nice: 0, sys: 5853950, idle: 195329350, irq: 0 } },
{ model: 'Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz',
speed: 2900,
times: { user: 1249330, nice: 0, sys: 727480, idle: 218844460, irq: 0 } } ]
cluster.worker.id
ないしは worker.id
は、プロセスの生成順に 1 からの連番が振られるだけで特に意味なし。プロセス ID (PID) は (cluster.)worker.process.pid
で確認できる。
$ ps
PID TTY TIME CMD
57532 ttys000 0:00.06 -bash
53123 ttys002 0:00.09 /bin/bash -l
57329 ttys003 0:00.05 /bin/bash -l
58923 ttys003 0:00.24 npm
58924 ttys003 0:00.15 node index.js
58925 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58926 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58927 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58928 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58929 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58930 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58931 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
58932 ttys003 0:00.21 /Users/Neo/.nodebrew/node/v8.15.0/bin/node /Users/Neo/express-cluster-practice/index.js
57819 ttys004 0:00.10 /bin/bash -l
ps
コマンドからも同じ PID が確認できる。
この状態で、curl
コマンドを3回ほど叩いてみる。
$ curl http://localhost:8080/
するとコンソールログには以下のように出力される。
[1] [PID 58925] Request
[2] [PID 58926] Request
[4] [PID 58928] Request
リクエストごとに別々のプロセスで反応していることが分かる。リスンするポートはどのプロセスも 8080
固定だが、cluster
モジュールがロードバランサの機能も持っているので、気にせず Fork させられる。
次に、Fork されたプロセスを一つ kill
してみる。
$ kill 58925
するとコンソールログには次のように出力される。
[1] Worker died : [PID 58925] [Signal SIGTERM] [Code null]
[9] [PID 59171] Worker
[9] [PID 59171] Server Started
cluster.on('exit')
部分の実装により、PID 58925
のプロセスが死んだことを検知し、新たなワーカプロセスを Fork できている。何か問題が起こってワーカプロセスがクラッシュした時も、コレで上手く対応できる。
以上
サクッと並列処理が実現できてよきよき。
- 参考 : Node.jsのClusterをセットアップして、処理を並列化・高速化する | POSTD
- 参考 : node.js clusterでHTTPサーバをマルチプロセス化する - Node.js/JavaScript入門
- 参考 : https://code.i-harness.com/ja/docs/node/cluster
- 参考 : 【Node.js+Express】Clusterモジュールでマルチスレッド化 - Qiita
- 参考 : Express.jsで作ったアプリをクラスタ化する - Qiita … express-cluster というラッパーモジュールもある
- 参考 : 【ps・kill】実行中のプロセス表示と強制終了 - Qiita