Node.jsは、イベント駆動型のノンブロッキングI/O(入出力)モデルを使用する、サーバーサイドJavaScriptランタイムです。高速でスケーラブルなウェブアプリケーションの構築に広く使用されています。またコミュニティも大規模で、さまざまなタスクやプロセスを簡素化するモジュールも豊富です。

そんなNode.jsアプリケーションにクラスタリングを実装すると、複数のプロセスで実行できるようになり、パフォーマンスが最適化されます。「クラスタ化」によって、マルチコアシステムの潜在能力を最大限に引き出すことができます。

今回は、Node.jsのクラスタ化、およびクラスタリング実装によるアプリケーションのパフォーマンス改善について包括的にご紹介していきます。

クラスタ化とは

Node.jsアプリケーションは、デフォルトではシングルスレッドで実行されます。これは、Node.jsがマルチコアシステムのすべてのコアを使用することができないことを意味します。

Node.jsは、ノンブロッキングI/O操作と非同期プログラミング技術を活用して、複数のリクエストを同時に処理します。

しかし、計算処理の重いタスクがイベントループをブロックし、アプリケーションが応答しなくなる可能性もあります。これを考慮し、Node.jsには、シングルスレッドであるかにかかわらず、マルチコアシステムの総合的な処理能力を活用するためのクラスタモジュールが標準搭載されています。

複数のプロセスを実行することで、複数の中央処理装置(CPU)コアの処理能力を活かして並列処理を可能にし、応答時間を短縮、そしてスループットを向上します。その結果、Node.jsアプリケーションのパフォーマンスとスケーラビリティが改善できます。

クラスタ化の仕組み

Node.jsのclusterモジュールにより、Node.jsアプリケーションは、アプリケーションの負荷の一部を処理し、同時に実行可能な子プロセスを作成することができます。

clusterモジュールを初期化する際、アプリケーションはマスタープロセスを作成し、子プロセスをワーカープロセスにフォークします。マスタープロセスはロードバランサーとして機能し、ワーカープロセスに負荷を分散します。

clusterモジュールには、受け取る接続を分散する以下2つの方法があります。

  1.  マスタープロセスがポートをリッスンし、接続を受け入れ、各プロセスに過負荷がかからないように負荷を均等に分散する。これはラウンドロビン方式と呼ばれ、Windowsを除くすべてのOSにおけるデフォルトのアプローチ。
  2. マスタープロセスがリッスンソケットを作成し、接続処理が可能なワーカープロセスに送信する。

理論的には、より複雑な2つ目のアプローチの方がパフォーマンスの面で優れていますが、実際の運用環境では接続の分配が不均衡であり、Node.jsの公式ドキュメントによると、全接続の70%が8つのうち2つに集中します。

Node.jsアプリケーションをクラスタ化する方法

ここからは、Node.jsアプリケーションにおけるクラスタ化の効果を検証していきます。例として、イベントループをブロックするために意図的に計算処理の重いタスクを実行するExpressアプリケーションを使用します。

まずは、アプリケーションにクラスタリングを実装せずに実行し、ベンチマークツールを使用してパフォーマンスを記録します。続いて、アプリケーションにクラスタリングを実装し、ベンチマークを繰り返します。最後に両者の結果を比較して、クラスタ化によってアプリケーションのパフォーマンスがいかに向上するかを確認します。

はじめに

以下にご紹介する内容には、Node.jsとExpressの知識が必要になります。まずは、Expressサーバーをセットアップします。

  1. プロジェクトを作成する
    mkdir cluster-tutorial
  2. アプリケーションディレクトリに移動し、以下のコマンドを実行してno-cluster.jscluster.jsの2つのファイルを作成する
    cd cluster-tutorial && touch no-cluster.js && touch cluster.js
  3. プロジェクトでNPMを初期化する
    npm init -y
  4. 以下のコマンドを実行してExpressをインストールする
    npm install express

クラスタリングなしのアプリケーションの作成

no-cluster.jsファイルに以下を貼り付けます。

const express = require("express");
const PORT = 3000;

const app = express();

app.get("/", (req, res) => {
  res.send("サーバーからの応答");
});

