PM2 是 Node.js 的 app server,就好比 Python 的 Uvicorn 或 Gunicorn,以及 Ruby 的 Passenger,PHP 的 PHP-FPM。

這些 app server 負責調度與監管 app,這裡的 app 就是我們撰寫的專案或程式,app server 通常作為系統服務,當它跑起來之後也會把我們的 app 叫起來,並且視負載的輕重決定要開幾隻 app 程序來消化負載,也負責在 app 出錯時把 app 重啟,app server 在正式環境是影響一款應用能否穩定服務的重要角色,而 PM2 是 Node.js 生態中主流的 app server 之一。

安裝

雖然 PM2 也是 Node.js 套件,但它作為 app server,我們視其與 web server 為類似的角色,通常會全域安裝,而不是安裝在專案內,安裝指令:

$ npm install pm2 -g

安裝完執行看看有沒有成功:

$ pm2

初次執行應該會輸出以下訊息:

__/\\\\\\\\\\\\\____/\\\\____________/\\\\____/\\\\\\\\\_____
 _\/\\\/////////\\\_\/\\\\\\________/\\\\\\__/\\\///////\\\___
  _\/\\\_______\/\\\_\/\\\//\\\____/\\\//\\\_\///______\//\\\__
   _\/\\\\\\\\\\\\\/__\/\\\\///\\\/\\\/_\/\\\___________/\\\/___
    _\/\\\/////////____\/\\\__\///\\\/___\/\\\________/\\\//_____
     _\/\\\_____________\/\\\____\///_____\/\\\_____/\\\//________
      _\/\\\_____________\/\\\_____________\/\\\___/\\\/___________
       _\/\\\_____________\/\\\_____________\/\\\__/\\\\\\\\\\\\\\\_
        _\///______________\///______________\///__\///////////////__

                          Runtime Edition

        PM2 is a Production Process Manager for Node.js applications
                     with a built-in Load Balancer.

                Start and Daemonize any application:
                $ pm2 start app.js

                Load Balance 4 instances of api.js:
                $ pm2 start api.js -i 4

                Monitor in production:
                $ pm2 monitor

                Make pm2 auto-boot at server restart:
                $ pm2 startup

                To go further checkout:
                http://pm2.io/

                        -------------

usage: pm2 [options] <command>

pm2 -h, --help             all available commands and options
pm2 examples               display pm2 usage examples
pm2 <command> -h           help on a specific command

Access pm2 files in ~/.pm2

上面的簡短介紹也差不多把 PM2 的基礎用法講完了,本文完。


事情當然沒有那麼簡單,注意到上面訊息的最末一句「Access pm2 files in ~/.pm2」,就先去看裡面有哪些東西吧。

~/.pm2/ 裡面長這樣:

.pm2/
├── logs/
├── module_conf.json
├── modules/
├── pids/
└── touch

目前裡面大多是空的,比較要留意的只有那 logs/,未來要排查問題時記得要來這裡找紀錄。

上面輸出訊息提示了一些 PM2 CLI 的用法,但在生產環境,我們會希望把配置寫成檔案,畢竟 CLI 參數總是會忘記,還是以配置檔的形式運作比較保險也可以納入版控管理。

配置

在 ~/.pm2/ 目錄下產生基礎的配置文件:

$ cd ~/.pm2
$ pm2 init

產出的檔名為 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': ''
    }
  }
}

內容相當簡單,大多可望文生義,apps 區塊定義了兩個 Node.js app,deploy 區塊則是一些遠端部署用的參數與指令。

apps 區塊方面,所有的配置項目可以參考 PM2 文件〈Configuration File〉,這裡不重複說明,下面是本人常用的配置:

apps: [
    {
      // General
      name: 'liveboard',
      script: './build/index.js',
      port: 4000,

      // Advanced features
      instances: 2,
      env: { NODE_ENV: "development" },
      env_production: { NODE_ENV: "production" },
      env_staging: { NODE_ENV: "staging" },
      env_development: { NODE_ENV: "development" },

      // Log files
      time: true,
    },
  ]

