CLOVER🍀

That was when it all began.

PM2を使って、Node.jsアプリケーションをクラスター化(CPUスケーリング)させてみる

これは、なにをしたくて書いたもの?

Node.jsで動作するアプリケーションは、単一プロセス、単一スレッドで動作するのでホスト側に複数のCPUが
あったとしても、そのままではCPUを有効に活用できません。

PM2などのプロセスマネージャーを使うと、このあたりを楽にできるようなので、試してみることにしました。

Node.jsとマルチコアCPU

Node.jsで動作するアプリケーションは、単一プロセス、単一スレッドで動作します。

そのままだとCPUが複数あっても利用できないのですが、clusterモジュールを利用するとNode.jsアプリケーションでも
マルチプロセスを扱えるようになり、マルチコアCPUを活用できるようになります。

A single instance of Node.js runs in a single thread. To take advantage of multi-core systems, the user will sometimes want to launch a cluster of Node.js processes to handle the load.

Cluster | Node.js v16.13.1 Documentation

clusterモジュールで作成した子プロセスは、サーバーポートを共有できるのでサーバーアプリケーションで便利です。

The cluster module allows easy creation of child processes that all share server ports.

また、Node.js 10.5.0以降であれば、worker_threadsモジュールが使えるので、マルチスレッドも利用できるように
なります。

Worker threads | Node.js v16.13.1 Documentation

こちらは、I/OではなくCPUバウンドな処理に効果的なようです(I/Oメインの場合は、Node.jsの非同期処理の方が
効率的だとされているため)。

プロセスマネージャー

今回は、clusterモジュールではなく(worker_threadsモジュールでもなく)、マルチプロセスを簡単に扱える
ツールが存在します。

Expressのドキュメントでは、StrongLoop PMとPM2が紹介されています。

Production best practices: performance and reliability / Run your app in a cluster

どちらも、アプリケーションコードを修正せずにクラスタリング(マルチプロセス)を利用できます。

If you deploy your application to StrongLoop Process Manager (PM), then you can take advantage of clustering without modifying your application code.

If you deploy your application with PM2, then you can take advantage of clustering without modifying your application code.

この他、プロセス管理という点ではForeverも紹介されています。

Foreverは、PM2やnodemonを使用することを勧めていますが。

これらのツールに対する比較は、StrongLoop PMが行っています。

StrongLoop Process Manager

StrongLoop PMの資料なので、捉え方には注意が要る気はしますが。
ざっとStrongLoop PMとPM2を見比べるとStrongLoop PMの方ができることが多く、以下が目立った差なように
思います。

  • デプロイ時のHTTPのサポート
  • パッケージング、Dockerへのデプロイのサポート
  • 実行中のクラスターのサイズ変更(※)
  • ロードバランサーの自動構成(※)
  • プロファイリングのサポート

※ … 今回書いた範囲で見ていると、このあたりはPM2でもできそうですが…?

PM2はNode.jsアプリケーション以外のものもプロセス管理できるようです。

今回は、PM2を使ってみたいと思います。

PM2

PM2は、daemonプロセスを管理するツールです。

PM2 - Home

ドキュメントを見てもあまりまとまった説明がないのですが、トップページにある機能を見るとこんな感じみたいです。

f:id:Kazuhira:20220101233404p:plain

Quick Startを見ると、およその機能の雰囲気をざっくり掴むことができます。

PM2 - Quick Start

今回は、これらの機能の中のCluster Modeに着目します。

PM2 - Cluster Mode

Cluster Modeを使うと、アプリケーションコードの変更なしに、サーバーアプリケーション(HTTP、HTTPS、TCP、UDP)を
CPUスケーリングさせることができる、としています。

内部的にはNode.jsのclusterモジュールを使用しているようで、子プロセスはサーバーポートの共有が可能なことも
同じだとか。

説明はこのくらいにして、実際に使ってみましょう。

環境

