#はじめに
Node.jsはシングルスレッドでノンブロッキングI/Oを採用していることはもうたくさんの方がご存知かと思いますが、その説明をする度に「じゃあマルチコアな環境は活かせないの?」という声をよく聞いたので、今回はNode.jsでHTTPサーバーを作成する際によく使用されているExpressフレームワークでマルチスレッド化をどう実現するのかをやってみました。
私自身マルチスレッド化できるのは知っていましたが実装するのは初めてですので、
誤り等あればご指摘いただけると嬉しいです。
#環境
- OS:Windows7 Professional(x64)
- Node.js:8.12.0
- Express:4.16.0
前提条件
express-generatorを使用して雛形を作成し、"Welcome to Express"が表示できていること
#Clusterモジュールについて
Node.jsのClusterモジュールはマスタープロセスもしくはOSカーネルがロードバランサーとなってワーカープロセス(子プロセス)に処理を実行させる機能を持っているモジュールです。接続済ソケット共有型とリスニングソケット共有型の2パターンがあるようです。
前の記事ですがNode.js日本ユーザーグループのブログに詳しく書かれていました。
Node.js 日本ユーザグループ Blog
###接続済みソケット共有型とは
- v0.11.2からUnix系(非Windows)プラットフォームのデフォルト
- マスタープロセスはラウンドロビン方式でワーカープロセスに接続済みソケットを渡す
- 接続後のクライアントとの送受信はワーカープロセスが行う
- どのワーカープロセスに割り振るかはマスタープロセスが決定
-
cluster.schedulingPolicy = cluster.SCHED_RR
を指定する - もしくは環境変数
NODE_CLUSTER_SCHED_POLICY
にrr
を指定する
###リスニングソケット共有型とは
- v0.11.2からWindows系プラットフォームのデフォルト
- マスタープロセスは初期化時にリスニングソケットを準備するのみ
- どのワーカープロセスに割り振るかはOSカーネルが決定(マスタープロセスは関与しない)
- クライアントとのやり取りはワーカープロセスが直接行う
-
cluster.schedulingPolicy = cluster.SCHED_NONE
を指定する - もしくは環境変数
NODE_CLUSTER_SCHED_POLICY
にnone
を指定する
#Clusterモジュールを実際に使用してみる
それでは早速 express-generator
で作成しておいた雛形にClusterモジュールを組み込んでいきたいと思います。 npm start
で実行した際に最初に実行される ./bin/www
に以下の通り処理を追加します。
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
+ const cluster = require('cluster');
var debug = require('debug')('cluster:server');
var http = require('http');
+ const numCpus = require('os').cpus().length;
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
+ // cluster.schedulingPolicy = cluster.SCHED_NONE; // <- Windowsのデフォルト
+ cluster.schedulingPolicy = cluster.SCHED_RR; // <- Unix系のデフォルト(非Windows)
+ if (cluster.isMaster) { // マスタープロセスの処理
+ console.log(`Master ${process.pid} is running`);
+ // コア数分ワーカープロセスを作る
+ for (let i = 0; i < numCpus; i++) {
+ cluster.fork();
+ }
+ // ワーカープロセスが死んだ場合
+ cluster.on('exit', (worker, code, signal) => {
+ console.log(`worker ${worker.process.pid} died with signal`, signal);
+ // プロセス再起動
+ cluster.fork();
+ });
+ } else { // ワーカープロセスの処理
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
console.log(`Worker ${process.pid} started`);
+ }
/** 中略 **/
はい、これだけです。とっても簡単に実装できました。
今回は接続済みソケット共有型で実装しています(リスニングソケット共有型だと同一プロセスばかりでマルチ感が伝わってこなかったので)。
次は実行時にマルチプロセスで動作していることを確認するためにデフォルトのloggerの設定を書き換えてプロセスIDが確認できるように以下の変更を加えます。
/** 中略 **/
- app.use(logger('dev'));
+ app.use(logger(`:method :url :response-time ms pid=${process.pid}`));
/** 中略 **/
#実行結果
npm start
で実行すると以下のような形でワーカープロセスが起動していることがわかります。
D:\98_Study\cluster>npm start
> cluster@0.0.0 start D:\98_Study\cluster
> node ./bin/www
Master 11012 is running
Worker 12392 started
Worker 12144 started
Worker 13056 started
Worker 6632 started
Worker 12000 started
Worker 11764 started
Worker 1268 started
Worker 8832 started
試しにコマンド curl http://localhost:3000/
を実行して複数回リクエストを投げてみます。
すると以下のような形にログがはかれ、リクエストを投げるたびに異なるプロセスで処理されていることがわかります。
GET / 5.520 ms pid=12392
GET / 5.184 ms pid=12144
GET / 4.231 ms pid=13056
GET / 5.262 ms pid=6632
GET / 5.647 ms pid=12000
GET / 4.514 ms pid=11764
GET / 4.786 ms pid=1268
GET / 4.886 ms pid=8832
GET / 0.605 ms pid=12392
GET / 0.521 ms pid=12144
#おわりに
マルチスレッド化は複雑で難しそうな感じがしてなかなか手を出していませんでしたが、Clusterモジュールを使用することで結構簡単に実装できました。Node.jsはシングルスレッドのイベントループで処理されていることを頭に置いて、WEBアプリなどのリクエストを大量に捌く必要があるものに関してはマルチスレッドで実装する必要があるなと思いました。
私はVS Codeでコーディングするので、いつもの癖でデバッグ実行(シングルスレッド)で動作確認をしていて、「あれれ?ワーカープロセスが起動しないぞ?」とあわあわしちゃいましたが、皆さんはClusterモジュールを使用する際は npm start
で実行して下さいね。
#参考
Node.js v11.2.0 Documentation
Node.jsのClusterをセットアップして、処理を並列化・高速化する
Node.js 日本ユーザグループ Blog