PM2 进程守护实战:ecosystem 配置、集群模式、日志轮转与开机自启全流程

Node 应用又双叒挂了?你需要一个靠谱的进程守护

用 Node.js 写小工具、跑 API 服务、做定时任务的站长越来越多,但新手常犯同一个错:SSH 上去 node app.js 一敲,看着它跑起来就关掉终端,结果一断线进程就没了,或者半夜抛个异常进程直接退出,第二天早上网站 502。裸跑 Node 进程在生产环境里是不行的,你需要一个进程管理器来守护它:崩溃自动重启、开机自启、日志落盘、多核利用。

PM2 就是为这个场景而生的工具。它轻量、上手快、功能全,能同时管 Node、Python、Go 甚至任意可执行程序,是个人站长和小团队部署 Node 服务的首选。这篇教程从安装讲到生产级配置:用 ecosystem 配置文件固化启动参数、开集群模式吃满多核、日志轮转防止磁盘被写爆、开机自启、零停机重载,以及一组常用排障命令。全程基于一台普通 Linux VPS。

安装 PM2

前提是服务器上装了 Node.js 和 npm。确认版本:

node -v
npm -v

全局安装 PM2:

npm install -g pm2

装完验证:

pm2 -v

如果提示 command not found,多半是 npm 全局目录没进 PATH。用 npm config get prefix 找到全局目录(通常 /usr/local 或某个 nvm 目录),确认 bin 在其中。用 nvm 装的 Node 一般不会有这个问题。

第一次启动:先跑起来再说

最简单的启动方式,指定一个名称方便管理:

pm2 start app.js --name myapi

看它有没有跑起来:

pm2 list

你会看到一张表,列出进程名、ID、状态(online)、CPU、内存、重启次数。这一步先确认应用真的起来了。但别急着就这样上线——命令行启动的参数不会保存,服务器重启后 PM2 不知道该跑什么。生产环境应该用配置文件。

用 ecosystem 配置文件固化启动参数

在项目目录下生成一份默认配置:

pm2 init simple

它会生成 ecosystem.config.js。手动整理成下面这样一份生产可用的模板:

module.exports = {
  apps: [{
    name: 'myapi',
    script: './app.js',
    cwd: '/var/www/myapi',
    instances: 'max',
    exec_mode: 'cluster',
    watch: false,
    max_memory_restart: '500M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    env_development: {
      NODE_ENV: 'development',
      PORT: 3001
    },
    error_file: '/var/log/pm2/myapi-error.log',
    out_file: '/var/log/pm2/myapi-out.log',
    merge_logs: true,
    time: true
  }]
};

几个关键字段解释一下:name 是进程名;script 是入口;cwd 是工作目录(相对路径的解析基准,写绝对路径最稳);instances 和 exec_mode 见下一节;max_memory_restart 是内存超过阈值自动重启,防止内存泄漏把机器拖死;env 是环境变量;merge_logs 让集群模式下所有实例的日志合并成一个文件,time 给每行日志加上时间戳,排障时非常有用。

用配置文件启动(或重载):

pm2 start ecosystem.config.js

集群模式:让 Node 吃满多核 CPU

Node.js 是单线程的,一个进程只能用到一个 CPU 核。你的 VPS 如果是 4 核,裸跑一个 Node 进程等于浪费了 3 个核。PM2 的 cluster 模式能根据核数自动拉起多个实例,并让它们共享同一个端口——PM2 内部做了负载均衡,请求被分发到各个实例。

设置 instances: 'max' 就是按 CPU 核数起满;也可以写具体数字如 instances: 4。exec_mode: 'cluster' 开启集群。改完 pm2 reload myapi 让它生效。

注意集群模式的隐含要求:多个实例之间不共享内存。这意味着如果有会话状态、定时任务、内存缓存,需要放到外部(Redis、数据库),否则会出现「用户请求打到实例 A 登录,打到实例 B 就掉线」的问题。定时任务尤其要注意:如果每个实例都跑 crontab,任务会被执行 N 次。定时任务应该单独用 instances: 1 的进程跑,不要放进集群。

日志管理:别让日志把磁盘写爆

PM2 默认把输出写进 ~/.pm2/logs/,默认不会自动轮转。一个跑久了的高流量服务,日志文件能涨到几十 GB,把磁盘撑满导致整站故障。两步解决:

第一,安装日志轮转模块:

pm2 install pm2-logrotate

第二,配置轮转策略:

pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'

这样日志单文件超过 50M 就切分,最多保留 7 份,自动压缩,每天零点额外轮转一次。设置好后不用重启应用,模块会立即生效。

平时看日志用:

pm2 logs myapi           # 实时跟踪
pm2 logs myapi --lines 200   # 看最近 200 行
pm2 flush                # 清空所有日志文件

开机自启:让服务在重启后自动回来

VPS 难免重启(内核升级、故障、计划维护),不配开机自启,重启后服务就静默消失了。PM2 有专门的命令生成 systemd 服务:

pm2 startup

它会打印一条需要你以 root 执行的命令(类似 sudo env PATH=$PATH:... pm2 startup systemd -u youruser --hp /home/youruser),把那条命令复制粘贴执行。这一步配置了开机时 PM2 自动拉起守护进程。

然后保存当前进程列表:

pm2 save

pm2 save 会把你当前正在跑的进程快照写到 ~/.pm2/dump.pm2,开机的 PM2 会读取这份快照恢复所有进程。以后每次增删了进程,都要重新执行一次 pm2 save,否则重启后新加的进程不会自动起来。这是新手最常忘的一步。

零停机重载:更新代码不打脸

