はじめてのNode.js:マルチプロセスアプリケーションを作成する

 Node.jsは原則としてシングルスレッドで動作する。そのため、多くのリソースを消費するプログラムでは、リクエストを受信してからレスポンスを返すまでの遅延が大きくなってしまう可能性がある。このような場合、複数のプロセスでプログラムを実行し、リクエストを振り分けることでサーバーのCPUリソースをより効率良く利用できる可能性がある。今回はNode.jsで複数プロセスを利用するための方法を紹介する。

【連載】はじめてのNode.js

本記事について

 本記事は、3月13日にソフトバンク クリエイティブより発売された書籍「はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-」から、「第14章 複数のプロセスを利用するアプリケーションを作る」の一部を抜き出し再構成したものです。

 出版社ページ / Amazon.co.jpの商品ページ

 大型本: 384ページ、価格:3,045円(税込)、ISBN: 978-4797370904

Node.jsで複数プロセスを利用するアプリケーションを作る

 Node.jsでは通常、1つのスレッドですべての処理を実行する。そのため、プログラム中にCPUリソースを大きく消費するような個所があった場合、それ以外の処理がその影響を受ける可能性がある。たとえばサーバーアプリケーションにおいてクライアントからのリクエストを受けてイベントが発生しても、そのイベントハンドラは即座に呼び出されるわけではなく、そのときに実行されている処理が完了した段階で次のイベントハンドラが実行される。たとえば実行開始から実行終了まで10秒かかるような処理を実行している場合、その10秒間はほかのイベントを一切処理できないのである。

 また、近年では複数コア・複数CPUを搭載するサーバーが一般的になっており、多くのサーバーソフトウェアでは複数のスレッドやプロセスでリクエストを処理することでCPUリソースをより活用できるようにしている。Node.jsはマルチスレッドによる処理はサポートしていないものの、複数のプロセスを利用した処理についてはサポートされている。複数のプロセス間でメモリ空間を共有することはできないものの、独自のプロセス間通信機構によって情報をやり取りしたり、ポートやソケットを共有してリクエストを複数プロセスで分散処理することが可能だ。これにより、複数のCPUやコアを有効に活用することができる。以下では、Node.jsで子プロセスを生成したり、プロセス間で情報のやり取りを行う方法について紹介する。

子プロセスとしてNode.jsモジュールを実行する

 Node.jsのコアモジュールであるchild_processモジュールでは、子プロセスを生成するための関数が提供されている。これを利用することで、子プロセスを生成して任意のコマンドを実行したり、指定したNode.jsモジュールを子プロセスで実行させることができる

 child_process.fork関数は、子プロセスとして指定したNode.jsモジュールを実行する関数だ。

child_process.fork(modulePath, [args], [options])

 modulePath引数には子プロセスとして実行するモジュールのパス名を、args引数には実行時に与えるコマンドライン引数を、options引数にはオプション設定を格納したオブジェクトを指定する。options引数で設定できるプロパティは表1のとおりだ。

表1 child_process.fork関数のoptions引数で指定できるオプション
オプション名説明
cwd子プロセスのカレントディレクトリ
env子プロセスに与える環境変数を格納したオブジェクト
encoding標準出力および標準エラー出力のエンコーディング。デフォルトは’utf8′

 また、関数の戻り値としてはchild_process.ChildProcessクラスのインスタンスを返す。

 child_process.fork関数は実行されるとNode.jsの新たな実行インスタンスを作成して指定したモジュールを実行する。このとき、標準入出力は子プロセスと親プロセスで共有して利用される。なお、モジュールの実行開始までには30ミリ秒ほどが必要で、またメモリも各プロセスごとに最低でも10MB程度が必要となる。そのため、多数のプロセスを生成して利用するのは現実的ではない。また、プロセス間での変数の共有などは行われない。ただし、後述するIPC機構を利用してプロセス間でメッセージを送信しあうことは可能だ。

子プロセスと通信を行う——child_process.ChildProcessクラス

 さて、child_processモジュールで定義されているchild_process.ChildProcessクラスについてもう少し詳しく解説をしておこう。child_process.ChildProcessクラスでは表2のプロパティや表3のメソッド、そして表4のイベントが定義されている。

表2 child_process.ChildProcessクラスで定義されているプロパティ
プロパティ名説明
stdinWritable Stream子プロセスの標準入力にアクセスするためのストリームオブジェクト。標準入力が親プロセスと共有される場合はundefinedに設定される
stdoutReadable Stream子プロセスの標準出力にアクセスするためのストリームオブジェクト。標準出力が親プロセスと共有される場合はundefinedに設定される
stderrReadable Stream子プロセスの標準エラー出力にアクセスするためのストリームオブジェクト。標準エラー出力が親プロセスと共有される場合はundefinedに設定される
pid整数子プロセスのプロセスID(PID)
表3 child_process.ChildProcessクラスで定義されているメソッド
メソッド名説明
kill([signal]子プロセスに対しsignal引数で指定したシグナルを送信する。signal引数が省略された場合、SIGTERMシグナルが送信される
send(message, [sendHandle])子プロセスに対しIPCを使ってメッセージを送信する
disconnect()子プロセスとのIPC接続を切断する
表4 child_process.ChildProcessクラスで定義されているイベント
イベント名イベントハンドラに与えられる引数説明
exit(code, signal)子プロセスが終了したときに発生する。code引数にはその終了コードが、外部からシグナルを受信して終了した際にはsignal引数にそのシグナル名が格納される
closeなし子プロセスの標準入出力が閉じられたときに発生する
disconnectなしdisconnectメソッドでIPC接続が切断されたときに発生する
message(message, sendHandle)sendメソッドでメッセージが送信されたときに発生する。message引数には送信されたメッセージが、sendHandle引数にはsendメソッドで指定されたハンドルオブジェクトが格納される

 ここで、特に説明が必要なのはsendメソッドおよびmessageイベントだろう。sendメソッドでは、message引数で指定した任意のメッセージを子プロセスに送信するものだ。子プロセスはメッセージを受け取ると、processオブジェクトでmessageイベントが発生し、そのイベントハンドラにメッセージが渡される。また、このときsendHandle引数を使ってnet.Server型のオブジェクト、もしくはnet.Socket型のオブジェクトを親プロセスに送信することもできる。これを利用することで、サーバーの待ち受けを親プロセスで行い、リクエストの処理は子プロセスで行う、といった処理が実現できる。

送信できるメッセージに関する制限
ただしNode.jsは「{cmd: ‘NODE_foobar’}」のようなcmdプロパティを持ち、かつその値が’NODE_’で始まるようなメッセージを内部的に利用する。そのため、このようなメッセージの送信は避けることが好ましい。

 いっぽう、子プロセスではprocess.sendメソッドを使って親プロセスにメッセージを送信できる。

process.send(message, [sendHandle])

 process.sendメソッドに与えられる引数は、child_process.ChildProcessクラスのsendメソッドに与えられる引数と同一だ。process.sendメソッドが実行されると、その親プロセス内で対応するchild_process.ChildProcessクラスのインスタンスでmessageイベントが発生し、イベントハンドラにメッセージなどが与えられる。

複数プロセスを使ったサーバーを実装する

 前述のように、child_process.ChildProcessクラスのsendメソッドではsendHandle引数を使ってnet.Server型のオブジェクトやnet.Socket型のオブジェクトを子プロセスに送信することで、あたかも複数のプロセス間でオブジェクトを共有しているかのように利用できる。

 たとえばnet.Server型オブジェクトを複数プロセスで共有している状態でそのサーバーへの接続が行われると、その接続はオブジェクトを共有しているプロセスのどれか1つに振り分けられ、そのプロセスでのみイベントが発生する。たとえば以下のserver.js(リスト1)は、親プロセスと1つの子プロセスを使って8080版ポートへのHTTPリクエストを処理するものだ。

リスト1 server.js
var net = require('net');
var http = require('http');
var child_process = require('child_process');

var child = child_process.fork('child.js');
var server = net.createServer();

server.listen(8080, '127.0.0.1', function () {
  child.send('server', server);
  console.log('create parent server...');

  var httpServer = http.createServer();
  httpServer.on('request', function (request, response) {
    console.log('processed by parent: ' + request.url);
    response.writeHead(200);
    response.write('processed by parent');
    response.end();
  });
  httpServer.listen(server);
});

 まず親プロセス(server.js)ではchild_process.fork関数を使ってchild.jsというモジュールを子プロセスで実行するとともに、8080番ポートで待ち受けを行うnet.Server型のserverオブジェクトを作成し、そのオブジェクトを子プロセスに’server’というメッセージとともに送信している。続いてhttp.Serverクラスのインスタンスをを作成し、serverオブジェクトを使ってHTTPでの待ち受けを行わせている。このHTTPサーバーはリクエストを受け付けると「processed by parent」という文字列をレスポンスとして返すとともに、コンソールに同様の文字列を表示するようになっている。

 また、子プロセスとして実行させるchild.jsモジュールはリスト2のようになる。

リスト2 child.js
var http = require('http');

process.on('message', function (msg, server) {
  if (msg === 'server') {
    console.log('create child server...');
    var httpServer = http.createServer();
    httpServer.on('request', function (request, response) {
      console.log('processed by child: ' + request.url);
      response.writeHead(200);
      response.write('processed by child');
      response.end();
    });
    httpServer.listen(server);
  }
});

 child.jsモジュールでは親プロセスから’server’というメッセージとともにserverオブジェクトを受け取るようになっている。続いてhttp.Serverクラスのインスタンスを作成し、受け取ったserverオブジェクトを使って待ち受けを行わせている。このHTTPサーバーではリクエストを受け付けると「processed by child」という文字列をレスポンスとして返すとともに、コンソールに同様の文字列を表示するようになっている。

 このアプリケーションを実行した状態でpsコマンドでプロセスを確認すると、次のように2つのnodeプロセスが実行されていることが分かる。

$ ps -a | grep node
10881 ttys000    0:00.11 node server.js
10882 ttys000    0:00.09 /usr/local/bin/node child.js
10884 ttys001    0:00.00 grep node

 また、Webブラウザなどから「http://127.0.0.1/」に繰り返しアクセスすると、次のようにコンソールに表示されるメッセージが変化し、親プロセスと子プロセスの両方にリクエストが振り分けられていることが分かる。

$ node server.js
create parent server...
create child server...
processed by child: /
processed by child: /
processed by child: /
processed by parent: /
processed by child: /
processed by parent: /
processed by parent: /
processed by child: /
processed by parent: /
processed by child: /

【連載】はじめてのNode.js

本記事について

 本記事は、3月13日にソフトバンク クリエイティブより発売された書籍「はじめてのNode.js -サーバーサイドJavaScriptでWebアプリを開発する-」から、「第14章 複数のプロセスを利用するアプリケーションを作る」の一部を抜き出し再構成したものです。

 出版社ページ / Amazon.co.jpの商品ページ

 大型本: 384ページ、価格:3,045円(税込)、ISBN: 978-4797370904