PM2 的 restarts 计数一直涨、uptime 永远只有几秒,说明进程没能稳定跑起来。下面按「先看日志、再排路径、再看端口、最后看资源」的顺序走一遍。
报错现象
典型表现有几种,可以对照自己遇到的是哪一种:
pm2 list里状态在online、stopping、errored之间跳,↺(restarts)数字几分钟内涨到几十上百。uptime始终是0s或几秒,说明进程刚起来就退出。pm2 logs里同一个错误反复刷屏,例如:
```
Error: listen EADDRINUSE: address already in use :::3000
Error: Cannot find module 'express'
Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'dotenv'
```
- 应用启动后没有明显报错,但每隔一段时间就被重启一次,日志里能看到内存相关的提示,或者
dmesg里出现Out of memory: Killed process ...。
影响范围通常是:服务对外表现为间歇性 502/504、接口偶发连接被拒、定时任务重复执行或漏执行。因为进程一直在重启,HTTP 请求会在「刚起来」和「已挂掉」之间随机命中。
可能原因
按出现概率从高到低:
1. 应用自身启动就抛异常:配置项缺失、环境变量没读到、数据库连不上、代码里有语法错误或未捕获异常,进程启动后立刻退出,退出码通常是 1。
2. 模块或文件路径不对:Cannot find module、ENOENT: no such file or directory。常见于 node_modules 没装全、生产环境装了 --omit=dev 但代码引用了开发依赖、以及 PM2 的 cwd 和当初启动时不一致。
3. 端口被占用:EADDRINUSE。典型场景是 fork 模式下 instances 设成了大于 1,或者旧进程没退干净,新进程抢不到端口。
4. 内存超限:配置了 max_memory_restart 且阈值偏低,进程一涨到阈值就被主动重启;或者进程被系统 OOM Killer 杀掉,退出码是 137。
5. 崩溃循环触发 PM2 的熔断:短时间内重启次数超过 max_restarts(或存活时间低于 min_uptime),PM2 会把进程标成 errored 并停止拉起,看起来像「重启失败」。
6. 系统层因素:磁盘写满、文件描述符耗尽、日志目录没权限、.env 文件在 pm2 startup 之后丢失等。
逐条排查与解决
1. 先把日志和退出码拿到手
不要凭感觉猜,先看证据:
```bash
pm2 list
pm2 describe my-app
pm2 logs my-app --lines 200 --nostream
pm2 logs my-app --err --lines 200 --nostream
```
pm2 describe 会给出 script path、exec cwd、restarts、unstable restarts、exit code 等信息。日志文件一般在 ~/.pm2/logs/ 下,按名字找 <app-name>-error.log:
```bash
ls -lh ~/.pm2/logs/
tail -n 200 ~/.pm2/logs/my-app-error.log
```
判断依据:如果报错栈指向你自己的代码(比如 TypeError: Cannot read properties of undefined),那就是应用问题;如果报的是 Cannot find module、EADDRINUSE,就往下走对应小节。
想看到最原始的报错,可以绕开 PM2 直接在项目目录里跑:
```bash
cd /srv/my-app
NODE_ENV=production node dist/server.js
```
能稳定跑住,说明代码没问题,问题在 PM2 的配置或运行环境;照样崩,那就是应用自己的事,先修代码。
2. 检查模块与路径
先确认 PM2 眼里的工作目录是不是你期望的:
```bash
pm2 describe my-app | grep -i -E 'script path|exec cwd|node args'
```
cwd 不对是最隐蔽的一类问题——手动 pm2 start 时当前目录是项目根目录,重启后(尤其是机器重启、pm2 resurrect 之后)可能变成 /,于是读 .env、读模板文件全都 ENOENT。
在正确目录里验证依赖是否完整:
```bash
cd /srv/my-app
npm ls --depth=0
node -e "require('express'); console.log('ok')"
ls node_modules | head
```
如果有 Cannot find module,按生产环境重新装一次:
```bash
cd /srv/my-app
npm ci --omit=dev
```
注意:如果代码里 require 了只在 devDependencies 里的包(比如某些模板引擎、构建工具),生产安装时会被剪掉,需要把它挪到 dependencies,或者用 npm ci 全量安装。含 C++ 原生模块的包(如 bcrypt、sharp 之类)换机器或换 Node 大版本后需要重新编译,npm rebuild 通常能解决。
解决路径问题的方式是把配置固化下来,不用命令行裸启动:
```js
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'my-app',
script: 'dist/server.js',
cwd: '/srv/my-app',
instances: 2,
exec_mode: 'cluster',
max_memory_restart: '700M',
min_uptime: '10s',
max_restarts: 20,
restart_delay: 3000,
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: '/var/log/pm2/my-app-error.log',
out_file: '/var/log/pm2/my-app-out.log',
merge_logs: true,
time: true
}
]
};
```
然后:
```bash
pm2 delete my-app
pm2 start ecosystem.config.js
pm2 save
```
cwd 写绝对路径,能避免绝大多数「本地能跑、服务器上找不到文件」的问题。
3. 检查端口占用
看到 EADDRINUSE 就查这个端口现在被谁占着:
```bash
ss -ltnp | grep ':3000'
或者
lsof -i :3000
```
如果输出里有 node 进程,先确认它是不是你的应用:
```bash
ps -fp <PID>
```
处理办法分两种:
- 是残留的旧进程:
kill <PID>,等它退出后再pm2 restart my-app。如果kill不掉,再考虑kill -9。 - 是新旧两个 PM2 实例在抢:检查
instances和exec_mode的搭配。在 fork 模式下把instances设成大于 1,多个进程会各自去监听同一个端口,必然冲突。要跑多实例就用exec_mode: 'cluster',由 Node 的 cluster 机制共享端口:
```bash
pm2 delete my-app
pm2 start ecosystem.config.js # 里面 exec_mode 为 cluster
```
另外确认一下应用代码里 listen 的端口是不是硬编码的,如果是,多实例部署时容易和环境变量里的 PORT 对不上。
4. 检查内存限制与系统 OOM
先看 PM2 自己有没有因为内存触发重启:
```bash
pm2 describe my-app | grep -i -E 'memory|restart'
free -h
```
再看系统有没有杀进程:
```bash
dmesg -T | grep -i -E 'oom|killed process'
journalctl -k --since "1 hour ago" | grep -i oom
```
判断依据:exit code 是 137(128 + 9,即被 SIGKILL),且 dmesg 里有对应记录,基本可以确定是被 OOM 杀掉,而不是代码抛异常。
如果确实是 max_memory_restart 设得太低(比如 256M 却跑了 400M),两种做法:一是调大阈值,二是去查内存泄漏。定位泄漏可以用:
```bash
pm2 monit
curl -s http://127.0.0.1:9615/ # 需要启用 pm2 的监控模块时才有,按官方文档配置
```
更直接的办法是给 Node 设置堆上限,让它在被杀之前先抛出堆溢出错误,便于抓现场:
```bash
node --max-old-space-size=1024 dist/server.js
```
在 ecosystem 里通过 node_args 传入:
```js
node_args: ['--max-old-space-size=1024']
```
阈值设多少,要看机器的实际内存和应用的常驻内存,参考 pm2 describe 里的 memory 数值再定。
5. 处理崩溃循环熔断
如果 pm2 list 里状态是 errored,说明 PM2 已经放弃拉起了。先重置计数:
```bash
pm2 reset my-app
pm2 restart my-app
```
排查期可以临时关掉自动重启,避免日志被刷爆,同时把真实退出码固定下来:
```bash
pm2 delete my-app
pm2 start ecosystem.config.js --no-autorestart
```
配上 min_uptime 和 restart_delay,能让 PM2 在进程快速崩溃时不要疯狂重试:
```js
min_uptime: '10s', // 存活不足 10 秒算一次不稳定启动
max_restarts: 20, // 不稳定启动超过 20 次就进入 errored
restart_delay: 3000 // 每次重启前等 3 秒
```
这几个值没有统一标准,按自己的启动耗时和容错要求来定。
6. 系统层排查
顺手确认这几项,成本很低但经常中招:
```bash
df -h # 磁盘是不是满了
ulimit -n # 文件描述符够不够
ls -ld ~/.pm2 ~/.pm2/logs /var/log/pm2
pm2 show my-app | grep -i 'user\|cwd'
```
日志目录没写权限会导致进程起来就退出;ENOSPC(磁盘满)会让写入操作全部失败;ulimit -n 太小会在高并发下报 EMFILE: too many open files。这几项改完记得 pm2 save。
都不管用时的兜底方案
1. 彻底清掉 PM2 的状态再重建。运行久了 ~/.pm2 里的 dump 和日志会残留脏数据:
```bash
pm2 delete all
pm2 flush
pm2 save
pm2 start ecosystem.config.js
pm2 save
```
2. 前台模式观察。让 PM2 不后台化,直接在当前终端看输出:
```bash
pm2 start ecosystem.config.js --no-daemon
```
3. 换掉进程管理器定位问题。用 systemd 直接托管,排除 PM2 这一层的干扰:
```ini
/etc/systemd/system/my-app.service
[Unit]
Description=my-app
After=network.target
[Service]
Type=simple
WorkingDirectory=/srv/my-app
Environment=NODE_ENV=production
EnvironmentFile=-/srv/my-app/.env
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=3
StandardOutput=append:/var/log/my-app.log
StandardError=append:/var/log/my-app-error.log
[Install]
WantedBy=multi-user.target
```
```bash
systemctl daemon-reload
systemctl start my-app
systemctl status my-app
journalctl -u my-app -f
```
如果 systemd 下也一样反复重启,问题几乎肯定在应用或环境里,与 PM2 无关。
4. 容器场景。容器里 PM2 不是 PID 1,信号可能收不到,建议容器内直接用 node 启动,把进程守护交给编排系统;确实要用 PM2,就把 --no-daemon 或对应的前台模式打开。
如何预防再次发生
- 配置进版本库。用
ecosystem.config.js固化cwd、env、instances、exec_mode、内存阈值,禁止在生产机上裸敲pm2 start app.js。 - 日志轮转。装一次
pm2-logrotate,避免日志把磁盘写满后引发一连串启动失败:
```bash
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
```
- 加重启告警。把
pm2 jlist输出的restart_time定期采集,超过阈值就告警,别等用户报障。
```bash
pm2 jlist | node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>{JSON.parse(d).forEach(p=>console.log(p.name,p.pm2_env.restart_time,p.pm2_env.status))})"
```
- 上线前冒烟测试。CI 里跑一遍
npm ci && node dist/server.js,跑几秒确认能起来,再发布。 - 锁死依赖。提交 lockfile,部署时用
npm ci而不是npm install,避免依赖漂移带来的Cannot find module。 - 发布用
pm2 reload。cluster 模式下reload逐个替换实例,不会造成端口抢占式的抖动。 - 保存开机自启配置。
pm2 startup与pm2 save配对使用,且确认开机自启脚本里带上需要的环境变量(尤其是.env里的内容),版本与参数以官方文档当前版本为准。
按「日志 → 路径依赖 → 端口 → 内存 → 熔断 → 系统资源」这个顺序走一遍,绝大多数反复重启都能定位到一个明确的退出原因,而不是继续靠重启碰运气。