今回の環境は、こちら。

$ node --version
v16.13.1


$ npm --version
8.1.2

プロジェクトを作成する

確認用のプロジェクトを作成しましょう。

$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ mkdir src

アプリケーションは、Expressで作成することにします。

$ npm i express
$ npm i -D @types/node@v16 @types/express

この時点での依存関係。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.17",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "express": "^4.17.2"
  }

各種設定ファイル。

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.json

{
  "singleQuote": true
}

ソースコードは、こんな感じにしました。Expressアプリケーションで、フィボナッチ数を計算して返すものです。

src/app.ts

import express from 'express';

const app = express();

const address = '0.0.0.0';
const port = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

function fib(n: number): number {
  if (n === 0) {
    return 0;
  } else if (n === 1) {
    return 1;
  }

  return fib(n - 1) + fib(n - 2);
}

app.get('/fib', (req, res) => {
  const num = req.query['num'] ? parseInt(req.query['num'] as string, 10) : 0;

  const result = fib(num);

  return res.json({
    num: num,
    result: result,
    pid: process.pid,
  });
});

app.listen(port, address, () => {
  console.log(
    `[${new Date().toISOString()}] server[${address}:${port}] startup.`
  );
});

少し大きめの数を与えて、CPUを専有させることがこの処理の目的です。

動作確認しましょう。

ビルドして

$ npx tsc --project .

起動。

$ node dist/app.js
[2022-01-01T18:01:19.391Z] server[0.0.0.0:3000] startup.

44くらいが時間的にちょうど良さそうでした。10秒ほどかかります。

$ time curl localhost:3000/fib?num=44
{"num":44,"result":701408733,"pid":62052}
real    0m10.250s
user    0m0.015s
sys     0m0.000s

与えた数字、計算結果、そして処理を行ったプロセスのPIDが返ってきます。

この時のCPU使用率を見てみます。

$ mpstat -P ALL 1

この環境ではCPUが8つあるのですが、ひとつ完全に使い切っています。

03時02分24秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時02分25秒  all   15.89    0.00    1.34    0.12    0.00    2.57    0.00    0.00    0.00   80.07
03時02分25秒    0    2.70    0.00    1.80    0.00    0.00   10.81    0.00    0.00    0.00   84.68
03時02分25秒    1    3.77    0.00    0.94    0.94    0.00    6.60    0.00    0.00    0.00   87.74
03時02分25秒    2  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時02分25秒    3    4.04    0.00    1.01    0.00    0.00    0.00    0.00    0.00    0.00   94.95
03時02分25秒    4    6.86    0.00    1.96    0.00    0.00    0.98    0.00    0.00    0.00   90.20
03時02分25秒    5    3.03    0.00    0.00    0.00    0.00    1.01    0.00    0.00    0.00   95.96
03時02分25秒    6    5.88    0.00    3.92    0.00    0.00    0.00    0.00    0.00    0.00   90.20
03時02分25秒    7    3.03    0.00    1.01    0.00    0.00    0.00    0.00    0.00    0.00   95.96

03時02分25秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時02分26秒  all   15.11    0.00    1.57    0.00    0.00    2.90    0.00    0.00    0.00   80.41
03時02分26秒    0    2.54    0.00    0.85    0.00    0.00   16.10    0.00    0.00    0.00   80.51
03時02分26秒    1    1.92    0.00    1.92    0.00    0.00    3.85    0.00    0.00    0.00   92.31
03時02分26秒    2  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時02分26秒    3    3.92    0.00    1.96    0.00    0.00    0.00    0.00    0.00    0.00   94.12
03時02分26秒    4    3.06    0.00    1.02    0.00    0.00    0.00    0.00    0.00    0.00   95.92
03時02分26秒    5    3.92    0.00    1.96    0.00    0.00    0.00    0.00    0.00    0.00   94.12
03時02分26秒    6    4.90    0.00    2.94    0.00    0.00    0.98    0.00    0.00    0.00   91.18
03時02分26秒    7    3.96    0.00    1.98    0.00    0.00    0.00    0.00    0.00    0.00   94.06

