跳到主内容
快讯直播
AI智模界
教程

PM2 进程反复重启的排查顺序与实操

PM2 的 restarts 计数一直涨、uptime 永远只有几秒,说明进程没能稳定跑起来。下面按「先看日志、再排路径、再看端口、最后看资源」的顺序走一遍。

报错现象

典型表现有几种,可以对照自己遇到的是哪一种:

  • pm2 list 里状态在 onlinestoppingerrored 之间跳,(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 moduleENOENT: 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 pathexec cwdrestartsunstable restartsexit 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 moduleEADDRINUSE,就往下走对应小节。

想看到最原始的报错,可以绕开 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++ 原生模块的包(如 bcryptsharp 之类)换机器或换 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 实例在抢:检查 instancesexec_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 code137(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_uptimerestart_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 固化 cwdenvinstancesexec_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 startuppm2 save 配对使用,且确认开机自启脚本里带上需要的环境变量(尤其是 .env 里的内容),版本与参数以官方文档当前版本为准。

按「日志 → 路径依赖 → 端口 → 内存 → 熔断 → 系统资源」这个顺序走一遍,绝大多数反复重启都能定位到一个明确的退出原因,而不是继续靠重启碰运气。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。