代码改了要发布新版本,直接 restart 会有几秒的不可用窗口。集群模式下用 reload 可以逐个实例替换,做到零停机:

pm2 reload myapi

reload 会先启动新实例、等它 ready 之后再下线旧实例,用户完全无感知。非集群模式(fork)下 reload 和 restart 效果一样,会有短暂中断。所以想享受零停机,就要开集群模式。

配套的发布流程通常是:git pull 拉代码 → npm ci 装依赖 → pm2 reload myapi。如果你有多个应用,可以 pm2 reload all 一次性重载。

常用排障命令速查

掌握这几条,日常运维基本够用:

pm2 list / pm2 ls:查看所有进程状态、CPU、内存、重启次数。重启次数(restart 字段)不断上涨说明应用在反复崩溃,要去看日志。

pm2 show myapi:查看单个进程的详细元信息——启动路径、日志路径、运行时长、脚本、Node 版本等。

pm2 monit:实时监控面板,看 CPU 和内存曲线,CPU 飙高时能第一时间发现。

pm2 restart myapi:重启进程。pm2 stop myapi 停止但不删除。pm2 delete myapi 彻底移除。

pm2 describe 0:用 ID 查询(ID 是 pm2 list 里第一列的数字)。

进程状态显示 errored 或不断重启,第一件事就是 pm2 logs myapi --err 看错误输出。常见原因:端口被占用、依赖没装全、环境变量缺失、Node 版本不兼容。

配合 Nginx 做反向代理

PM2 管的是 Node 进程本身,它监听的是本地端口(比如 3000)。对外提供 HTTPS 访问还需要 Nginx 反向代理:

location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
}

这几行里,Upgrade 和 Connection 是为了支持 WebSocket;X-Real-IP / X-Forwarded-For 把真实访客 IP 透传给 Node,否则你的应用拿到的全是 127.0.0.1,日志和限流都会失效。Nginx 处理 HTTPS 证书、静态文件、gzip,Node 只专注业务逻辑,这是最经典的组合。

用配置文件管理多种运行环境

真实项目往往有开发、测试、生产三套环境,数据库地址、API 密钥、端口都不一样。硬编码在代码里最要命,稍不留神就把生产密钥提交到了 Git 仓库。PM2 的 ecosystem 配置天然支持多套环境变量:上面模板里的 env 块对应默认(生产)环境,env_development 对应开发环境。启动时用 --env 指定:

pm2 start ecosystem.config.js --env development

这样同一份代码在不同环境下读到不同的变量,代码里统一用 process.env.DB_HOST 这类读取方式。不过要注意,敏感密钥不要写进 ecosystem.config.js 再提交到仓库。更好的做法是把密钥放在服务器上的 .env 文件里(用 dotenv 加载),配置文件里只放非敏感的启动参数。环境的边界越清晰,线上事故越少。

健康检查与优雅退出

进程管理器再强,也救不了写得不健壮的应用。生产级 Node 服务有两个必备素质。第一是优雅退出:当 PM2 发送停止信号时,应用应该先停止接收新请求、把手头正在处理的请求做完、关闭数据库连接,然后再退出。在 Node 里监听 SIGINT 和 SIGTERM:

process.on('SIGTERM', () => {
  server.close(() => {
    // 关闭数据库连接池等资源
    process.exit(0);
  });
});

没有这段代码,PM2 重载时会直接杀掉进程,正在处理的用户请求被硬切断,表现为用户看到报错或请求超时。集群模式下逐个实例重载本来是为了零停机,如果应用不优雅退出,这个好处就大打折扣。

第二是健康检查端点。给应用加一个 /healthz 路由,返回 200 表示服务正常。pm2 reload 在集群模式下会等待新实例启动,但更精确的做法是配合外部监控(比如用 systemd 或一个简单的 curl 巡检脚本)定期打这个端点。一旦连续失败,就告警。这样你能在用户投诉之前就知道服务出了问题。

常见故障与对应排查

实践中反复出现的几类问题,记住对应关系能省很多时间。进程不断重启:看 pm2 logs --err,八成是端口被占用(EADDRINUSE)、依赖缺失(Cannot find module)或环境变量没传进去。内存持续上涨:典型的内存泄漏,观察 pm2 monit 的曲线,配合 max_memory_restart 先兜底,再逐步排查代码里未释放的监听器、全局数组、未取消的定时器。CPU 单核跑满:可能是有同步阻塞代码(比如大 JSON 的同步解析、死循环),检查是否真的开了集群模式、请求是否被均衡到了所有实例。日志文件暴涨:确认 pm2-logrotate 模块装好并生效。

还有一个容易踩的坑:用 root 账号启动 PM2,管的是 root 的进程列表;换成普通用户又看不到这些进程。PM2 的进程列表是跟用户绑定的(存在该用户的 ~/.pm2/)。团队协作或切换用户时,务必确认你用的是同一个账号,否则会出现「明明在跑但 pm2 list 是空的」这种困惑。

小结

PM2 把「守护 Node 进程」这件事做到了一键化:崩溃自动重启、`max_memory_restart` 兜底内存泄漏、集群模式吃满多核、`reload` 实现零停机发布、配合 logrotate 模块管好日志、`startup` + `save` 搞定开机自启。生产的标准姿势是:用 ecosystem.config.js 固化配置、集群模式启动、装好 logrotate、执行过 startup 和 save,最后交给 Nginx 反代。记住那条最容易忘的规则——每次增删进程后都要 pm2 save,否则重启后你的服务不会自己回来。

Last modification:October 11th, 2026 at 12:27 pm

Leave a Comment