次に、ターミナルを2つ開き、curlでリクエストを2つ送ってみます。

$ time curl localhost:3000/fib?num=44
{"num":44,"result":701408733,"pid":62052}
real    0m10.408s
user    0m0.000s
sys     0m0.008s


$ time curl localhost:3000/fib?num=44
{"num":44,"result":701408733,"pid":62052}
real    0m20.344s
user    0m0.004s
sys     0m0.005s

2つ目のリクエストは、処理時間がほぼ2倍になりました。

CPU使用率は、ひとつのCPUを使い切ったまま変わりません。

03時04分24秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時04分25秒  all   17.73    0.00    1.57    3.26    0.00    2.90    0.00    0.00    0.00   74.55
03時04分25秒    0    5.26    0.00    0.88    0.00    0.00   12.28    0.00    0.00    0.00   81.58
03時04分25秒    1    9.35    0.00    0.93   22.43    0.00    6.54    0.00    0.00    0.00   60.75
03時04分25秒    2    7.92    0.00    3.96    0.00    0.00    0.00    0.00    0.00    0.00   88.12
03時04分25秒    3    2.97    0.00    2.97    2.97    0.00    0.00    0.00    0.00    0.00   91.09
03時04分25秒    4    8.74    0.00    1.94    0.00    0.00    1.94    0.00    0.00    0.00   87.38
03時04分25秒    5    5.88    0.00    0.98    0.00    0.00    0.98    0.00    0.00    0.00   92.16
03時04分25秒    6  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時04分25秒    7    4.95    0.00    0.99    0.00    0.00    0.00    0.00    0.00    0.00   94.06

03時04分25秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時04分26秒  all   24.76    0.00    0.85    0.00    0.00    3.38    0.00    0.00    0.00   71.01
03時04分26秒    0   11.57    0.00    0.83    0.00    0.00   18.18    0.00    0.00    0.00   69.42
03時04分26秒    1   14.15    0.00    0.94    0.00    0.00    5.66    0.00    0.00    0.00   79.25
03時04分26秒    2   17.82    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   82.18
03時04分26秒    3   13.86    0.00    0.99    0.00    0.00    0.00    0.00    0.00    0.00   85.15
03時04分26秒    4   13.86    0.00    1.98    0.00    0.00    0.00    0.00    0.00    0.00   84.16
03時04分26秒    5   14.14    0.00    1.01    0.00    0.00    0.00    0.00    0.00    0.00   84.85
03時04分26秒    6  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時04分26秒    7   16.16    0.00    1.01    0.00    0.00    0.00    0.00    0.00    0.00   82.83

つまり、ひとつしかCPUを使えないので、2つ目のリクエストが完全に待っている状態になっています。

PM2をインストールして、マルチプロセス化してみる

では、ここからはPM2を使ってcluster化してみましょう。

まずはインストールから。

PM2 - Quick Start

ドキュメントはnpm install -gでグローバルにインストールしようとするのですが、今回はローカルインストールと
しました。

$ npm i -D pm2

devDependenciesかdependenciesかは悩みましたが、今回はdevDependenciesにしました…。

依存関係は、このようになりました。

  "devDependencies": {
    "@types/express": "^4.17.13",
    "@types/node": "^16.11.17",
    "pm2": "^5.1.2",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "express": "^4.17.2"
  }

バージョン確認。

$ npx pm2 --version

バナーなどが表示されますが、こちらも5.1.2です。

5.1.2

PM2は、コマンドを指定して使うようなのですが、コマンドごとのヘルプを見てもあまり情報がありません。