裡面的 nameport 就是字面上的意思,下面的 instance 自然就是這支 liveboard app 要開的程序數量了,透過多個程序來增加服務的容納量,這裡 PM2 會用 Node.js 的 cluster 模式來管理程序,那對外的 port 4000 由所有的 liveboard instance 共享,不會發生搶 port 的問題。

再往下是幾組 env 開頭的配置,這裡的 env_xxx 是可以自由制定的,裡面當然就是放環境變數啦,而不帶任何後綴的 env 則是預設的環境變數。

最後有個 time: true,就只是把 log 加上時間戳。

運行 App

配置好就可以跑起來了:

$ pm2 start ecosystem.config.js

查看納管 app 狀態:

$ pm2 list

輸出如下:

⇆ PM2+ activated
┌────┬───────────┬───────────┬─────────┬─────────┬───────┬────────┬───┬────────┬─────┬────────┬──────┬──────────┐
│ id │ name      │ namespace │ version │ mode    │ pid   │ uptime │ ↺ │ status │ cpu │ mem    │ user │ watching │
├────┼───────────┼───────────┼─────────┼─────────┼───────┼────────┼───┼────────┼─────┼────────┼──────┼──────────┤
│ 0  │ liveboard │ default   │ 0.0.1   │ cluster │ 11784 │ 63m    │ 0 │ online │ 0%  │ 32.9mb │ user │ disabled │
│ 1  │ liveboard │ default   │ 0.0.1   │ cluster │ 16076 │ 63m    │ 0 │ online │ 0%  │ 32.0mb │ user │ disabled │
└────┴───────────┴───────────┴─────────┴─────────┴───────┴────────┴───┴────────┴─────┴────────┴──────┴──────────┘

產生 systemd 服務配置檔案

另外 PM2 也可以產生 systemd 的服務配置檔,以普通用戶身分執行:

$ pm2 startup

它會輸出:

[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u mes --hp /home/mes

依照吩咐執行:

$ sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u mes --hp /home/mes

輸出訊息如下:

[PM2] Init System found: systemd
Platform systemd
Template

[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=mes
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/home/mes_staging/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/usr/bin:/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin
Environment=PM2_HOME=/home/mes/.pm2
PIDFile=/home/mes/.pm2/pm2.pid
Restart=on-failure

ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

Target path
/etc/systemd/system/pm2-mes.service

Command list
[ 'systemctl enable pm2-mes' ]

[PM2] Writing init configuration in /etc/systemd/system/pm2-mes.service
[PM2] Making script booting at startup...
[PM2] [-] Executing: systemctl enable pm2-mes...
Created symlink /etc/systemd/system/multi-user.target.wants/pm2-mes.service → /etc/systemd/system/pm2-mes.service.
[PM2] [v] Command successfully executed.
+---------------------------------------+
[PM2] Freeze a process list on reboot via:
$ pm2 save

[PM2] Remove init script via:
$ pm2 unstartup systemd

如訊息所表示,它幫我們生成一份 systemd 服務配置檔案,也幫我們啟用了,如此以後重開機就會自動把 PM2 服務跑起來。

接著我們要做的是,告訴 PM2 它要把哪些 app 叫起來。延續前面的例子,先確定要跑的 app 有在運行中:

$ pm2 start ecosystem.config.cjs
$ pm2 list

輸出如下:

⇆ PM2+ activated
┌────┬───────────┬───────────┬─────────┬─────────┬───────┬────────┬───┬────────┬─────┬────────┬──────┬──────────┐
│ id │ name      │ namespace │ version │ mode    │ pid   │ uptime │ ↺ │ status │ cpu │ mem    │ user │ watching │
├────┼───────────┼───────────┼─────────┼─────────┼───────┼────────┼───┼────────┼─────┼────────┼──────┼──────────┤
│ 0  │ liveboard │ default   │ 0.0.1   │ cluster │ 11784 │ 63m    │ 0 │ online │ 0%  │ 32.9mb │ user │ disabled │
│ 1  │ liveboard │ default   │ 0.0.1   │ cluster │ 16076 │ 63m    │ 0 │ online │ 0%  │ 32.0mb │ user │ disabled │
└────┴───────────┴───────────┴─────────┴─────────┴───────┴────────┴───┴────────┴─────┴────────┴──────┴──────────┘

把目前運行中的 app 保存作為 PM2 服務啟動時要帶起來的 app:

$ pm2 save

輸出如下:

[PM2] Saving current process list...
[PM2] Successfully saved in /home/mes/.pm2/dump.pm2

這個 dmp.pm2 是一份 JSON 文件,裡面紀錄一大堆前面兩個 app instance 的參數。

最後,當然就是重開機一波確認服務是否如常運行。

最後,因為 PM2 的角色是 app server,在正式環境通常還會在 app server 之前擺一個負責反向代理的 web server,最典型的就是 NGINX,由 NGINX 負責對外提供服務,再把部分請求轉交給對應的 app server,關於 NGINX 與 PM2 的轉發配置,可以參考 PM2 的〈Production Setup with Nginx〉,而如果是 Caddy,那可以參考以下 caddyfile 配置:

http:// {
	handle_path /* {
		reverse_proxy :4000
	}
}

上面配置會把所有的請求都轉發到 4000 埠,也就是 PM2 納管監聽的埠,至於轉發要帶上的那些 HTTP 標頭,X-Forwarded-ForX-Real-IP 什麼的,Caddy 原本就會帶上,無須額外配置,GJ。

其他常用命令

還有一些常用命令:

  • $ pm2 restart app_name:重啟 app。
  • $ pm2 stop app_name:停止 app。
  • $ pm2 delete app_name:終止 app。
  • $ pm2 logs app_name:顯示 app log 訊息。
  • $ pm2 describe app_name:顯示 app 資訊。

pm2 describe app_name 資訊滿多的,輸出如下:

Describing process with id 0 - name liveboard-svelte 
┌───────────────────┬──────────────────────────────────────────────────────────┐
│ status            │ online                                                   │
│ namespace         │ default                                                  │
│ version           │ 0.0.1                                                    │
│ restarts          │ 0                                                        │
│ uptime            │ 4h                                                       │
│ script args       │ N/A                                                      │
│ interpreter       │ node                                                     │
│ interpreter args  │ N/A                                                      │
│ script id         │ 0                                                        │
│ exec mode         │ cluster_mode                                             │
│ node.js version   │ 18.17.1                                                  │
│ node env          │ development                                              │
│ watch & reload    │ ✘                                                        │
│ unstable restarts │ 0                                                        │
│ created at        │ 2023-05-09T09:05:58.233Z                                 │
└───────────────────┴──────────────────────────────────────────────────────────┘
 Revision control metadata 
┌──────────────────┬───────────────────────────────────────────────────────┐
│ revision control │ git                                                   │
│ last update      │ 2023-09-20T01:33:17.933Z                              │
│ revision         │ e05dfe075a2f791f530ed15b3110f46ee7325772              │
│ comment          │ Merge pull request 'main' (#7) from main into staging │
│                  │                                                       │
│                  │ Revie                                                 │
│ branch           │ staging                                               │
└──────────────────┴───────────────────────────────────────────────────────┘
 Actions available 
┌────────────────────────┐
│ km:heapdump            │
│ km:cpu:profiling:start │
│ km:cpu:profiling:stop  │
│ km:heap:sampling:start │
│ km:heap:sampling:stop  │
└────────────────────────┘
 Trigger via: pm2 trigger liveboard-svelte <action_name>

 Code metrics value 
┌────────────────────────┬───────────┐
│ Used Heap Size         │ 13.38 MiB │
│ Heap Usage             │ 91.06 %   │
│ Heap Size              │ 14.69 MiB │
│ Event Loop Latency p95 │ 1.22 ms   │
│ Event Loop Latency     │ 0.32 ms   │
│ Active handles         │ 1         │
│ Active requests        │ 0         │
└────────────────────────┴───────────┘
 Divergent env variables from local env 
┌────────────────┬──────────────────────────────────────────┐
│ XDG_SESSION_ID │ 180                                      │
└────────────────┴──────────────────────────────────────────┘