クラスタリングを使用してNode.jsアプリケーションをスケーリングする方法
著者は、Write for DOnationsプログラムの一環として、女性技術者の会に寄付を受け取るよう選びました。
紹介
複数のCPUを持つシステムでNode.jsプログラムを実行すると、デフォルトでは単一のCPUを使用して実行するプロセスが作成されます。Node.jsはJavaScriptコードを実行するために単一のスレッドを使用するため、アプリケーションへのすべてのリクエストは、その単一のCPUで動作しているスレッドによって処理されなければなりません。アプリケーションにCPU集約タスクがある場合、オペレーティングシステムはそれらを完了するまで単一のCPUを共有するようにスケジュールする必要があります。リクエストが多すぎると、単一のプロセスが圧倒されることがあり、それがパフォーマンスの低下を引き起こす可能性があります。プロセスがクラッシュすると、ユーザーはあなたのアプリケーションにアクセスできなくなります。
解決策として、Node.jsはclusterモジュールを導入しました。このモジュールは、同じマシン上で同じアプリケーションの複数のコピーを作成し、同時に実行します。また、ラウンドロビンアルゴリズムを使用してプロセス間で負荷を均等に分配するロードバランサーも備えています。単一のインスタンスがクラッシュした場合でも、ユーザーはまだ実行中の残りのプロセスによってサービスを受けることができます。このように負荷が複数のプロセス間で均等に分散されるため、アプリケーションのパフォーマンスは大幅に向上し、単一のインスタンスが圧倒されることを防ぎます。
このチュートリアルでは、4つ以上のCPUを持つマシン上で、clusterモジュールを使用してNode.jsアプリケーションをスケールさせます。まず、クラスター化を使用しないアプリケーションを作成し、その後、アプリをクラスター化を使用するように変更します。また、pm2モジュールを使用して、複数のCPUにわたってアプリケーションをスケールさせます。クラスター化を使用したアプリと使用していないアプリの性能を比較するために、負荷テストツールを使用し、pm2モジュールを評価します。
前提条件
このチュートリアルを続けるには、以下が必要です:
4つ以上のCPUを持つシステム。
- Ubuntu 22.04を使用しているリモートサーバーを利用している場合は、初期サーバー設定に従ってシステムを設定できます。
ステップ 1 - プロジェクトディレクトリの設定
このステップでは、プロジェクトのディレクトリを作成し、このチュートリアルの後の段階で構築するアプリケーションの依存関係をダウンロードします。ステップ2では、Expressを使用してアプリケーションを構築します。その後、ステップ3で組み込みのnode-clusterモジュールを使用して複数のCPUにスケールします。この結果は、ステップ4でloadtestパッケージを使用して測定します。そこから、pm2パッケージを使って再度スケールし、ステップ5で再度測定します。
始めるには、ディレクトリを作成してください。これを cluster_demo などお好みの名前で呼んでも構いません:
- mkdir cluster_demo
次に、ディレクトリに移動します:
- cd cluster_demo
次に、プロジェクトを初期化します。これにより、package.jsonファイルも作成されます。
- npm init -y
-yオプションは、NPMにすべてのデフォルトオプションを受け入れるように指示します。
コマンドが実行されると、以下に一致する出力が得られます:
Wrote to /home/sammy/cluster_demo/package.json:
{
"name": "cluster_demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
あなたの特定のプロジェクトに沿ったこれらの特性に注意してください:
name: npmパッケージの名前。バージョン: あなたのパッケージのバージョン番号。main: プロジェクトのエントリーポイント。
他のプロパティについてもっと学ぶには、NPMのドキュメントのpackage.jsonセクションを確認できます。
次に、好みのエディタ(このチュートリアルでは nano を使用します)で package.json ファイルを開きます:
- nano package.json
package.jsonファイルに、パッケージをインポートする際にESモジュールのサポートを有効にするために、強調表示されたテキストを追加してください:
{
...
"author": "",
"license": "ISC",
"type": "module"
}
ファイルを保存して閉じるにはCTRL+Xを押してください。
次に、次のパッケージをダウンロードします:
express:Node.jsでウェブアプリケーションを構築するためのフレームワークです。loadtest: アプリのパフォーマンスを測定するためにトラフィックを生成するのに役立つ負荷テストツールです。pm2: アプリを複数のCPUに自動的にスケールするツール。
次のコマンドを実行してExpressパッケージをダウンロードします:
- npm install express
次に、loadtest と pm2 パッケージをグローバルにダウンロードするためのコマンドを実行してください:
- npm install -g loadtest pm2
必要な依存関係をインストールしたので、クラスタリングを使用しないアプリケーションを作成します。
ステップ2 — クラスターを使用せずにアプリケーションを作成する
このステップでは、ユーザーの訪問ごとにCPU集中的なタスクを開始する単一のルートを含むサンプルプログラムを作成します。このプログラムでは、単一のCPUでアプリのインスタンスを実行する際のパフォーマンスへの影響を理解できるように、cluster モジュールは使用しません。このアプローチを、チュートリアルの後半でcluster モジュールのパフォーマンスと比較します。
nanoやお気に入りのテキストエディタを使用して、index.jsファイルを作成します:
- nano index.js
index.jsファイルに、Expressをインポートしインスタンス化するために以下の行を追加してください:
import express from "express";
const port = 3000;
const app = express();
console.log(`worker pid=${process.pid}`);
最初の行では、expressパッケージをインポートします。2行目では、アプリケーションのサーバーがリッスンする3000ポートにport変数を設定します。次に、app変数にExpressのインスタンスを設定します。その後、ビルトインのprocessモジュールを使用して、アプリケーションのプロセスのプロセスIDをコンソールにログ出力します。
次に、CPUバウンドループを含むルート/heavyを定義するために、これらの行を追加します。
...
app.get("/heavy", (req, res) => {
let total = 0;
for (let i = 0; i < 5_000_000; i++) {
total++;
}
res.send(`The result of the CPU intensive task is ${total}\n`);
});
/heavy ルートでは、total 変数を500万回増加させるループを定義します。そして、res.send() メソッドを使用して、total 変数の値を含むレスポンスを送信します。CPUに負荷がかかるタスクの例は任意ですが、複雑さを加えずにCPUに負荷がかかるタスクを示しています。他の名前をルートに使用することもできますが、このチュートリアルでは重いパフォーマンスタスクを示すために /heavy を使用しています。
次に、Expressモジュールのlisten()メソッドを呼び出して、port変数に格納されたポート3000でサーバーがリッスンするようにします:
...
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
完全なファイルは次のように一致します:
import express from "express";
const port = 3000;
const app = express();
console.log(`worker pid=${process.pid}`);
app.get("/heavy", (req, res) => {
let total = 0;
for (let i = 0; i < 5_000_000; i++) {
total++;
}
res.send(`The result of the CPU intensive task is ${total}\n`);
});
app.listen(port, () => {
console.log(`App listening on port ${port}`);
});
コードの追加が終わったら、ファイルを保存して終了します。その後、nodeコマンドを使ってファイルを実行します:
- node index.js
コマンドを実行すると、出力は以下のようになります:
worker pid=11023
App listening on port 3000
出力は、実行中のプロセスのプロセスIDと、サーバーがポート3000でリッスンしていることを確認するメッセージを示します。
アプリケーションが動作しているかテストするために、別のターミナルを開き、次のコマンドを実行してください:
- curl http://localhost:3000/heavy
注意:このチュートリアルをリモートサーバー上で実行している場合は、別のターミナルを開き、次のコマンドを入力してください:
- ssh -L 3000:localhost:3000 your_non_root_user@your_server_ip
接続後、curlを使用してアプリにリクエストを送信するには、次のコマンドを入力します:
- curl http://localhost:3000/heavy
出力は次のようになります:
The result of the CPU intensive task is 5000000
出力はCPU集約型計算の結果を提供します。
この時点で、CTRL+Cを使用してサーバーを停止できます。
index.jsファイルをnodeコマンドで実行すると、オペレーティングシステム(OS)がプロセスを作成します。プロセスは、オペレーティングシステムが実行中のプログラムのために作成する抽象です。OSはプログラムのためにメモリを割り当て、すべてのOSプロセスを含むプロセスリストにエントリを作成します。そのエントリはプロセスIDです。
プログラムのバイナリはその後、プロセスに割り当てられたメモリに配置され、ロードされます。そこから実行を開始します。実行中は、システム内の他のプロセスについての認識はなく、プロセス内で発生することは他のプロセスには影響しません。
複数のCPUを持つサーバー上で実行されているNode.jsアプリケーションのために単一のプロセスがあるため、すべての受信リクエストを受け取り処理します。この図では、すべての受信リクエストが単一のCPUで実行されているプロセスに向けられ、他のCPUはアイドル状態のままです。
アプリを cluster モジュールを使用せずに作成したので、次に cluster モジュールを使用してアプリケーションをスケールアップし、複数の CPU を利用することになります。
ステップ3 — アプリケーションのクラスタリング
このステップでは、clusterモジュールを追加して、同じプログラムの複数のインスタンスを作成し、より多くの負荷を処理し、パフォーマンスを向上させます。clusterモジュールを使用してプロセスを実行すると、マシンの各CPU上で複数のプロセスを実行することができます:
この図では、リクエストはプライマリプロセスのロードバランサーを経由し、その後ラウンドロビンアルゴリズムを使用してプロセス間でリクエストを分配します。
これからclusterモジュールを追加します。ターミナルでprimary.jsファイルを作成してください:
- nano primary.js
あなたのprimary.jsファイルに、依存関係をインポートするために次の行を追加してください。
import cluster from "cluster";
import os from "os";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
最初の二行では、clusterおよびosモジュールをインポートします。その後の二行では、dirnameとfileURLToPathをインポートし、これを使用して__dirname変数の値をindex.jsファイルが実行されているディレクトリの絶対パスに設定します。これらのインポートは、ESモジュールを使用しているときに__dirnameが定義されず、CommonJSモジュールではデフォルトでのみ定義されるため、必要です。
次に、index.jsファイルを参照するために次のコードを追加します:
...
const cpuCount = os.cpus().length;
console.log(`The total number of CPUs is ${cpuCount}`);
console.log(`Primary pid=${process.pid}`);
cluster.setupPrimary({
exec: __dirname + "/index.js",
});
まず、cpuCount変数を、あなたのマシンにあるCPUの数に設定しますが、これは4以上であるべきです。次に、コンソールにCPUの数をログします。その後、すべてのリクエストを受け取る主要プロセスのプロセスIDをログし、負荷分散装置を使用してワーカープロセス間でそれらを分配します。
その後、clusterモジュールのsetupPrimary()メソッドを使用して、index.jsファイルを参照し、生成された各ワーカープロセスで実行されるようにします。
次に、プロセスを作成するために以下のコードを追加します:
...
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`worker ${worker.process.pid} has been killed`);
console.log("Starting another worker");
cluster.fork();
});
ループは、cpuCountの値と同じ回数だけ繰り返し、各イテレーションでclusterモジュールのfork()メソッドを呼び出します。プロセスがexitイベントを発生させたとき、通常はプロセスが終了したときに、clusterモジュールのon()メソッドを使用してexitイベントをリッスンします。exitイベントがトリガーされると、終了したワーカーのプロセスIDをログに記録し、その後、死んだプロセスを置き換えるために新しいワーカープロセスを作成するためにfork()メソッドを呼び出します。
あなたの完全なコードは、次の内容に一致します:
import cluster from "cluster";
import os from "os";
import { dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const cpuCount = os.cpus().length;
console.log(`The total number of CPUs is ${cpuCount}`);
console.log(`Primary pid=${process.pid}`);
cluster.setupPrimary({
exec: __dirname + "/index.js",
});
for (let i = 0; i < cpuCount; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`worker ${worker.process.pid} has been killed`);
console.log("Starting another worker");
cluster.fork();
});
行を追加し終えたら、ファイルを保存して終了してください。
次に、ファイルを実行します:
- node primary.js
出力は以下の内容に密接に一致します(プロセスIDおよび情報の順序は異なる場合があります):
The total number of CPUs is 4
Primary pid=7341
worker pid=7353
worker pid=7354
worker pid=7360
App listening on port 3000
App listening on port 3000
App listening on port 3000
worker pid=7352
App listening on port 3000
出力は、4つのCPU、ロードバランサーを含む1つの主要プロセス、およびポート3000でリッスンしている4つのワーカープロセスを示します。
次に、2番目の端末に戻り、/heavy ルートにリクエストを送信します:
- curl http://localhost:3000/heavy
出力はプログラムが動作していることを確認します:
The result of the CPU intensive task is 5000000
サーバーを今停止しても構いません。
この時点で、あなたのマシンのすべてのCPUで4つのプロセスが実行されていることになります:
アプリケーションにクラスタリングが追加されることで、clusterモジュールを使用したプログラムのパフォーマンスと、clusterモジュールを使用しないプログラムのパフォーマンスを比較できます。
ステップ4 — 負荷テストツールを使用したパフォーマンスの比較
このステップでは、loadtest パッケージを使用して、あなたが作成した二つのプログラムに対してトラフィックを生成します。cluster モジュールを使用する primary.js プログラムと、クラスターを使用しない index.js プログラムのパフォーマンスを比較します。cluster モジュールを使用するプログラムの方が、クラスターを使用しないプログラムよりも高速で、特定の時間内により多くのリクエストを処理できることに気付くでしょう。
最初に、cluster モジュールを使用せず、単一のインスタンスでのみ実行される index.js ファイルのパフォーマンスを測定します。
最初のターミナルで、サーバーを起動するためにindex.jsファイルを実行します:
- node index.js
アプリが実行中であるという出力が表示されます:
worker pid=7731
App listening on port 3000
次に、2番目のターミナルに戻り、loadtestパッケージを使用してサーバーにリクエストを送信します:
- loadtest -n 1200 -c 200 -k http://localhost:3000/heavy
コマンドの -n オプションは、パッケージが送信するリクエストの数を受け付けます。ここでは 1200 リクエストです。 -c オプションは、サーバーに同時に送信するリクエストの数を受け付けます。
リクエストが送信されると、パッケージは次のような出力を返します:
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Requests: 430 (36%), requests per second: 87, mean latency: 1815.1 ms
Requests: 879 (73%), requests per second: 90, mean latency: 2230.5 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 13.712728601 s
Requests per second: 88
Mean latency: 2085.1 ms
Percentage of the requests served within a certain time
50% 2234 ms
90% 2340 ms
95% 2385 ms
99% 2406 ms
100% 2413 ms (longest request)
この出力から、以下の指標に注意してください:
合計時間は、すべてのリクエストが処理されるのにかかる時間を測定します。この出力では、すべての1200リクエストを処理するのにわずか13秒以上かかりました。1秒あたりのリクエスト数は、サーバーが1秒間に処理できるリクエストの数を測定します。この出力では、サーバーは88リクエストを1秒あたり処理しています。平均レイテンシは、リクエストを送信して応答を受け取るのにかかる時間を測定したもので、サンプル出力では2085.1 msです。
これらのメトリックは、ネットワークやプロセッサの速度によって異なることがありますが、これらの例に近いものになるでしょう。
index.jsファイルのパフォーマンスを測定したので、サーバーを停止できます。
次に、cluster モジュールを使用する primary.js ファイルのパフォーマンスを測定します。
そのためには、最初のターミナルに戻り、primary.jsファイルを再実行してください:
- node primary.js
以前と同じ情報を含む返信を受け取ることになります:
The total number of CPUs is 4
Primary pid=7841
worker pid=7852
App listening on port 3000
worker pid=7854
App listening on port 3000
worker pid=7853
worker pid=7860
App listening on port 3000
App listening on port 3000
2番目のターミナルで、loadtestコマンドを再度実行します:
- loadtest -n 1200 -c 200 -k http://localhost:3000/heavy
終了すると、類似の出力を受け取ります(システムのCPU数によって異なる場合があります):
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 3.412741962 s
Requests per second: 352
Mean latency: 514.2 ms
Percentage of the requests served within a certain time
50% 194 ms
90% 2521 ms
95% 2699 ms
99% 2710 ms
100% 2759 ms (longest request)
cluster モジュールを使用している primary.js アプリの出力は、クラスタリングを使用しないプログラムの 13 秒から 3 秒に総時間が短縮されたことを示しています。サーバーが処理できるリクエスト数は、以前の 88 から 352 に3倍になり、サーバーは大きな負荷を受けることができるようになりました。もう一つの重要な指標は平均レイテンシで、2085.1 ms から 514.2 ms に大幅に減少しました。
この応答は、スケーリングが機能し、アプリケーションが短時間に遅延なくより多くのリクエストを処理できることを確認します。もしマシンをアップグレードしてCPUを増やすと、アプリは自動的にCPUの数にスケールし、さらにパフォーマンスが向上します。
念のためお知らせしますが、ターミナルの出力に表示される指標は、ネットワークとプロセッサの速度によって異なります。総時間と平均レイテンシは大幅に低下し、総時間は急速に増加します。
比較を行い、アプリがclusterモジュールを使用した方がパフォーマンスが良いことに気付いたので、サーバーを停止できます。次のステップでは、clusterモジュールの代わりにpm2を使用します。
ステップ5 — クラスタリングのためのpm2の使用
これまで、あなたはclusterモジュールを使用して、マシンのCPUの数に応じてワーカープロセスを作成してきました。また、ワーカープロセスが死んだときに再起動する機能も追加しました。このステップでは、pm2プロセスマネージャを使用してアプリのスケーリングを自動化する代替策を設定します。これはclusterモジュールに基づいて構築されています。このプロセスマネージャにはロードバランサーが含まれており、マシンのCPU数と同じだけのワーカープロセスを自動的に作成することができます。また、プロセスを監視することができ、1つのプロセスが死んだ場合に自動的に新しいワーカープロセスを生成することができます。
これを使用するには、スケールする必要があるファイル、つまりこのチュートリアルの index.js ファイルを指定して pm2 パッケージを実行する必要があります。
初期ターミナルで、次のコマンドを使用してpm2クラスタを起動してください。
- pm2 start index.js -i 0
-i オプションは、pm2 が作成するワーカープロセスの数を受け入れます。引数として 0 を渡すと、pm2 は自動的にマシンのCPUの数と同じだけのワーカープロセスを作成します。
コマンドを実行すると、pm2はワーカープロセスについての詳細を表示します:
...
[PM2] Spawning PM2 daemon with pm2_home=/home/sammy/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/sammy/cluster_demo/index.js in cluster_mode (0 instance)
[PM2] Done.
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ index │ default │ 1.0.0 │ cluster │ 7932 │ 0s │ 0 │ online │ 0% │ 54.5mb │ nod… │ disabled │
│ 1 │ index │ default │ 1.0.0 │ cluster │ 7939 │ 0s │ 0 │ online │ 0% │ 50.9mb │ nod… │ disabled │
│ 2 │ index │ default │ 1.0.0 │ cluster │ 7946 │ 0s │ 0 │ online │ 0% │ 51.3mb │ nod… │ disabled │
│ 3 │ index │ default │ 1.0.0 │ cluster │ 7953 │ 0s │ 0 │ online │ 0% │ 47.4mb │ nod… │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
そのテーブルには各作業者のプロセスID、状態、CPU使用率、およびメモリ消費量が含まれており、これを使用してプロセスの動作を理解することができます。
pm2でクラスターを起動すると、パッケージはバックグラウンドで実行され、システムを再起動しても自動的に再起動します。
ワーカー プロセスのログを読みたい場合は、次のコマンドを使用できます:
- pm2 logs
ログの出力が得られます:
[TAILING] Tailing last 15 lines for [all] processes (change the value with --lines option)
/home/sammy/.pm2/pm2.log last 15 lines:
...
PM2 | 2022-12-25T17:48:37: PM2 log: App [index:3] starting in -cluster mode-
PM2 | 2022-12-25T17:48:37: PM2 log: App [index:3] online
/home/sammy/.pm2/logs/index-error.log last 15 lines:
/home/sammy/.pm2/logs/index-out.log last 15 lines:
0|index | worker pid=7932
0|index | App listening on port 3000
0|index | worker pid=7953
0|index | App listening on port 3000
0|index | worker pid=7946
0|index | worker pid=7939
0|index | App listening on port 3000
0|index | App listening on port 3000
最後の8行では、ログに各4つの稼働中のワーカープロセスからの出力が記載されており、プロセスIDとポート番号3000が含まれています。この出力は、すべてのプロセスが稼働していることを確認しています。
次のコマンドを使用してプロセスの状態を確認することもできます:
- pm2 ls
出力は以下の表と一致します:
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ index │ default │ 1.0.0 │ cluster │ 7932 │ 5m │ 0 │ online │ 0% │ 56.6mb │ nod… │ disabled │
│ 1 │ index │ default │ 1.0.0 │ cluster │ 7939 │ 5m │ 0 │ online │ 0% │ 55.7mb │ nod… │ disabled │
│ 2 │ index │ default │ 1.0.0 │ cluster │ 7946 │ 5m │ 0 │ online │ 0% │ 56.5mb │ nod… │ disabled │
│ 3 │ index │ default │ 1.0.0 │ cluster │ 7953 │ 5m │ 0 │ online │ 0% │ 55.9mb │ nod… │ disabled │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
クラスタが稼働しているので、パフォーマンスをテストするために、同じターミナルで次のコマンドを入力してください:
- loadtest -n 1200 -c 200 -k http://localhost:3000/heavy
出力は以下の内容に密接に一致します:
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:3000/heavy
Max requests: 1200
Concurrency level: 200
Agent: keepalive
Completed requests: 1200
Total errors: 0
Total time: 3.771868785 s
Requests per second: 318
Mean latency: 574.4 ms
Percentage of the requests served within a certain time
50% 216 ms
90% 2859 ms
95% 3016 ms
99% 3028 ms
100% 3084 ms (longest request)
コードの「総時間」、「秒間リクエスト数」、および「平均レイテンシ」は、あなたが「クラスタ」モジュールを使用しているときに生成されたメトリクスに近いです。この整合性は、「pm2」のスケーリングが同様に機能することを示しています。
pm2を使用して作業フローを改善するために、アプリケーションの設定を渡すための設定ファイルを生成することができます。この方法を使用すれば、オプションを渡さずにクラスターを起動または再起動することができます。
設定ファイルを使用するには、現在のクラスタを削除してください。
- pm2 delete index.js
消えたという返答が届きます:
[PM2] Applying action deleteProcessId on app [index.js](ids: [ 0, 1, 2, 3 ])
[PM2] [index](2) ✓
[PM2] [index](1) ✓
[PM2] [index](0) ✓
[PM2] [index](3) ✓
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
次に、設定ファイルを生成します。
- pm2 ecosystem
出力はファイルが生成されたことを確認します:
File /home/sammy/cluster_demo/ecosystem.config.js generated
.jsを.cjsに名前を変更して、ESモジュールのサポートを有効にします:
- mv ecosystem.config.js ecosystem.config.cjs
エディターを使用して、設定ファイルを開いてください:
- nano ecosystem.config.cjs
あなたのecosystem.config.cjsファイルに、以下の強調表示されたコードを追加してください。
module.exports = {
apps : [{
script: 'index.js',
watch: '.',
name: "cluster_app",
instances: 0,
exec_mode: "cluster",
}, {
script: './service-worker/',
watch: ['./service-worker']
}],
deploy : {
production : {
user : 'SSH_USERNAME',
host : 'SSH_HOSTMACHINE',
ref : 'origin/master',
repo : 'GIT_REPOSITORY',
path : 'DESTINATION_PATH',
'pre-deploy-local': '',
'post-deploy' : 'npm install && pm2 reload ecosystem.config.cjs --env production',
'pre-setup': ''
}
}
};
このscriptオプションは、pm2パッケージが生成する各プロセスで実行されるファイルを受け入れます。nameプロパティは、クラスタを識別できる任意の名前を受け入れ、停止、再起動、またはその他のアクションを行う必要がある場合に役立ちます。instancesプロパティは、必要なインスタンス数を受け入れます。instancesを0に設定すると、pm2はCPUの数だけプロセスを生成します。exec_modeは、clusterオプションを受け入れ、pm2にクラスタで実行するよう指示します。
終わったら、ファイルを保存して閉じてください。
クラスタを始めるには、以下のコマンドを実行してください:
- pm2 start ecosystem.config.cjs
次のような回答を受け取ります:
[PM2][WARN] Applications cluster_app, service-worker not running, starting...
[PM2][ERROR] Error: Script not found: /home/node-user/cluster_demo/service-worker
[PM2] App [cluster_app] launched (4 instances)
最後の行は、4つのプロセスが実行されていることを確認しています。このチュートリアルではservice-workerスクリプトを作成していないため、service-workerが見つからないというエラーは無視しても構いません。
クラスターが動作していることを確認するために、ステータスをチェックしてください:
- pm2 ls
4つのプロセスが実行中であることを確認する応答が届きます:
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 0 │ cluster_app │ cluster │ 0 │ online │ 0% │ 56.9mb │
│ 1 │ cluster_app │ cluster │ 0 │ online │ 0% │ 57.6mb │
│ 2 │ cluster_app │ cluster │ 0 │ online │ 0% │ 55.9mb │
│ 3 │ cluster_app │ cluster │ 0 │ online │ 0% │ 55.9mb │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
クラスターを再起動したい場合は、ecosystem.config.cjsファイルで定義したアプリ名を使用できます。この場合はcluster_appです:
- pm2 restart cluster_app
クラスタが再起動します:
Use --update-env to update environment variables
[PM2] Applying action restartProcessId on app [cluster_app](ids: [ 0, 1, 2, 3 ])
[PM2] [cluster_app](0) ✓
[PM2] [cluster_app](1) ✓
[PM2] [cluster_app](2) ✓
[PM2] [cluster_app](3) ✓
┌────┬────────────────────┬──────────┬──────┬───────────┬──────────┬──────────┐
│ id │ name │ mode │ ↺ │ status │ cpu │ memory │
├────┼────────────────────┼──────────┼──────┼───────────┼──────────┼──────────┤
│ 0 │ cluster_app │ cluster │ 1 │ online │ 0% │ 48.0mb │
│ 1 │ cluster_app │ cluster │ 1 │ online │ 0% │ 47.9mb │
│ 2 │ cluster_app │ cluster │ 1 │ online │ 0% │ 38.8mb │
│ 3 │ cluster_app │ cluster │ 1 │ online │ 0% │ 31.5mb │
└────┴────────────────────┴──────────┴──────┴───────────┴──────────┴──────────┘
クラスタの管理を続けるには、以下のコマンドを実行できます:
| コマンド | 説明 |
|---|---|
| クラスタを開始します |
| クラスタを停止し、再起動します。 |
| ダウンタイムなしでクラスターを再起動します |
| クラスタを停止します |
| クラスタを削除します |
今、pm2モジュールとclusterモジュールを使用してアプリケーションをスケールすることができます。
結論
このチュートリアルでは、cluster モジュールを使用してアプリケーションをスケーリングしました。まず、cluster モジュールを使用しないプログラムを作成しました。次に、アプリをマシンの複数の CPU にスケールするために cluster モジュールを使用するプログラムを作成しました。その後、cluster モジュールを使用するアプリと使用しないアプリのパフォーマンスを比較しました。最後に、アプリを複数の CPU にスケールするための代替として pm2 パッケージを使用しました。
さらに進むために、clusterモジュールのドキュメントページを訪れて、このモジュールについてもっと学ぶことができます。
もしpm2を使い続けたい場合は、PM2 - プロセスマネジメントのドキュメントを参照してください。また、Ubuntu 22.04でのプロダクション用Node.jsアプリケーションの設定方法について、pm2を使ったチュートリアルもお試しください。
Node.js には、CPU 集中型のタスクをワーカースレッドに分割して迅速に処理できる worker_threads モジュールも含まれています。Node.js でマルチスレッドを使用する方法についてのチュートリアルを試してみてください。また、専用のウェブワーカーを使用してフロントエンドの CPU バウンドタスクを最適化することもできます。これは、「ウェブワーカーを使用して CPU バウンドタスクを処理する方法」というチュートリアルに従うことで実現できます。アプリケーションのリクエスト/レスポンスサイクルに CPU バウンドタスクが影響しないようにする方法を学びたい場合は、「Node.js と BullMQ を使用して非同期タスクを処理する方法」をチェックしてください。