$ npx pm2 start --help

  Usage: start [options] [name|namespace|file|ecosystem|id...]

  start and daemonize an app

  Options:

    --watch                 Watch folder for changes
    --fresh                 Rebuild Dockerfile
    --daemon                Run container in Daemon mode (debug purposes)
    --container             Start application in container mode
    --dist                  with --container; change local Dockerfile to containerize all files in current directory
    --image-name [name]     with --dist; set the exported image name
    --node-version [major]  with --container, set a specific major Node.js version
    --dockerdaemon          for debugging purpose
    -h, --help              output usage information

オプション全体を見たい場合は、pm2 --helpで見るのが良さそうです。

$ npx pm2 --help

  Usage: pm2 [cmd] app

  Options:

    -V, --version                                                output the version number
    -v --version                                                 print pm2 version
    -s --silent                                                  hide all messages
    --ext <extensions>                                           watch only this file extensions

    〜省略〜

    --v8                                                         enable v8 data collecting
    --event-loop-inspector                                       enable event-loop-inspector dump in pmx
    --deep-monitoring                                            enable all monitoring tools (equivalent to --v8 --event-loop-inspector --trace)
    -h, --help                                                   output usage information

  Commands:

    start [options] [name|namespace|file|ecosystem|id...]        start and daemonize an app
    trigger <id|proc_name|namespace|all> <action_name> [params]  trigger process action
    deploy <file|environment>                                    deploy your json

    〜省略〜

    serve|expose [options] [path] [port]                         serve a directory over http via port
    autoinstall
    examples                                                     display pm2 usage examples
    *

では、アプリケーションを起動してみます。

$ npx pm2 start dist/app.js
[PM2] Starting /path/to/dist/app.js in fork_mode (1 instance)
[PM2] Done.
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ app    │ default     │ 1.0.0   │ fork    │ 63874    │ 0s     │ 0    │ online    │ 0%       │ 30.0mb   │ xxxxx │ disabled │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

1 instanceと表示されているので、プロセスはひとつなのでしょうね。nameを見ると、スクリプトの拡張子を除いた
部分がアプリケーション名として認識されている気がします。

このtable表示は、npx pm2 statusやnpx pm2 lsで確認できます。

1度停止します。

$ npx pm2 stop app                                                                                             
[PM2] Applying action stopProcessId on app [app](ids: [ 0 ])                                                                                                                 
[PM2] [app](0) ✓                                                                                                                                                             
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐                                        
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │                                        
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤                                        
│ 0   │ app    │ default     │ 1.0.0   │ fork    │ 0        │ 0      │ 0    │ stopped   │ 0%       │ 0b       │ xxxxx │ disabled │                                        
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

削除。

$ npx pm2 delete app                                                                                           
[PM2] Applying action deleteProcessId on app [app](ids: [ 0 ])                                                                                                               
[PM2] [app](0) ✓                                                                                                                                                             
┌─────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐                                     
│ id  │ name      │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │                                     
└─────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

ドキュメントを見ながら、プロセス数を増やしてみます。

PM2 - Cluster Mode

プロセス数を指定するには、-iまたは--instancesオプションを使用するようです。正しくは、プロセス数を指定する
というよりCluster Modeを有効にするオプションのようですが。

maxを指定すると、使用できるCPUの数だけプロセスを起動します。

$ npx pm2 start -i max dist/app.js


## または
$ npx pm2 start --instances max dist/app.js

指定できる値と意味は、それぞれ以下になります。

  • 0またはmax … 使用可能なCPUの数だけプロセスを起動する
  • -1 … 使用可能なCPU - 1だけプロセスを起動する
  • 数値 … 指定した数だけプロセスを起動する

まずは2つ割り当ててみましょう。

$ npx pm2 start -i 2 dist/app.js
[PM2] Starting /path/to/dist/app.js in cluster_mode (2 instances)
[PM2] Done.
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ app    │ default     │ 1.0.0   │ cluster │ 68864    │ 0s     │ 0    │ online    │ 0%       │ 37.7mb   │ xxxxx │ disabled │
│ 1   │ app    │ default     │ 1.0.0   │ cluster │ 68871    │ 0s     │ 0    │ online    │ 0%       │ 31.7mb   │ xxxxx │ disabled │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

