PM2部署实战:从零配置到线上稳定运行

Mc.淑然 前端 阅读 2,889
赞 14 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

我用 PM2 部署 Node.js 项目已经快五年了,从一开始直接 pm2 start app.js 到后来折腾各种配置,踩过不少坑。现在我的标准做法是:永远用 ecosystem.config.js 文件来管理部署,而不是靠命令行参数。

PM2部署实战:从零配置到线上稳定运行

为什么?因为命令行参数一多就乱,而且不同环境(开发、测试、生产)的配置根本没法复用。有一次我在服务器上手动敲了一堆参数,结果第二天服务挂了,重跑的时候漏了个 --max-memory-restart,内存爆了直接 OOM。从那以后我就彻底改用配置文件了。

这是我现在的模板,直接复制就能用:

module.exports = {
  apps: [
    {
      name: 'my-app',
      script: './dist/server.js',
      instances: 'max', // 自动根据 CPU 核数启动实例
      exec_mode: 'cluster',
      watch: false, // 生产环境千万别开!
      max_memory_restart: '500M',
      env: {
        NODE_ENV: 'production',
        PORT: 3000
      },
      error_file: './logs/err.log',
      out_file: './logs/out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      merge_logs: true,
      autorestart: true,
      restart_delay: 3000,
      kill_timeout: 5000
    }
  ]
};

几个关键点我得强调一下:

  • instances 设为 ‘max’:能充分利用多核 CPU,但要注意你的应用是否真的支持多进程(比如有没有全局状态)。我之前有个 WebSocket 服务没处理好连接共享,结果多个实例导致用户被踢来踢去,折腾了半天才定位到是 PM2 多实例的问题。
  • watch 必须关掉:生产环境开这个等于埋雷。它会监听文件变化自动重启,但线上代码是编译后的,你根本不会在服务器上改文件。更糟的是,如果日志文件被写入,它也会触发重启(因为日志在项目目录里),导致服务疯狂重启。
  • log 路径要显式指定:默认日志在 ~/.pm2/logs 下,找起来麻烦。我习惯把日志放在项目根目录的 logs 文件夹,配合 logrotate 管理,避免磁盘被撑爆。

这几种错误写法,别再踩坑了

我见过太多人这么干,结果半夜被报警叫醒:

错误写法一:直接 pm2 start 后不管了

比如:pm2 start app.js --name myapp。问题在于,一旦服务器重启,PM2 进程本身也会挂掉,你的服务就没了。必须执行 pm2 save 并设置开机自启(pm2 startup),但很多人忘了这一步。我现在都是在部署脚本里自动加这两行,省得手抖。

错误写法二:在 ecosystem.config.js 里硬编码敏感信息

比如直接写数据库密码:

// 千万别这么干!
env: {
  DB_PASSWORD: 'mysecretpassword123'
}

正确做法是通过环境变量注入,或者用 .env 文件(但记得加到 .gitignore 里)。我一般在 CI/CD 流程里动态生成配置,或者用 Vault 这类工具。实在不行,至少把敏感信息从代码仓库里剥离出去。

错误写法三:kill_timeout 设得太小

默认是 1600ms,但如果你的应用有长连接(比如 WebSocket)或需要清理资源(比如关闭数据库连接),这点时间根本不够。我之前一个服务因为 timeout 太短,每次重启都留下一堆 TIME_WAIT 连接,最后端口耗尽。现在一律设成 5000ms 以上,看业务需求调整。

实际项目中的坑

上周刚遇到一个诡异问题:服务在 PM2 里跑着跑着就假死,CPU 和内存都正常,但接口完全没响应。查了半天发现是 autorestart: true 在作怪——当进程卡死(比如死循环)时,PM2 以为它 crash 了,就不断重启,但其实旧进程还在占着端口。最后用了 pm2 reload 替代 pm2 restart,配合 graceful shutdown 才解决。

说到 graceful shutdown,这是个大坑。Node.js 默认不会处理 SIGTERM 信号,所以 PM2 发送 kill 信号后,你的服务可能直接被干掉,正在处理的请求就断了。我的做法是在入口文件加一段:

// server.js
const httpServer = app.listen(port, () => {
  console.log(Server running on port ${port});
});

process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  httpServer.close(() => {
    console.log('Process terminated');
  });
  // 如果有数据库连接池,这里也要 close
});

另外,PM2 的日志默认不切割,跑一个月下来几个 G 很常见。我现在的方案是:用 PM2 写日志到文件,再用系统的 logrotate 按天切割。配置文件大概是这样:

# /etc/logrotate.d/myapp
/home/ubuntu/myapp/logs/*.log {
  daily
  missingok
  rotate 30
  compress
  delaycompress
  notifempty
  create 644 ubuntu ubuntu
  postrotate
    pm2 reloadLogs
  endscript
}

注意最后那个 pm2 reloadLogs,不然 PM2 还会往旧文件句柄写日志,新文件一直是空的。

一点不完美的妥协

其实 PM2 也不是万能的。比如它没法像 systemd 那样精细控制资源限制(CPU、内存 quota),也没法做网络隔离。但在中小型项目里,它的简单和稳定性足够用了。我试过用 Docker + systemd,但维护成本太高,最后还是回归 PM2。

还有一个小毛病:PM2 的 Web UI(pm2 web)在高并发下会拖慢主进程,所以我从来不用。监控靠 Prometheus + Grafana,日志用 ELK,PM2 只负责进程守护——各司其职最稳。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们怎么处理多环境配置差异?或者有没有遇到 PM2 和 TypeScript 编译缓存冲突的问题?(我上周刚被这个坑了)

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
UI国娟
UI国娟 Lv1
作者分享的调试技巧很实用,帮我快速定位了项目中的问题,节省了大量时间。
点赞 1
2026-03-01 15:25