app.get("/slow", (req, res) => {
  //タイマーを開始
  console.time("slow");

  // 大規模な乱数配列を生成
  let arr = [];
  for (let i = 0; i < 100000; i++) {
  arr.push(Math.random());
  }

  // 配列に対して重い計算処理を行う
  let sum = 0;
  for (let i = 0; i  {
  console.log(`サーバーが${PORT}ポートをリッスンしています`);
});

上のコードは、3000ポートで動作するExpressサーバーを生成します。このサーバーには/ルートと/slowルートの2つが含まれ、/ルートは「サーバーからの応答」のメッセージとともにクライアントにレスポンスを送信します。

/slowルートでは、イベントループをブロックするために、意図的に重い計算処理を行います。このルートがタイマーを開始し、forのループを使用して配列を10万個の乱数で埋めます。

次に、別のforループを使用して生成された配列の各数を2乗し、それらを加算します。完了すると同時にタイマーが終了し、サーバーが結果を返します。

以下のコマンドを実行してサーバーを起動します。

node no-cluster.js

続いて、localhost:3000/slowにGETリクエストを行います。

この間に/ルートなど、サーバーに他のリクエストを送信しようとすると、/slowルートがイベントループをブロックしているため、レスポンスが遅くなります。

クラスタリングを実装したアプリケーションの作成

clusterモジュールを使用して子プロセスを生成し、負荷の高いタスクの処理中にアプリケーションが応答しなくなり、後続のリクエストがストールしないようにします。

各子プロセスはイベントループを実行し、親プロセスとサーバーポートを共有します。

まずは、cluster.jsファイルにclusterおよびosモジュールをインポートします。clusterモジュールを使用すると、子プロセスを作成して複数のCPUコアに作業負荷を分散させることができます。

osモジュールは、コンピュータのオペレーティングシステムに関する情報を提供します。システムで利用可能なコア数を取得し、システムのコア数よりも多くの子プロセスを作成しないようにするために必要になります。

以下のコードを追加してモジュールをインポートし、システム上のコア数を取得します。

const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;

次に、以下のコードをcluster.jsファイルに追加します。

if (cluster.isMaster) {
  console.log(`マスター${process.pid}が実行中`);
  console.log(`このマシンのコア数は${numCores}です`);

  // ワーカープロセスをフォーク
  for (let i = 0; i  {
  console.log(`ワーカー${worker.process.pid}が終了`);

  // 終了したワーカープロセスを置き換える
  console.log("新しいワーカーを生成");
  cluster.fork();
  });
}

上記コードは、現在のプロセスがマスタープロセスか、ワーカープロセスかを検証します。マスタープロセスの場合、システムのコア数に基づいて子プロセスが生成されます。次にプロセスの終了イベントをリッスンし、新しいプロセスを生成してそれらを置き換えます。

最後に、関連するすべてのExpressアプリケーションのロジックをelseブロック内にまとめます。cluster.jsファイルが以下のようになれば完成です。

//cluster.js
const express = require("express");
const PORT = 3000;
const cluster = require("node:cluster");
const numCores = require("node:os").cpus().length;

if (cluster.isMaster) {
  console.log(`マスター${process.pid}が実行中`);
  console.log(`このマシンのコア数は${numCores}です`);

  // ワーカープロセスをフォーク
  for (let i = 0; i  {
  console.log(`ワーカー${worker.process.pid}が終了`);

  // 終了したワーカープロセスを置き換える
  console.log("新しいワーカーを生成");
  cluster.fork();
  });
} else {
  const app = express();

  app.get("/", (req, res) => {
    res.send("サーバーからの応答");
  });

  app.get("/slow", (req, res) => {
   console.time("slow");
  // 大規模な乱数配列を生成
  let arr = [];
  for (let i = 0; i < 100000; i++) {
  arr.push(Math.random());
    }

   // 配列に対して重い計算処理を行う
   let sum = 0;
  for (let i = 0; i  {
  console.log(`サーバーが${PORT}ポートをリッスンしています`);
  });
}

クラスタリングを実装すると、複数のプロセスがリクエストを処理するようになるため、負荷の高いタスクの処理中にもアプリケーションの応答性を維持することができます。

loadtestを使用してパフォーマンスをベンチマークする方法

Node.jsアプリケーションでクラスタ化の効果を正確に実証するには、npmパッケージのloadtestを使って、クラスタリング前後のアプリケーションのパフォーマンスを比較します。

以下のコマンドを実行し、loadtestをグローバルインストールします。

npm install -g loadtest

loadtestパッケージが、指定したHTTP/WebSockets URLで負荷テストを実行します。

次に、ターミナルインスタンスでno-cluster.jsファイルを起動し、別のターミナルインスタンスを開いて以下の負荷テストを実行します。

loadtest http://localhost:3000/slow -n 100 -c 10

上のコマンドは、100件のリクエストを同時実行数10でクラスタリングを実装していないアプリに送信します。結果は以下のようになります。

クラスタリングを実装していないアプリの負荷テスト結果
クラスタリングを実装していないアプリの負荷テスト結果

クラスタリングを実装していないアプリでリクエストを完了するのには約100秒かかり、最も長いリクエストは完了までに最大12秒かかりました。

なお、結果はシステムによって異なります。

次に、no-cluster.jsファイルの実行を停止し、ターミナルインスタンス上でcluster.jsファイルを起動して、別のターミナルインスタンスで以下の負荷テストを実行します。

loadtest http://localhost:3000/slow -n 100 -c 10

上のコマンドは、100件のリクエストを同時実行数10でクラスタリングを実装したアプリに送信します。この結果は、以下のようになります。

クラスタリングを実装したアプリの負荷テスト結果
クラスタリングを実装したアプリの負荷テスト結果

クラスタ化した場合、リクエストの完了に要した時間は0.13秒(136ミリ秒)。クラスタリングを実装していないアプリの100秒から劇的に短縮されているのがわかります。また、最も長いリクエストの処理にかかった時間はたった41ミリ秒でした。

このように、クラスタリングを実装すると、アプリケーションのパフォーマンスが大幅に向上することがわかります。注意点として、本番環境でクラスタ化を管理するには、PM2のようなプロセスマネージャーが必要になります。

KinstaのアプリケーションホスティングでNode.jsを使用する

Kinstaのアプリケーションホスティングでは、Node.jsアプリケーションを簡単にデプロイすることができます。プラットフォームはGoogle Cloud Platform上に構築され、高トラフィックに対応する複雑なアプリケーションを考慮して設計された信頼性の高いインフラストラクチャを提供。これにより、Node.jsアプリケーションのパフォーマンスを最適化することができます。

データベースの内部接続、Cloudflare統合、GitHubを使用したデプロイメント、Google Cloud最速のC2およびC3Dマシンなど、さまざまな機能により、デプロイと管理が簡単になり、開発プロセス全体が合理化されます。

KinstaのアプリケーションホスティングにNode.jsアプリケーションをデプロイするには、任意のGitサービス(BitbucketGitHub、またはGitLab)にアプリケーションのコードとファイルをプッシュします。

リポジトリを設定したら、以下の手順に従ってExpressアプリケーションをデプロイします。

  1. ログインまたはアカウントを作成してMyKinstaを開く
  2. GitサービスでKinstaを認証する
  3. 左サイドバーの「アプリケーション」を選択して「アプリケーションを追加」をクリック
  4. デプロイしたいリポジトリとブランチを選択する
  5. アプリに一意の名前を割り当て、「データセンターの所在地」を選択する
  6. ビルド環境の設定でビルドリソースに「Standardビルドマシン」を選択し、「Nixpacksを使用してコンテナイメージを設定」を選択
  7. すべての設定を終えたら「今すぐデプロイする」をクリックする

まとめ

クラスタ化を行うと、複数のワーカープロセスを生成して作業負荷を分散し、Node.jsアプリケーションのパフォーマンスとスケーラビリティを劇的に向上させることができます。可能性を最大限に引き出すには、適切にクラスタリングを実装することが重要です。

Node.jsでクラスタリングを実装する場合は、アーキテクチャの設計、リソース割り当ての管理、そしてネットワークレイテンシの最小化が極めて重要になります。この実装の重要性と複雑さを考慮し、本番環境にはPM2のようなプロセスマネージャーを使用してください。

Node.jsのクラスタ化を行ったことはありますか?以下のコメント欄にてお聞かせください。

Jeremy Holcombe Kinsta

Kinstaのコンテンツ&マーケティングエディター、WordPress開発者、コンテンツライター。WordPress以外の趣味は、ビーチでのんびりすること、ゴルフ、映画。高身長が特徴。