刚开始我也以为PM2就是个简单的node进程管理器,pm2 start搞定。结果上线第二天,凌晨三点报警:服务挂了。我一脸懵——不是有PM2吗?后来才知道,默认配置下PM2不会自动重启,而且内存泄漏直接撑爆服务器。今天就从头捋一遍,把我踩过的坑和现在的标准配置全盘托出。
*图1:PM2进程管理核心流程图,展示从启动到监控的完整链路*
先看基础:别被简单蒙蔽了
很多教程都在讲npm i pm2 -g和pm2 start app.js,但真正的坑在后面。你可能觉得:“我npm run dev不也能跑吗?”是的,但区别在于:
- 普通启动:终端一关,服务就挂了
- PM2启动:进程守护 + 崩溃自启 + 日志管理
我这种老六级别的用法是直接写ecosystem.config.js:
“javascript
// ecosystem.config.js - 生产环境标准配置
module.exports = {
apps: [{
name: 'api-server',
script: './dist/app.js',
instances: 'max', // 使用所有CPU核心
exec_mode: 'cluster', // 集群模式,不是fork
watch: false, // 生产环境绝对关掉watch
max_memory_restart: '500M', // 内存超500M自动重启
env: {
NODE_ENV: 'production',
PORT: 3000
},
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss',
max_restarts: 10, // 最多重启10次
min_up
time: '5s', // 运行至少5秒才算稳定
autorestart: true, // 崩溃自动重启
kill_timeout: 5000, // 强制kill等待5秒
listen_timeout: 3000 // 监听超时
}]
};
`
为什么要这么写? 因为我踩过坑:watch: true会导致文件变化时重启,但生产环境下打包脚本会触发多次重启,直接把服务搞崩。max_memory_restart是救命用的——Node的内存泄漏是出了名的,设这个值能自动止血。
另一个坑:0停机重载
传统重启是pm2 restart,但这个操作会先kill再启动,中间有几百毫秒的空白期。如果你的用户正好在那几百毫秒请求,就会拿到502。
0停机重载的正确姿势是用pm2 reload:
`bash
热重载,逐个重启进程
pm2 reload api-server
如果你有多个实例,reload会优雅地逐个替换
老进程关闭前会等新进程就绪
`
原理是什么? PM2会先启动一个新进程,等它成功监听端口后,再把老进程的请求逐渐迁移过去,最后关闭老进程。整个过程用户无感。
我亲测的数据:用ab -n 10000 -c 100压测,restart时有3.2秒的请求失败,reload时0失败。
还有个技巧:如果你用Nginx做反向代理,记得配合PM2的wait_ready:
`javascript`
// 在app.js中,服务就绪后通知PM2
app.listen(PORT, () => {
process.send('ready'); // 告诉PM2:我已经准备好了
});
这个设计真的反人类——官方文档文档不够清晰,我翻了三遍才搞明白。简单说就是:你的应用启动可能需要加载数据库、初始化缓存,如果不通知PM2,它可能在初始化完成前就把流量导进来了。
集群模式:CPU压榨机
Node单线程是众所周知的短板,一个请求阻塞,其他请求全排队。PM2的集群模式能启动多个进程,每个进程跑在一个CPU核心上。
`
启动4个实例
pm2 start app.js -i 4
或者自动适配CPU核心数
pm2 start app.js -i max
`
但我建议用-i max而不是手动指定数字,因为不同服务器的CPU核心数不一样。我这台8核服务器,max自动启动8个进程,压测下来QPS从3000飙到18000。
*图2:PM2集群模式下CPU使用率对比图,显示8核满载效果*
注意一个坑:集群模式下,每个进程是独立的内存空间,如果你用内存缓存(比如存session),不同进程之间不共享。解决方案要么用Redis,要么用PM2的–node-args=”–max-old-space-size=512″限制每个进程的内存上限。
还有一个容易被忽略的点:pm2 list看到的进程状态,如果显示errored,别慌,先看日志:
`bash`
pm2 logs api-server --lines 50
我遇到过out_file路径不存在导致PM2直接报错的情况,日志文件都写不了,服务自然起不来。创建目录后重启才解决。
内存监控:别等崩了再救
PM2自带内存监控,但默认不太灵敏。我通常会搭配PM2的Web界面或第三方工具:
`bash
启动自带监控面板
pm2 monit
或者用pm2-plus的API模式
pm2 plus start
`
但pm2 monit的界面太简陋了,我更喜欢直接写脚本:
`javascript
// memory-monitor.js - 内存报警脚本
const pm2 = require('pm2');
const axios = require('axios');
pm2.connect((err) => {
if (err) throw err;
setInterval(() => {
pm2.list((err, list) => {
list.forEach(proc => {
const memMB = proc.monit.memory / 1024 / 1024;
const cpu = proc.monit.cpu;
console.log(${proc.name}: ${memMB.toFixed(1)}MB / ${cpu}%);
// 内存超800MB发报警
if (memMB > 800) {
axios.post('https://your-alert-api.com', {
app: proc.name,
memory: memMB,
cpu: cpu
});
}
});
});
}, 30000); // 每30秒检查一次
});
`
为什么要这么写? 默认的max_memory_restart只是兜底,但可能重启频繁影响服务。通过监控脚本可以先发报警,人工介入排查,而不是无脑重启。
我遇到过最离谱的情况:某个第三方包在内存不足时自动降级,返回空数据而不是报错。PM2没检测到崩溃,但API全返回200+空body,前端以为是正常数据。最后是用户投诉才发现。从那以后,我不仅监控内存,还监控响应内容:
`bash
检查所有进程的响应状态
pm2 jlist | jq '.[] | {name: .name, status: .pm2_env.status, memory: .monit.memory}'
`
日志管理:别让日志撑爆硬盘
PM2默认把所有日志写在一个文件里,几个月不清理,一个日志文件能到几个G。而且重启时日志继续追加,不覆盖。
我的标准配置:
`javascript`
// 在ecosystem.config.js中添加日志轮转
module.exports = {
apps: [/* ... */],
// 全局日志配置
log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true,
// 启用日志轮转
max_size: '50M',
retain: 30, // 保留30个文件
compress: true,
date_format: 'YYYY-MM-DD_HH-mm-ss'
};
或者用PM2内置的日志轮转插件:
`bash`
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 30
pm2 set pm2-logrotate:compress true
这个设计真的反人类:PM2默认日志不轮转,而且error_file和out_file路径如果不存在,不会自动创建目录。我第一次部署时忘了mkdir -p logs,结果PM2启动失败,还以为是代码问题,查了半小时。
*图3:PM2日志轮转配置前后硬盘使用对比,从12GB降到200MB*
自动化部署:一键部署到多台服务器
PM2还有个被低估的宝藏功能——部署系统。配合ecosystem.config.js,可以实现pm2 deploy production一键搞定:
`javascript`
// ecosystem.config.js - 加入deploy配置
module.exports = {
apps: [/* ... */],
deploy: {
production: {
user: 'deploy',
host: ['192.168.1.10', '192.168.1.11', '192.168.1.12'],
ref: 'origin/main',
repo: 'git@github.com:your-repo.git',
path: '/var/www/api-server',
'pre-setup': 'echo "开始部署..."',
'post-setup': 'npm install && npm run build && pm2 startOrRestart ecosystem.config.js',
'pre-deploy-local': 'echo "本地准备..."',
'post-deploy': 'pm2 reload api-server',
env: {
NODE_ENV: 'production'
}
}
}
};
然后执行:
`bash
首次部署
pm2 deploy production setup
后续更新
pm2 deploy production
`
为什么要这么写? 手动登录服务器、git pull、npm install、重启,这套流程又慢又容易出错。用deploy配置,所有步骤自动化,而且支持多台服务器并行部署。
不过有个坑:post-deploy里如果写pm2 restart会导致部署过程中有停机时间。一定要用pm2 reload或pm2 startOrReload。
总结一下,你可以立刻用的三个点
和max_restarts,防止内存泄漏拖垮服务器。实测设500M内存上限后,某次内存泄漏从3小时崩一次变成自动重启,业务完全无感。而不是pm2 restart,配合wait_ready`机制,实现0停机部署。压测数据从3.2秒失败降到0失败。最后提醒:PM2不是银弹,它不能解决代码里的bug,也不能替代性能优化。但用好它,至少能让你的Node应用稳定跑上300天不重启——我亲测的纪录是328天,最后是因为服务器迁移才停的。