modeがforkからclusterになりました。これは、負荷分散できるモードであることを意味しているようです。

この状態で、2つリクエストを投げてみます。

$ time curl localhost:3000/fib?num=44
{"num":44,"result":701408733,"pid":68864}
real    0m10.784s
user    0m0.007s
sys     0m0.004s


$ time curl localhost:3000/fib?num=44
{"num":44,"result":701408733,"pid":68871}
real    0m10.805s
user    0m0.000s
sys     0m0.008s

両方とも10秒ほどになり、PIDも別々になりました。

CPU使用率を見ると、2つCPUを使っていることが確認できます。

03時34分19秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時34分20秒  all   28.47    0.00    0.85    0.00    0.00    3.16    0.00    0.00    0.00   67.52
03時34分20秒    0    6.14    0.00    1.75    0.00    0.00   12.28    0.00    0.00    0.00   79.82
03時34分20秒    1    6.14    0.00    1.75    0.00    0.00   10.53    0.00    0.00    0.00   81.58
03時34分20秒    2  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時34分20秒    3    6.06    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   93.94
03時34分20秒    4    4.12    0.00    2.06    0.00    0.00    0.00    0.00    0.00    0.00   93.81
03時34分20秒    5    5.00    0.00    1.00    0.00    0.00    0.00    0.00    0.00    0.00   94.00
03時34分20秒    6    5.10    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   94.90
03時34分20秒    7  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00

03時34分20秒  CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
03時34分21秒  all   28.57    0.00    0.85    0.00    0.00    3.51    0.00    0.00    0.00   67.07
03時34分21秒    0    6.78    0.00    0.85    0.00    0.00   15.25    0.00    0.00    0.00   77.12
03時34分21秒    1    2.78    0.00    0.93    0.00    0.00   10.19    0.00    0.00    0.00   86.11
03時34分21秒    2  100.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00
03時34分21秒    3    6.06    0.00    2.02    0.00    0.00    0.00    0.00    0.00    0.00   91.92
03時34分21秒    4    6.06    0.00    1.01    0.00    0.00    0.00    0.00    0.00    0.00   92.93
03時34分21秒    5    5.94    0.00    0.99    0.00    0.00    0.00    0.00    0.00    0.00   93.07
03時34分21秒    6    7.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00    0.00   93.00
03時34分21秒    7   99.01    0.00    0.99    0.00    0.00    0.00    0.00    0.00    0.00    0.00

もちろん、これ以上リクエスト数を増やすと遅延することになります。使えるように指定したCPUは2つなので、
同時に処理できるのは2つまでです。

実行中にプロセス数を増やす場合は、pm2 scaleオプションでアプリケーション名とインスタンス数を指定します。

$ npx pm2 scale app 4
[PM2] Scaling up application
[PM2] Scaling up application
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ app    │ default     │ 1.0.0   │ cluster │ 68864    │ 106s   │ 0    │ online    │ 0%       │ 48.7mb   │ xxxx │ disabled │
│ 1   │ app    │ default     │ 1.0.0   │ cluster │ 68871    │ 106s   │ 0    │ online    │ 0%       │ 48.5mb   │ xxxx │ disabled │
│ 2   │ app    │ default     │ 1.0.0   │ cluster │ 69009    │ 0s     │ 0    │ online    │ 0%       │ 37.0mb   │ xxxx │ disabled │
│ 3   │ app    │ default     │ 1.0.0   │ cluster │ 69016    │ 0s     │ 0    │ online    │ 0%       │ 30.8mb   │ xxxx │ disabled │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

小さくすることもできます。

