进程管理与守护

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 服务

对比进程管理方案:

维度PM2SystemdDockerForever
上手难度低(一条命令启动)中(需写 unit 文件)中(需 Dockerfile)
集群模式内置(-i max不支持(需手动)不支持(需编排)不支持
零停机重启支持(reload)支持(socket activation)支持(滚动更新)不支持
日志管理内置轮转journaldDocker logs基础
监控内置 monitsystemctl statusDocker 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

性能调优

参数默认值调优建议说明
instances1CPU 核数(max)集群模式实例数
max_memory_restart物理内存 70% / 实例数内存超限自动重启
kill_timeout1600ms5000-10000ms等待优雅关闭的时间
listen_timeout3000ms10000ms等待应用 ready 的时间
restart_delay04000ms崩溃重启间隔,防止频繁重启
max_restarts1610-15最大重启次数,超限停止
node_args--max-old-space-size=1024Node.js 内存限制

常见问题与踩坑

问题原因解决方案
PM2 重启后环境变量丢失PM2 保存的是启动命令,不含环境变量ecosystem.config.jsenv 字段
集群模式 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 体系统一管理。


相关链接: