我服务器上挂着 6 个 pm2 进程,一直觉得 pm2 就是「起个服务然后不管了」。直到某天 df -h 看到磁盘 85%,翻日志发现一个早就删掉的应用,toolsite-error.log 还在那儿躺着,1.8M,32690 行,全是同一个报错:
Error: Cannot find module '/opt/toolsite/server.js'
at Function._resolveFilename (node:internal/modules/cjs/loader:1430:15)
at defaultResolveImpl (node:internal/modules/cjs/loader:1040:19)
at Function._load (node:internal/modules/cjs/loader:1216:25)
应用目录 /opt/toolsite 早就没了,pm2 里也没这个进程了,但日志还在一直刷。
下面每条都是隔离 PM2_HOME 在 49.233.191.177 上实测的。
坑一:崩溃循环会把你刷到没话说
pm2 的重启保护逻辑写在 lib/God.js 里,核心是这段:
var min_uptime = typeof(proc.pm2_env.min_uptime) !== 'undefined' ? proc.pm2_env.min_uptime : 1000;
var max_restarts = typeof(proc.pm2_env.max_restarts) !== 'undefined' ? proc.pm2_env.max_restarts : 16;
if ((Date.now() - proc.pm2_env.created_at) < (min_uptime * max_restarts)) {
if ((Date.now() - proc.pm2_env.pm_uptime) < min_uptime) {
proc.pm2_env.unstable_restarts += 1;
}
}
if (proc.pm2_env.unstable_restarts >= max_restarts) {
proc.pm2_env.status = cst.ERRORED_STATUS;
proc.process.pid = 0;
console.log('Script %s had too many unstable restarts (%d). Stopped. %j', ...);
}
注意这个条件链:unstable_restarts 只在进程活不到 1 秒(min_uptime)时才累加。
我先写了个延迟 1 秒再抛异常的脚本,按理 uptime 超过 1 秒,不该计入 unstable。结果 20 秒内重启了 51 次,状态还是 online:
│ 0 │ lab-crash │ default │ N/A │ fork │ 4089382 │ 0s │ 35 │ online │ 0% │ 59.5mb │ ubuntu │ disabled │
换成启动就崩的脚本,判定立刻生效:
throw new Error('instant crash');
│ 0 │ lab-crash2 │ default │ N/A │ fork │ 0 │ 0 │ 15 │ errored │ 0% │ 0b │ ubuntu │ disabled │
15 次之后状态变 errored,pid 归零,pm2 停止拉起。日志里留下 176 行堆栈。
这就是 pm2 的设计意图:崩溃得「够快」才算不稳定,偶发重启不会误伤。但反过来说,一个每 1.05 秒崩一次的进程能永远重试下去。真实场景就是「端口没释放」「配置没加载」这种每次启动都必崩、但启动耗时略超 1 秒的情况——服务起不来,日志还在疯长,磁盘就是这么被吃掉的。
我给这类服务加了 min_uptime 把判定基线拉高:
pm2 start app.js --name myapp --min-uptime 5000 --max-restarts 10
--min-uptime 5000 之后,进程要活过 5 秒才算「稳定启动」,10 秒内崩 10 次就停手。日常看三个字段基本能判断健康度:status 是 online 还是 errored,restarts 涨得多快,unstable restarts 是不是在往 16 顶。
坑二:内存泄漏进程不会自己死,但会把服务器拖垮
shortlink 用了 better-sqlite3,跑久了内存只涨不降。pm2 有个参数专门治这个:
pm2 start app.js --name myapp --max-memory-restart 60M
我写了个持续吃内存的脚本验证:
const blocks = [];
setInterval(() => {
for (let i = 0; i < 8; i++) blocks.push(Buffer.alloc(8 * 1024 * 1024, i));
console.log('mem MB', Math.round(process.memoryUsage().rss / 1024 / 1024));
}, 300);
启动时内存 17mb,一路涨到 396mb,pm2 在越过阈值后自动重启了它:
│ status │ online │
│ restarts │ 1 │
│ max memory restart │ 62914560 │
62914560 就是 60 × 1024 × 1024,pm2 内部按字节存这个值。注意日志里同时有 mem MB 396 的输出——pm2 是发现超限后才重启的,不是提前拦住的,阈值设 60M 的话峰值可能冲到几百 M。
我这服务器就 3.6G 内存,多跑几个服务就得出问题,所以给每个服务都加了上限:
pm2 start app.js --name myapp --max-memory-restart 512M --min-uptime 5000 --max-restarts 10
pm2 describe 里还能看实时堆内存:
│ Heap Size │ 21.35 MiB │
│ Heap Usage │ 53.65 % │
│ Used Heap Size │ 11.45 MiB │
坑三:日志只增不减,删了应用日志还在
pm2 默认不装 pm2-logrotate,我服务器上 ~/.pm2/ 已经 7.5M,ls ~/.pm2/logs/ 里躺着一堆早已下线的应用:
-rw-r--r-- 1 ubuntu ubuntu 1.8M Jun 25 16:51 toolsite-error.log
-rw-r--r-- 1 ubuntu ubuntu 14K Jul 10 15:25 groups-api-error.log
-rw-r--r-- 1 ubuntu ubuntu 5.8K Jul 1 09:07 bagua-fortune-error.log
原因很简单:pm2 delete <name> 只删进程,不删日志。程序目录被 rm -rf 了,pm2 那边也摘干净了,日志就成了孤儿。
第一种是 pm2 flush,一条命令清空所有进程的日志:
pm2 flush
[PM2] /home/ubuntu/.pm2/logs/lab-A-out.log
[PM2] /home/ubuntu/.pm2/logs/lab-A-error.log
[PM2] Logs flushed
实测 17 字节的日志被截成 0 字节,进程没重启。但它是「清零」不是「轮转」,历史日志全没了,没法回溯。
第二种是真正的轮转,用系统自带的 logrotate(我服务器上 3.21.0,logrotate.timer 已 enabled)。配置文件放 /etc/logrotate.d/pm2:
/home/ubuntu/.pm2/logs/*.log {
size 50M
rotate 5
copytruncate
missingok
notifempty
compress
delaycompress
su ubuntu ubuntu
}
几个参数的意思:
size 50M而不是daily——日志量波动大的服务,按大小切更可控。copytruncate是关键。pm2 一直持有日志文件的 fd,单纯mv走文件句柄不会断,进程还会往老文件里写。只有 copy + truncate 能让 pm2 继续写同一个 inode。su ubuntu ubuntu告诉 logrotate 以谁的身份轮转。不加这行,压缩出来的文件属主会变成 root。
验证轮转效果:
sudo logrotate -f -v /etc/logrotate.d/pm2
copying /home/ubuntu/.pm2/logs/lab-A-out.log to /home/ubuntu/.pm2/logs/lab-A-out.log.1
truncating /home/ubuntu/.pm2/logs/lab-A-out.log
compressing log with: /bin/gzip
轮转后出现 lab-A-out.log.1.gz,原文件归零,进程在线:
-rw-rw-r-- 1 ubuntu ubuntu 0 Nov 11 08:25 lab-A-out.log
-rw-rw-r-- 1 ubuntu ubuntu 524 Nov 11 08:24 lab-A-out.log.1.gz
我用普通用户身份跑 logrotate -f,报了这么个错:
error: error opening state file /var/lib/logrotate/status: Permission denied
state 文件归 root,普通用户没权限写。加 sudo 就好,测试时也可以用独立 state 文件绕过:
logrotate -f -v -s /tmp/lr.state /tmp/lr.conf
删应用时顺手清掉孤儿日志:
pm2 delete <name> && rm -f ~/.pm2/logs/<name>-*.log
坑四:pm2 save 之后再加的进程,重启服务器就没了
这条最容易踩:服务在跑好好的,服务器一重启,全没了。
先起 lab-A,保存,再起 lab-B:
pm2 start /tmp/pm2-lab/ok.js --name lab-A
pm2 save --force
pm2 start /tmp/pm2-lab/ok.js --name lab-B
然后模拟机器重启(pm2 kill 干掉整个 daemon)再 pm2 resurrect:
[PM2] [v] All Applications Stopped
[PM2] [v] PM2 Daemon Stopped
resurrect 之后只有 lab-A:
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
│ 0 │ lab-A │ default │ N/A │ fork │ 4092336 │ 3s │ 0 │ online │ 0% │ 59.4mb │ ubuntu │ disabled │
lab-B 没了。 原因在 pm2 save 的语义:它把当前进程列表快照写进 ~/.pm2/dump.pm2,pm2 resurrect 只读这个快照。快照之后新增的进程不在文件里,自然恢复不出来。
开机自启的链路是这样串起来的(pm2 startup systemd),我服务器上生成的是 /etc/systemd/system/pm2-ubuntu.service:
[Unit]
Description=PM2 process manager
After=network.target
[Service]
Type=forking
User=ubuntu
Environment=PM2_HOME=/home/ubuntu/.pm2
PIDFile=/home/ubuntu/.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
ExecStart 就是 pm2 resurrect,读的是 dump.pm2。所以 每次新增或修改进程配置后,必须重新 pm2 save:
pm2 save
pm2 save --force
--force 会跳过那些被标记为「不需要保存」的进程。另外有个坑:pm2 save 默认存的是当前用户,sudo pm2 save 存的是 root 的,两份 dump 互相不通用。
检查 dump 里都有谁:
python3 -c "
import json
d = json.load(open('/home/ubuntu/.pm2/dump.pm2'))
[print(p['name'], '| watch =', repr(p.get('watch'))) for p in d]
"
dump 的内容:
shortlink watch = False
wechat-webhook watch = None
blog-admin watch = False
blog-stats watch = False
article-qa watch = ['false']
filehub watch = False
看到 article-qa 那行的 ['false'] 了吗?pm2 start x.js --watch false 里的 false 被当成字符串存进了数组,而非空数组在 JS 里是真值,结果 watch 模式被意外打开了,pm2 list 的 watching 列显示 enabled。同一行的 ↺ 列显示 article-qa 已经重启过 6 次。
判断 dump 是否过期,看时间戳对不对得上:
stat -c '%y %n' ~/.pm2/dump.pm2
2026-10-21 10:54:56 +0800 /home/ubuntu/.pm2/dump.pm2
再和每个进程的 created_at 比一比。filehub 的创建时间戳 1792551296452 正好是 10-21 10:54,也就是最后一次 pm2 save 的时刻——说明之后没再新增进程。
我现在的配置
我现在给每个服务都用这套参数启动:
pm2 start app.js --name myapp \
--max-memory-restart 512M \
--min-uptime 5000 \
--max-restarts 10
这三个参数各管一件事:内存超限重启、启动 5 秒内崩算不稳定、10 次不稳定就彻底停手。
顺带一个提醒:pm2 5.x 时代有个 --expire 参数(超过 N 毫秒没重启就主动重启一次)。我在 pm2 7.0.1 源码里全局搜过,expire 已经不存在了,照着老教程敲 --expire 不会报错,但也不会生效。想要定时探活,交给 systemd 的 Restart= 更靠谱。
再加一个保存脚本,防止改完配置忘记 save:
#!/bin/bash
set -euo pipefail
pm2 save
pm2 describe --all | grep -E 'name|status|restarts|max memory'
重点是每次 pm2 start / pm2 delete 后跑一次。
日常巡检就看这几条:
pm2 list # status 和 ↺ 重启次数
pm2 describe <name> | grep -E 'status|restarts|unstable|max memory'
du -sh ~/.pm2/ # 日志目录有没有失控
pm2 jlist | python3 -c "import json,sys; [print(p['name'], p['monit']['memory'], p['pm2_env']['restart_time']) for p in json.load(sys.stdin)]"
最后一条能列出内存占用和重启次数,适合写进监控脚本。
收尾
pm2 本身不复杂,坑都在「默认行为不告诉你」上:崩溃了会无限重启、日志永远不轮转、save 只是快照、--watch false 反而开 watch。这些都不会报错,只会慢慢把服务器拖垮,得自己盯着 status、restarts、~/.pm2 体积这三样东西。
我服务器上那些孤儿日志就是最好的例子——应用都删干净了,日志还在那儿躺着提醒我当初没做对。