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 savepm2 save 会把你当前正在跑的进程快照写到 ~/.pm2/dump.pm2,开机的 PM2 会读取这份快照恢复所有进程。以后每次增删了进程,都要重新执行一次 pm2 save,否则重启后新加的进程不会自动起来。这是新手最常忘的一步。
零停机重载:更新代码不打脸
代码改了要发布新版本,直接 restart 会有几秒的不可用窗口。集群模式下用 reload 可以逐个实例替换,做到零停机:
pm2 reload myapireload 会先启动新实例、等它 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,否则重启后你的服务不会自己回来。