进程管理与守护
What — 是什么
进程管理与守护是 Node.js 生产部署的核心环节,通过进程管理器(如 PM2)实现自动重启、集群模式、日志管理、优雅关闭等能力,确保应用高可用稳定运行。
核心概念:
- PM2:Node.js 最流行的进程管理器,提供守护进程、集群模式、日志管理、监控、部署等功能
- 守护进程(Daemon):在后台持续运行的进程,不受终端关闭影响,PM2 默认以守护模式运行
- 优雅重启(Graceful Restart):先启动新进程处理新请求,待旧进程完成已有请求后再关闭,实现零停机
- 集群模式(Cluster Mode):PM2 自动创建多个工作进程(等于 CPU 核数),利用 Node.js Cluster 模块实现负载均衡
- ecosystem.config.js:PM2 的声明式配置文件,定义应用名称、脚本路径、环境变量、实例数等
- 信号处理:
SIGTERM(优雅终止)、SIGINT(Ctrl+C 中断)、SIGHUP(配置重载)、SIGUSR2(PM2 重载) - Graceful Shutdown:应用收到终止信号后,停止接收新请求、完成已有请求、关闭数据库连接、退出进程
关键特性:
- PM2 自动重启崩溃进程,可配置最大重启次数和间隔
pm2 reload实现零停机重启(逐个重启集群中的工作进程)- 内置日志轮转(
pm2-logrotate),按大小/日期分割日志 pm2 monit实时监控 CPU/内存/请求数- 支持 Docker/Systemd 集成
Why — 为什么
适用场景:
- 生产部署:确保应用崩溃后自动恢复
- 高可用服务:集群模式利用多核,提升吞吐量
- 日志管理:集中收集和轮转应用日志
- 零停机部署:
reload替代restart,用户无感知 - 多应用管理:同一服务器运行多个 Node.js 服务
对比进程管理方案:
| 维度 | PM2 | Systemd | Docker | Forever |
|---|---|---|---|---|
| 上手难度 | 低(一条命令启动) | 中(需写 unit 文件) | 中(需 Dockerfile) | 低 |
| 集群模式 | 内置(-i max) | 不支持(需手动) | 不支持(需编排) | 不支持 |
| 零停机重启 | 支持(reload) | 支持(socket activation) | 支持(滚动更新) | 不支持 |
| 日志管理 | 内置轮转 | journald | Docker logs | 基础 |
| 监控 | 内置 monit | systemctl status | Docker stats | 无 |
| 适用场景 | Node.js 专用 | 通用系统服务 | 容器化部署 | 简单守护 |
优缺点:
- ✅ 优点:
- 专为 Node.js 设计,开箱即用
- 集群模式一条命令开启多核利用
- 零停机 reload 保证服务连续性
- 内置日志管理和进程监控
- 丰富的生态系统(logrotate/metrics/god)
- ❌ 缺点:
- 额外的进程开销(PM2 主进程占用内存)
- 集群模式下进程间不共享内存(需 Redis 等外部存储)
- Docker 环境中 PM2 集群模式不推荐(容器本身做扩缩容)
- 诊断问题有时需要查看 PM2 日志而非应用日志
How — 怎么用
安装配置
# 全局安装 PM2
npm install -g pm2
# 启动应用
pm2 start app.js --name my-api
# 常用命令
pm2 list # 查看所有进程
pm2 logs my-api # 查看日志
pm2 monit # 实时监控
pm2 restart my-api # 重启
pm2 reload my-api # 零停机重启
pm2 stop my-api # 停止
pm2 delete my-api # 删除
pm2 describe my-api # 查看详情
pm2 flush my-api # 清空日志
快速上手
# 单实例启动
pm2 start app.js --name my-api
# 集群模式启动(利用所有 CPU 核心)
pm2 start app.js -i max --name my-api
# 带环境变量启动
pm2 start app.js --name my-api --env production
# 保存进程列表(开机自启)
pm2 save
pm2 startup # 生成开机自启脚本
代码示例
ecosystem.config.js 配置:
module.exports = {
apps: [
{
name: 'api-server', // 应用名称
script: './dist/index.js', // 入口文件
instances: 'max', // 集群实例数(max = CPU核数)
exec_mode: 'cluster', // 执行模式:cluster / fork
watch: false, // 生产环境关闭 watch
max_memory_restart: '1G', // 内存超过 1G 自动重启
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
env_staging: {
NODE_ENV: 'staging',
PORT: 3001
},
// 日志配置
error_file: './logs/error.log',
out_file: './logs/out.log',
merge_logs: true, // 集群模式合并日志
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// 重启策略
max_restarts: 10, // 最大重启次数
restart_delay: 4000, // 重启间隔(ms)
autorestart: true, // 崩溃自动重启
// 优雅关闭
kill_timeout: 5000, // 强制杀掉前等待时间
listen_timeout: 10000, // 等待应用 ready 的时间
// 高级配置
instance_var: 'INSTANCE_ID', // 实例 ID 环境变量
increment_var: 'PORT' // 端口递增(每个实例+1)
},
{
name: 'worker',
script: './dist/worker.js',
instances: 2,
exec_mode: 'fork',
cron_restart: '0 3 * * *', // 每天凌晨3点定时重启
max_memory_restart: '512M'
}
],
// 部署配置
deploy: {
production: {
user: 'deploy',
host: 'api.example.com',
ref: 'origin/main',
repo: 'git@github.com:org/api.git',
path: '/var/www/api',
'pre-deploy-local': '',
'post-deploy': 'npm install && npm run build && pm2 reload ecosystem.config.js --env production',
'pre-setup': ''
}
}
};
// 使用
// pm2 start ecosystem.config.js --env production
// pm2 reload ecosystem.config.js --env production
优雅关闭实现:
const http = require('http');
const app = require('./app');
const server = http.createServer(app);
const PORT = process.env.PORT || 3000;
// 数据库连接等资源
let dbConnection;
async function startServer() {
dbConnection = await connectDatabase();
server.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
}
// 优雅关闭
let isShuttingDown = false;
async function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;
console.log(`\n${signal} received, shutting down gracefully...`);
// 1. 停止接受新连接
server.close(() => {
console.log('HTTP server closed');
});
// 2. 设置强制退出超时
const forceExit = setTimeout(() => {
console.error('Forcing exit after timeout');
process.exit(1);
}, 10000);
forceExit.unref(); // 不阻止进程退出
// 3. 关闭数据库连接
try {
if (dbConnection) {
await dbConnection.close();
console.log('Database connection closed');
}
} catch (err) {
console.error('Error closing database:', err);
}
// 4. 关闭 Redis 连接
try {
if (redisClient) {
await redisClient.quit();
console.log('Redis connection closed');
}
} catch (err) {
console.error('Error closing Redis:', err);
}
console.log('Graceful shutdown completed');
process.exit(0);
}
// 监听信号
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
// 未捕获异常
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
gracefulShutdown('uncaughtException');
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled Rejection:', reason);
gracefulShutdown('unhandledRejection');
});
startServer();
PM2 集群模式与 Systemd 集成:
# PM2 集群模式(单机多核)
pm2 start app.js -i 4 --name api # 4 个实例
pm2 reload api # 零停机重启
# Systemd 服务配置(替代 PM2)
# /etc/systemd/system/my-api.service
[Unit]
Description=My Node.js API
After=network.target
[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/api
ExecStart=/usr/bin/node /var/www/api/dist/index.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-api
# 环境变量
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/var/www/api/.env
# 资源限制
LimitNOFILE=65536
TimeoutStopSec=30
# 安全配置
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/www/api/logs
[Install]
WantedBy=multi-user.target
# Systemd 常用命令
sudo systemctl daemon-reload
sudo systemctl enable my-api # 开机自启
sudo systemctl start my-api
sudo systemctl status my-api
sudo journalctl -u my-api -f # 查看日志
sudo systemctl restart my-api
性能调优
| 参数 | 默认值 | 调优建议 | 说明 |
|---|---|---|---|
| instances | 1 | CPU 核数(max) | 集群模式实例数 |
| max_memory_restart | 无 | 物理内存 70% / 实例数 | 内存超限自动重启 |
| kill_timeout | 1600ms | 5000-10000ms | 等待优雅关闭的时间 |
| listen_timeout | 3000ms | 10000ms | 等待应用 ready 的时间 |
| restart_delay | 0 | 4000ms | 崩溃重启间隔,防止频繁重启 |
| max_restarts | 16 | 10-15 | 最大重启次数,超限停止 |
| node_args | 无 | --max-old-space-size=1024 | Node.js 内存限制 |
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
| PM2 重启后环境变量丢失 | PM2 保存的是启动命令,不含环境变量 | 用 ecosystem.config.js 的 env 字段 |
| 集群模式 Session 不共享 | 多进程内存独立 | Session 存 Redis,用 @fastify/session 等 |
| reload 后旧进程不退出 | 优雅关闭超时 | 增大 kill_timeout,确保 SIGTERM 处理完整 |
| 日志文件过大 | 未配置轮转 | 安装 pm2-logrotate,设置大小和保留份数 |
| Docker 中用 PM2 集群 | 容器应单进程,编排由 K8s/Docker Compose 做 | Docker 中用 fork 模式或直接 node 运行 |
pm2 save 后重启丢失 | 未执行 pm2 startup | 运行 pm2 startup 生成开机自启脚本 |
| 文件描述符耗尽 | 大量连接未关闭 | LimitNOFILE=65536,检查连接泄漏 |
| 僵尸进程 | 强制 kill 后 PM2 未清理 | pm2 delete 清理后重新启动 |
最佳实践
- 使用
ecosystem.config.js管理配置,不依赖命令行参数 - 生产环境始终实现
SIGTERM优雅关闭处理 - 集群模式配合 Redis 存储共享状态(Session/缓存)
- 安装
pm2-logrotate防止日志文件无限增长 - 用
pm2 reload替代pm2 restart,实现零停机 - Docker 环境中不用 PM2 集群模式,用多容器编排
- 监控进程内存和重启次数,异常频繁重启需排查
pm2 save+pm2 startup确保开机自启
面试题
Q1: PM2 集群模式的原理是什么?
PM2 集群模式基于 Node.js 的
cluster模块。PM2 主进程(God Daemon)创建多个工作进程(Worker),每个工作进程是独立的 Node.js 实例,监听同一端口。操作系统通过SO_REUSEPORT或主进程的 Round-Robin 算法将连接分发给工作进程。工作进程间内存独立,不共享状态。崩溃的工作进程会被 PM2 主进程自动重启。注意:集群模式不适合有大量共享内存需求的场景。
Q2: 优雅关闭(Graceful Shutdown)如何实现?为什么重要?
实现步骤:① 监听
SIGTERM/SIGINT信号;② 调用server.close()停止接受新连接(已有连接继续处理);③ 关闭数据库连接、Redis 连接等资源;④ 设置超时强制退出(防止永远无法关闭);⑤process.exit(0)退出。重要性:如果不优雅关闭,正在处理的请求会被强制中断——数据库事务可能半提交、客户端收到连接重置、定时任务可能执行到一半。K8s/Docker 发送 SIGTERM 后有宽限期(默认 30s),应用必须在此期间完成清理。
Q3: PM2 和 Systemd 如何选型?
PM2 适合:Node.js 专用场景、需要集群模式、快速部署、多应用管理、开发环境。Systemd 适合:Linux 服务器标准服务管理、需要系统级依赖控制、与操作系统深度集成、安全沙箱需求、团队有运维经验。选型建议:简单 Node.js 部署用 PM2 快速上手;企业级生产环境推荐 Systemd + 健康检查;容器化环境(Docker/K8s)不需要 PM2,用容器编排替代。也可以混合使用:Systemd 管理 PM2 进程。
Q4: 进程信号 SIGTERM、SIGINT、SIGHUP、SIGUSR2 的区别?
SIGTERM(15):优雅终止信号,应用应清理资源后退出。K8s/Docker/PM2 reload 发送此信号。SIGINT(2):中断信号,通常由 Ctrl+C 触发,默认行为是终止进程。SIGHUP(1):挂起信号,终端关闭时发送。常被重载为”重新加载配置”(如 Nginx)。SIGUSR1/SIGUSR2:用户自定义信号。Node.js 中SIGUSR1触发调试器,PM2 用SIGUSR2触发日志重载。生产应用应处理 SIGTERM 实现优雅关闭。
Q5: ecosystem.config.js 的关键配置项有哪些?
核心配置:①
name——进程标识;②script——入口文件路径;③instances——实例数(数字或'max');④exec_mode——'cluster'(集群)或'fork'(单进程);⑤env/env_production——环境变量;⑥max_memory_restart——内存超限自动重启;⑦error_file/out_file——日志路径;⑧merge_logs——集群模式合并日志;⑨kill_timeout——优雅关闭超时;⑩max_restarts——最大重启次数;⑪cron_restart——定时重启(cron 表达式);⑫watch——文件变更自动重启(开发用)。
Q6: PM2 日志管理策略有哪些?
内置能力:
error_file(错误日志)和out_file(标准输出)分别存储;merge_logs: true集群模式合并;log_date_format添加时间戳。日志轮转:安装pm2-logrotate模块,配置--max-size(默认 10MB 分割)、--retain(保留份数,默认 30)、--compress(压缩旧日志)、--dateFormat(文件名日期格式)。结构化日志:应用中使用 pino/winston 输出 JSON 日志,配合 ELK/Loki 等日志系统检索分析。
Q7: 零停机重启的原理是什么?
PM2 的
reload命令实现零停机重启:① 在集群模式下,PM2 逐个重启工作进程;② 先启动一个新的工作进程,等待其listening事件(表示已准备好接收请求);③ 然后向旧工作进程发送SIGTERM,等待其优雅关闭;④ 旧进程关闭后,所有请求由新进程处理;⑤ 重复此过程直到所有工作进程都被替换。关键:始终有一部分工作进程在运行处理请求,因此不会出现服务中断。restart命令则会同时杀掉所有进程再启动,会有短暂停机。
Q8: 生产环境进程监控方案有哪些?
三种方案:① PM2 Plus(原 Keymetrics)——PM2 官方 SaaS 监控平台,提供 CPU/内存/事件循环延迟/HTTP 延迟/错误率等指标,有告警功能;② Prometheus + Grafana——
pm2-metrics暴露 Prometheus 指标端点,Grafana 展示仪表盘,适合已有 Prometheus 体系的项目;③ 自建监控——使用pm2.io的@pm2/io库自定义指标(请求计数/延迟/队列长度),推送到自建监控。推荐 PM2 Plus 快速上手,大型项目用 Prometheus 体系统一管理。
相关链接: