前書き
PM2 を使って Node.js プロセスを管理していると、クラスターモードをサポートしていることに気づくでしょう。このモードを有効にすると、Node.js で複数のプロセスを作成できます。クラスターモードでインスタンス数を max に設定すると、サーバーの CPU コア数に基づいて同数の Node プロセスが作成されます。
PM2 は、Node.js の Cluster モジュールを利用してこれを実現しています。このモジュールは、Node.js のシングルスレッド特性による制約を解決し、複数の CPU コアを活用できるようにするために導入されました。しかし、Cluster モジュールは内部でどのように機能しているのでしょうか?プロセス間の通信はどのように行われるのでしょうか?複数のプロセスが同じポートをどのようにリッスンするのでしょうか?Node.js はどのようにしてリクエストを各プロセスに分散させるのでしょうか?これらの疑問について詳しく知りたい方は、ぜひ読み進めてください。
基本原理
Node.js のワーカープロセスは child_process.fork()
メソッドを使用して作成されます。これは、1 つの親プロセスと複数の子プロセスが存在することを意味します。典型的なコードは次のようになります:
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
for (let i = 0, n = os.cpus().length; i < n; i++) {
cluster.fork();
}
} else {
// アプリケーションを起動する
}
オペレーティングシステムを学んだことがある方なら、fork()
システムコールに馴染みがあるかもしれません。このコールを行ったプロセスは親プロセスとなり、新しく作成されたプロセスが子プロセスとなります。子プロセスと親プロセスは同じデータセグメントやスタックを共有しますが、物理的なメモリ空間は必ずしも共有しません。Node.js クラスターでは、マスタープロセスがポートをリッスンし、受信したリクエストをワーカープロセスに分配します。この過程では、プロセス間通信 (IPC)、負荷分散戦略、およびマルチプロセスでのポートリッスンという 3 つの重要なトピックが関係します。
プロセス間通信 (IPC)
マスタープロセスは process.fork()
を使用して子プロセスを作成します。これらのプロセス間の通信は、IPC チャネルを通じて行われます。オペレーティングシステムが提供する主なプロセス間通信の手法には、以下のようなものがあります:
-
共有メモリ
複数のプロセスが同じメモリ空間を共有します。同期と排他制御を実現するためにセマフォ機構が導入されることが一般的です。 -
メッセージ伝達
プロセス間でメッセージを送受信することでデータをやり取りします。 -
セマフォ
セマフォはシステムがプロセスに割り当てる状態値で、制御権を持たないプロセスは特定の地点で停止させられ、信号を待機します。セマフォの値が 0 または 1 のみの場合は「ミューテックス (相互排他ロック)」と呼ばれます。 -
パイプ
パイプは 2 つのプロセスを接続し、1 つのプロセスの出力を他のプロセスの入力として渡します。pipe
システムコールで作成できます。シェルスクリプトで使用される|
コマンドは、このメカニズムを利用した例です。
Node.js は、親プロセスと子プロセス間のメッセージをやり取りするためのイベントベースのメカニズムを提供しています。以下は、TCP サーバーハンドルを子プロセスに送信する例です:
const subprocess = require('child_process').fork('subprocess.js');
// サーバーオブジェクトを作成し、そのハンドルを送信
const server = require('net').createServer();
server.on('connection', (socket) => {
socket.end('親プロセスが処理しました');
});
server.listen(1337, () => {
subprocess.send('server', server);
});
process.on('message', (m, server) => {
if (m === 'server') {
server.on('connection', (socket) => {
socket.end('子プロセスが処理しました');
});
}
});
負荷分散戦略
前述の通り、すべてのリクエストはマスタープロセスによって分配されます。サーバー負荷をワーカープロセスに均等に分配するには、負荷分散戦略が重要です。Node.js ではデフォルトで ラウンドロビン (round-robin) アルゴリズムが使用されています。
ラウンドロビン (round-robin)
ラウンドロビンは、Nginx などでも使用される一般的な負荷分散アルゴリズムです。このアルゴリズムは、ユーザーからのリクエストを順番に各プロセスに割り当て、最後のプロセスに到達すると最初のプロセスに戻ります。ただし、このアルゴリズムはすべてのプロセスが同じ処理能力を持つと仮定しています。そのため、リクエスト処理時間が大きく異なる場合、不均衡が発生しやすくなります。
これに対処するため、Nginx では 加重ラウンドロビン (WRR) が使用されることが一般的です。WRR では、各サーバーに異なる重みを設定し、重みが最も大きいサーバーが選ばれます。このサーバーの重みが 0 になるまで処理を割り当てた後、新しい重みの順序に基づいて再び割り当てが行われます。
Node.js の負荷分散戦略は、NODE_CLUSTER_SCHED_POLICY
環境変数を設定するか、cluster.setupMaster(options)
を使用して変更できます。多台構成の負荷分散には Nginx を、単一マシンでの負荷分散には Node.js Cluster を組み合わせるのが一般的です。
マルチプロセスでのポートリッスン
初期の Node.js では、複数のプロセスが同じポートをリッスンすると、新しい接続を受け入れる際に競合が発生し、負荷が不均衡になる問題がありました。この問題は後にラウンドロビン戦略を導入することで解決されました。現在の設計は次のように動作します:
- マスタープロセスがソケットを作成し、アドレスにバインドしてリッスンを開始します。
- このソケットのファイルディスクリプタ (fd) はワーカープロセスには渡されません。
- マスターが新しい接続を受け入れると、接続を処理するワーカープロセスを決定し、接続を転送します。
簡単に言えば、マスタープロセスがポートをリッスンし、接続をワーカープロセスに分配することで、競合による負荷不均衡の問題を解決しています。ただし、この設計ではマスタープロセスの安定性が非常に重要です。
まとめ
PM2 のクラスターモードを切り口に、Node.js クラスターがマルチプロセスを実現する基本原理を紹介しました。特に、プロセス間通信、負荷分散、およびマルチプロセスでのポートリッスンに焦点を当てました。
クラスター モジュールを調べると、多くの基本原理やアルゴリズムが普遍的であることに気づきます。例えば、ラウンドロビン アルゴリズムは、オペレーティングシステムのプロセススケジューリングやサーバーの負荷分散に使用されています。また、マスター-ワーカーのアーキテクチャは、Nginx のマルチプロセス設計に似ています。さらに、セマフォやパイプといった仕組みは、さまざまなプログラミングモデルで広く使用されています。
技術が進歩して新しい概念が次々と登場していますが、その基盤となる原理は変わりません。これらの基本を理解することで、新しい課題に対しても自信を持って対応できるようになるでしょう。
私たちはLeapcell、Node.jsプロジェクトのクラウドデプロイの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