$ npx pm2 scale app 2
[PM2] Applying action deleteProcessId on app [0](ids: [ 0 ])
[PM2] [app](0) ✓
[PM2] Applying action deleteProcessId on app [1](ids: [ 1 ])
[PM2] [app](1) ✓
┌─────┬────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name   │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 2   │ app    │ default     │ 1.0.0   │ cluster │ 69009    │ 47s    │ 0    │ online    │ 0%       │ 46.3mb   │ xxxxx │ disabled │
│ 3   │ app    │ default     │ 1.0.0   │ cluster │ 69016    │ 47s    │ 0    │ online    │ 0%       │ 47.7mb   │ xxxxx │ disabled │
└─────┴────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

確認できたので、アプリケーションを削除。

$ npx pm2 delete app

ちなみに、アプリケーション名については--nameオプションで明示的に指定することができます。

$ npx pm2 start --name myapp dist/app.js
[PM2] Starting /path/to/dist/app.js in fork_mode (1 instance)
[PM2] Done.
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ myapp    │ default     │ 1.0.0   │ fork    │ 67118    │ 0s     │ 0    │ online    │ 0%       │ 18.4mb   │ xxxxx │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

ここまでで、CPUを複数使えるようにNode.jsアプリケーションを構成できたことは確認できました。

PM2の設定ファイルを作成して、アプリケーションを管理する

ここまではコマンドラインオプションで指定してきましたが、PM2では設定ファイルも扱えるようです。

PM2 - Cluster Mode

PM2 - Ecosystem File

こちらも試してみましょう。

pm2 initまたはpm2 ecosystemを使って設定ファイルを作成するみたいです。

$ npx pm2 init --help

  Usage: ecosystem|init [options] [mode]

  generate a process conf file. (mode = null or simple)

  Options:

    -h, --help  output usage information

pm2 ecosystemの方で作ってみましょう。モードはsimpleとします。

$ npx pm2 ecosystem simple
File /path/to/ecosystem.config.js generated

ecosystem.config.jsというファイルが作成されました。

ちなみに、なにも指定しない場合は

$ npx pm2 ecosystem

こんなファイルが作成されます。

ecosystem.config.js

module.exports = {
  apps : [{
    script: 'index.js',
    watch: '.'
  }, {
    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.js --env production',
      'pre-setup': ''
    }
  }
};

今回は、simpleで作成したファイルを編集します。アプリケーション名をexpress-app、プロセス数を4、
モードはclusterを指定。

ecosystem.config.js

module.exports = {
  apps : [{
    name   : "express-app",
    script : "./dist/app.js",
    instances: 4,
    exec_mode: "cluster"
  }]
}

ドキュメントを見ると、modeをclusterに指定しないと負荷分散が行われない、みたいに書いているのですが、
この指定を外してもinstancesを指定しておくとclusterで起動しましたが…?

実行の際は、各コマンドの引数に設定ファイルを指定します。

$ npx pm2 start ecosystem.config.js

今回は、4つのプロセスが起動しました。

[PM2][WARN] Applications express-app not running, starting...
[PM2] App [express-app] launched (4 instances)
┌─────┬────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name           │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ express-app    │ default     │ 1.0.0   │ cluster │ 10133    │ 0s     │ 0    │ online    │ 0%       │ 43.0mb   │ xxxxx │ disabled │
│ 1   │ express-app    │ default     │ 1.0.0   │ cluster │ 10140    │ 0s     │ 0    │ online    │ 0%       │ 39.9mb   │ xxxxx │ disabled │
│ 2   │ express-app    │ default     │ 1.0.0   │ cluster │ 10147    │ 0s     │ 0    │ online    │ 0%       │ 37.9mb   │ xxxxx │ disabled │
│ 3   │ express-app    │ default     │ 1.0.0   │ cluster │ 10154    │ 0s     │ 0    │ online    │ 0%       │ 31.6mb   │ xxxxx │ disabled │
└─────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

アプリケーションの実行結果の確認は、省略します。

ステータス確認。

$ npx pm2 status ecosystem.config.js
┌─────┬────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name           │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ express-app    │ default     │ 1.0.0   │ cluster │ 10133    │ 49s    │ 0    │ online    │ 0%       │ 47.0mb   │ xxxxx │ disabled │
│ 1   │ express-app    │ default     │ 1.0.0   │ cluster │ 10140    │ 49s    │ 0    │ online    │ 0%       │ 47.6mb   │ xxxxx │ disabled │
│ 2   │ express-app    │ default     │ 1.0.0   │ cluster │ 10147    │ 49s    │ 0    │ online    │ 0%       │ 47.3mb   │ xxxxx │ disabled │
│ 3   │ express-app    │ default     │ 1.0.0   │ cluster │ 10154    │ 49s    │ 0    │ online    │ 0%       │ 48.4mb   │ xxxxx │ disabled │
└─────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

削除。

$ npx pm2 delete ecosystem.config.js
[PM2] [express-app](0) ✓
[PM2] [express-app](1) ✓
[PM2] [express-app](2) ✓
[PM2] [express-app](3) ✓
┌─────┬───────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name      │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
└─────┴───────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

設定ファイルのリロードでインスタンス数の変更はできなさそうだったので、1度削除してinstancesをmaxに変更して

ecosystem.config.js

module.exports = {
  apps : [{
    name   : "express-app",
    script : "./dist/app.js",
    instances: "max",
    exec_mode: "cluster"
  }]
}

起動。

$ npx pm2 start ecosystem.config.js
[PM2][WARN] Applications express-app not running, starting...
[PM2] App [express-app] launched (8 instances)
┌─────┬────────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name           │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼────────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ express-app    │ default     │ 1.0.0   │ cluster │ 10678    │ 0s     │ 0    │ online    │ 0%       │ 49.8mb   │ xxxxx │ disabled │
│ 1   │ express-app    │ default     │ 1.0.0   │ cluster │ 10685    │ 0s     │ 0    │ online    │ 0%       │ 50.2mb   │ xxxxx │ disabled │
│ 2   │ express-app    │ default     │ 1.0.0   │ cluster │ 10692    │ 0s     │ 0    │ online    │ 0%       │ 49.3mb   │ xxxxx │ disabled │
│ 3   │ express-app    │ default     │ 1.0.0   │ cluster │ 10699    │ 0s     │ 0    │ online    │ 0%       │ 45.7mb   │ xxxxx │ disabled │
│ 4   │ express-app    │ default     │ 1.0.0   │ cluster │ 10710    │ 0s     │ 0    │ online    │ 0%       │ 43.6mb   │ xxxxx │ disabled │
│ 5   │ express-app    │ default     │ 1.0.0   │ cluster │ 10721    │ 0s     │ 0    │ online    │ 0%       │ 40.7mb   │ xxxxx │ disabled │
│ 6   │ express-app    │ default     │ 1.0.0   │ cluster │ 10732    │ 0s     │ 0    │ online    │ 0%       │ 38.0mb   │ xxxxx │ disabled │
│ 7   │ express-app    │ default     │ 1.0.0   │ cluster │ 10743    │ 0s     │ 0    │ online    │ 0%       │ 31.6mb   │ xxxxx │ disabled │
└─────┴────────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

専有できる環境だったら、maxでいいんでしょうね。

なお、設定ファイルを使って管理している状態でもpm2 scaleでプロセス数を変更できましたが、設定ファイルの内容と
ずれるので微妙…。

まあ、今回確認したいことはだいたいできたので良しとしましょう。

まとめ

Node.jsのプロセスマネージャー、PM2を試してみました。

PM2には他にも多くの機能があるようですし、アプリケーションも複数管理できるようですが、今回はクラスター化に
絞って使ってみました。

他にも少し気になるところはあるので、また見る機会を作りたいと思います。