在这里插入图片描述

1. 引言

你大概率遇到过这样的场景:写完一个 Node.js 服务,SSH 连上服务器,敲下 node app.js,一切正常。然后你关掉终端下班回家,第二天发现服务挂了——因为终端退出时,进程也跟着死了。

或者更糟的:凌晨三点服务因为一个未捕获的异常崩了,没人知道,直到用户投诉才慌忙重启。

这些问题的本质是一样的:直接用 node 命令启动的服务是"裸奔"的——没有守护、没有自愈、没有监控。就像让一个没有社保、没有医保的员工直接上岗,出了事只能靠天命。

PM2 就是来解决这个问题的。它负责一件很朴素的事:让你的 Node.js 应用始终在线,挂了就拉起来,日志帮你记好,还能充分利用多核 CPU。

读完这篇文章,你将掌握:

  • PM2 是什么以及它解决了什么问题
  • 用 PM2 启动、停止、重启、监控 Node.js 应用
  • 配置集群模式榨干多核性能
  • 让应用在服务器重启后自动启动
  • 用一个真实的 Express 项目完成从零到生产的部署


2. PM2 是什么

一句定义

PM2(Process Manager 2)是一个 Node.js 生产环境进程管理器。它由 Keymetrics 团队维护,在 GitHub 上有超过 40k Star,是 Node.js 生态中事实上的进程管理标准。

GitHub地址:
GitHub - Unitech/pm2: Node.js Production Process Manager with a built-in Load Balancer.

它本质上是一个跑在后台的守护进程(Daemon),帮你看着所有应用进程——谁挂了就重启谁,谁出错了就把日志记下来,谁来请求了就帮你在多个进程间分配。

PM2 的 7 大核心能力

能力 一句话说明
进程守护 应用崩溃自动重启,不用你半夜爬起来救火
负载均衡 启动多个进程实例,自动把请求分给不同实例
日志管理 自动收集 stdout/stderr,支持日志轮转防止硬盘写满
零停机重载 pm2 reload 逐个重启进程,更新代码时服务不中断
开机自启 服务器重启后自动拉起所有应用
实时监控 终端仪表盘,CPU、内存、请求量一目了然
配置文件驱动 一份 ecosystem.config.js 描述所有应用,适合团队和 CI/CD

PM2 适合谁

你不需要是一个运维老手才能用 PM2。只要你的工作涉及以下任意一种场景,PM2 就对你有用:

  • 在云服务器上部署 Node.js 后端服务
  • 跑一个 Nuxt/Next 的全栈应用
  • 管理多个微服务进程
  • 需要一个比 nohup & 靠谱的"后台运行"方案
  • 想在单台机器上充分利用多核 CPU

在这里插入图片描述

守护进程 PM2 Daemon 在中央,管理着多个应用进程(app1、app2…),收集日志到统一位置,通过 pm2 命令行工具进行交互


3. 安装与第一个应用

3.1 安装 PM2

PM2 是一个 npm 全局包,一行命令搞定:

npm install -g pm2

安装完成后验证一下:

pm2 --version
# 输出类似:5.4.2

3.2 准备一个测试应用

我们先写一个最简 HTTP 服务来玩。创建文件 app.js

const http = require('http');

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({
    message: 'Hello from PM2!',
    pid: process.pid,       // 进程ID,后面讲集群模式时会很有趣
    time: new Date().toISOString()
  }));
});

server.listen(3000, () => {
  console.log(`Server running on port 3000, PID: ${process.pid}`);
});

3.3 用 PM2 启动它

pm2 start app.js --name my-app

--name my-app 给应用取个名字,后续管理时比用数字 ID 方便得多。

启动后你应该看到类似输出:

[PM2] Starting /home/user/app.js in fork_mode (1 instance)
[PM2] Done.
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id  │ name     │ namespace   │ version │ mode    │ pid      │ uptime │ ↺    │ status    │ cpu      │ mem      │ user     │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0   │ my-app   │ default     │ 1.0.0   │ fork    │ 12345    │ 0s     │ 0    │ online    │ 0%       │ 25.3mb   │ user     │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘

如图我启动的server服务:

现在用浏览器或 curl 访问 http://localhost:3000,你会看到 JSON 响应。

更有意思的是:关掉终端再打开一个新终端,访问 http://localhost:3000,服务还在。 这就是 PM2 守护进程在起作用——它不是挂在你那个终端会话下面的,而是被 PM2 Daemon 接管了。

3.4 理解 pm2 list 的输出

pm2 list 随时查看所有应用的状态。关键列含义:

列名 含义
id PM2 内部的进程编号
name 你给应用起的名字
mode fork(单进程)或 cluster(集群模式)
pid 操作系统级别的进程 ID
status online(正常)/ stopped(停止)/ errored(异常)
重启次数——这个数字如果疯涨,说明代码有问题
uptime 运行时长,刚启动显示 0s
cpu / mem 当前 CPU 和内存占用

实用技巧:如果你看到某个应用的重启次数(↺)在快速上涨,说明它反复崩溃——赶紧去看日志。


4. 核心功能详解

4.1 进程守护与自动重启

这可能是 PM2 最重要的功能:崩溃自愈

试试故意让我们的应用崩溃。修改 app.js,加一个定时炸弹:

const http = require('http');

const server = http.createServer((req, res) => {
  // 每处理 5 个请求就模拟一次崩溃
  if (Math.random() < 0.2) {
    process.exit(1);  // 模拟致命错误
  }
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ status: 'ok' }));
});

server.listen(3000);

用 PM2 重启应用后,连续发几次请求:

pm2 restart my-app

# 连续发 10 个请求
for i in {1..10}; do curl -s http://localhost:3000; echo; done

你会发现前几个请求还能正常响应,随机到 process.exit(1) 时服务瞬间挂掉——但下一秒又好了。这就是 PM2 在检测到进程退出后立刻重新拉起。

pm2 list 看一眼, 列的数字涨了。

防止无限重启:如果代码写了死循环的 bug,PM2 不会无限重启下去。它默认的机制是:如果 15 秒内重启超过 15 次,就放弃,把进程标为 errored。你可以通过参数调整:

pm2 start app.js --max-restarts 5 --restart-delay 3000
# 最多重启5次,每次间隔至少3秒

4.2 常用管理命令速查

所有管理命令的入口都是 pm2 <action> <app-name-or-id>

# 启动
pm2 start app.js --name my-app          # 指定名称启动
pm2 start app.js -i 2 --name api        # 集群模式启动2个实例
pm2 start ecosystem.config.js           # 用配置文件启动(推荐)

# 查看
pm2 list              # 应用列表(最常用)
pm2 show my-app       # 单个应用详情:路径、日志位置、环境变量
pm2 monit             # 实时仪表盘——CPU、内存、请求量都在滚动

# 控制
pm2 stop my-app       # 停止(保留在 PM2 列表中)
pm2 restart my-app    # 重启
pm2 delete my-app     # 从 PM2 列表中彻底移除
pm2 reload my-app     # 零停机重载(逐个重启实例,集群模式下特别有用)

# 全局
pm2 kill              # 杀掉 PM2 Daemon 本身,所有应用全部停止
pm2 flush             # 清空所有日志
pm2 save              # 保存当前进程列表,下次 PM2 启动时自动恢复

restart vs reload 的区别很重要:restart 是直接全部杀掉再启动(会短暂中断),reload 是一个一个重启(服务不中断)。生产环境更新代码时,用 reload

pm2 monit 实时仪表盘如下

4.3 日志管理

PM2 自动接管了应用的标准输出(stdout)和标准错误(stderr),存到统一位置:

pm2 logs                # 实时查看所有应用日志(tail -f 的效果)
pm2 logs my-app         # 只看指定应用
pm2 logs --lines 200    # 看最近200行
pm2 logs --err          # 只看错误日志

日志文件默认位置在 ~/.pm2/logs/

~/.pm2/logs/
├── my-app-out.log      # 标准输出日志
├── my-app-error.log    # 错误日志
└── ...

日志轮转:线上跑久了日志文件会变得巨大。安装 PM2 日志轮转模块:

pm2 install pm2-logrotate

# 配置(可选)
pm2 set pm2-logrotate:max_size 10M     # 单个日志文件最大 10MB
pm2 set pm2-logrotate:retain 30        # 保留最近 30 个日志文件
pm2 set pm2-logrotate:compress true    # 压缩旧日志

一旦日志文件超过 max_size,PM2 会自动切分、归档,旧的会自动删除——不用担心硬盘被日志写满。

4.4 集群模式:榨干多核性能

Node.js 默认是单线程的,一个进程只能跑在一个 CPU 核上。如果你的服务器有 4 核,只开一个 Node 进程意味着另外 3 个核在摸鱼。

PM2 的 集群模式(Cluster Mode) 就是答案。它在底层使用了 Node.js 原生的 cluster 模块,自动 fork 出多个工作进程,并把请求分发给它们:

# 启动与 CPU 核数相等的进程
pm2 start app.js -i max

# 或者指定数量
pm2 start app.js -i 4

再看看 pm2 list,你会发现同一个应用出现了多个进程实例,mode 列显示 cluster

注意:集群模式要求应用是无状态的(或共享状态)。如果应用依赖内存中的本地变量(非数据库/Redis),不同请求打到不同的工作进程时会拿到不同的数据。生产环境建议用 Redis 或数据库来共享状态。

clusterfork 模式的对比:

维度 fork 模式 cluster 模式
进程数 1 个 N 个(通常等于 CPU 核数)
CPU 利用 单核 多核
负载均衡 PM2 内置
适用场景 开发调试、简单脚本、cron job 生产环境 HTTP 服务
端口 直接监听 多进程共享同一端口

4.5 开机自启

服务器总会因为各种原因重启——安全补丁、机房维护、或是电源闪断。你不可能每次重启都手动 SSH 上去 pm2 start 一遍。

PM2 的开机自启方案很优雅,一条命令搞定:

# 1. 生成并配置启动脚本(会根据你的系统是 systemd/upstart/launchd 自动适配)
pm2 startup

# 上面命令会输出一条 sudo 命令,复制执行它
# 示例输出:sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u youruser --hp /home/youruser
# 2. 把当前运行的应用列表保存为"需要开机恢复"的快照
pm2 save

此后每次服务器重启,PM2 会自动启动,然后根据 pm2 save 存的快照把所有应用拉起来。

验证是否配置成功(以 systemd 为例):

systemctl status pm2-$(whoami)
# 或
systemctl status pm2-root

5. 配置文件生态(Ecosystem File)

5.1 为什么需要配置文件

前面所有操作都靠命令行传参。这在探索阶段没问题,但生产环境不行:

  • 不可复现:换个服务器还得重新敲一遍命令,忘了某个参数服务就跑得不对
  • 团队协作差:同事不知道你启动时传了什么参数
  • CI/CD 不友好:自动化脚本拼命令行参数又丑又脆
  • 多环境无法管理:开发、测试、生产环境参数不同,命令行根本管不过来

PM2 的答案是 Ecosystem File——一个 JavaScript 配置文件,完整描述你的所有应用和运行参数。

5.2 生成模板

pm2 ecosystem

这条命令会在当前目录生成一个 ecosystem.config.js 模板文件。PM2 5.x 默认生成 ESM 格式(export default),如果你需要旧的 CommonJS 格式(module.exports),用:

pm2 ecosystem --cjs

5.3 配置文件结构详解

下面是一个生产级的 ecosystem.config.js 示例,部署一个 Express API 服务:

module.exports = {
  apps: [
    {
      // ---- 基本 ----
      name: 'api-server',                // 应用名称,pm2 list 中显示
      script: './src/server.js',         // 入口文件
      cwd: '/var/www/api-server',        // 工作目录

      // ---- 集群 ----
      instances: 'max',                  // 'max' = CPU核数,或指定数字如 2
      exec_mode: 'cluster',              // 'fork' 或 'cluster'

      // ---- 环境变量 ----
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 8080,
      },
      env_staging: {
        NODE_ENV: 'staging',
        PORT: 3001,
      },

      // ---- 日志 ----
      error_file: './logs/err.log',      // 错误日志路径
      out_file: './logs/out.log',        // 输出日志路径
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',  // 日志时间戳格式
      merge_logs: true,                  // 集群模式下合并所有实例的日志

      // ---- 重启策略 ----
      max_restarts: 10,                  // 最多重启次数
      restart_delay: 2000,              // 两次重启的最小间隔(ms)
      min_uptime: '10s',                // 至少运行10秒才算启动成功
                                       // ——防止有bug的代码在无限快速重启循环
      max_memory_restart: '500M',       // 内存超过500M自动重启

      // ---- 更新 ----
      watch: false,                      // 生产环境关掉文件监听
      ignore_watch: ['node_modules', 'logs'],

      // ---- 其他 ----
      autorestart: true,                 // 崩溃自动重启
      kill_timeout: 5000,                // 强杀前的等待时间(ms)
    }
  ],

  // 部署配置(可选,用于 pm2 deploy)
  deploy: {
    production: {
      user: 'deploy',
      host: '192.168.1.100',
      ref: 'origin/main',
      repo: 'git@github.com:user/api-server.git',
      path: '/var/www/production',
      'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
    },
  },
};

5.4 用配置文件启动

# 默认环境(env)
pm2 start ecosystem.config.js

# 指定生产环境(使用 env_production)
pm2 start ecosystem.config.js --env production

# 重载生产环境(零停机更新!)
pm2 reload ecosystem.config.js --env production

有了配置文件,整条部署链路就通顺了:拉代码 → 装依赖 → pm2 reload ——一行搞定,服务不中断。


6. 实战:部署一个 Express 应用

让我们把前面学的串起来,完成一次完整的 PM2 部署流程。

6.1 项目准备

项目目录结构:

express-api/
├── src/
│   └── server.js
├── package.json
└── ecosystem.config.js

server.js——一个简单的 Express API,返回当前时间和服务的进程 PID:

const express = require('express');
const app = express();

// 一个简单的监控端点
app.get('/health', (req, res) => {
  res.json({ status: 'ok' });
});

// 主业务接口
app.get('/api/time', (req, res) => {
  res.json({
    time: new Date().toISOString(),
    pid: process.pid,      // 可以看到请求被分发到了哪个工作进程
    env: process.env.NODE_ENV || 'development',
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server started on port ${PORT}, PID: ${process.pid}`);
});

ecosystem.config.js

module.exports = {
  apps: [{
    name: 'express-api',
    script: './src/server.js',
    instances: 2,                     // 启动2个实例
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development',
      PORT: 3000,
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 8080,
    },
    max_restarts: 5,
    min_uptime: '5s',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss',
    merge_logs: true,
  }],
};

6.2 启动与验证

# 安装依赖
npm install

# 用 PM2 启动
pm2 start ecosystem.config.js

# 查看状态——应该看到两个 online 的进程
pm2 list

验证服务:

# 多请求几次,观察 pid 的变化——你会发现 pid 在两个值之间轮换
curl http://localhost:3000/api/time
curl http://localhost:3000/api/time
curl http://localhost:3000/api/time

这说明 PM2 的负载均衡在正常工作——请求被轮流分发给两个工作进程。

6.3 模拟崩溃与自愈

# 查看当前两个进程的 PID
pm2 list

# 手动杀掉其中一个进程(kill 它的 pid)
kill <pid>

# 再看一眼——被杀的那个进程 pid 变了(被重启了),↺ 次数 +1
pm2 list

6.4 零停机更新

假设你改了代码,需要更新线上服务:

git pull
npm install
pm2 reload ecosystem.config.js --env production

reload 会逐个重启实例:先起一个新实例,等它就绪后,再杀掉一个旧实例,循环直到全部替换。整个过程中,剩余实例继续处理请求,用户无感知。


7. 常见问题与踩坑

Q1:pm2 start 后提示端口被占用?

大概率是上一个 PM2 实例已经运行了但你没注意到。先用 pm2 list 确认,不要同时用 nodepm2 启动同一端口的服务。

如果确认端口被其他程序占用:

# Windows
netstat -ano | findstr :3000

# Linux / macOS
lsof -i :3000

Q2:环境变量不生效?

两个常见陷阱:

  1. 写错位置了env_xxxxxx 需要和 --env xxx 精确匹配。env_production 对应 --env production,不是 --env prod
  2. app.jsconsole.log(process.env) 没看到变量。在 ecosystem 中环境变量只对入口脚本及其子进程生效,你在 ecosystem 外面先 exportpm2 start 是无效的。

正确姿势是先在配置文件的 env 中声明,然后用 --env 指定环境。

Q3:升级 Node.js 后 PM2 应用挂了?

PM2 保存的是 Node.js 的绝对路径。你升级 Node.js(比如用 nvm 切版本),路径变了,PM2 还在找旧的 Node 二进制文件,自然启动不了。

解决方案:

# 先删掉旧进程
pm2 delete all

# 重新生成启动脚本
pm2 unstartup        # 清理旧的
pm2 startup           # 用新 Node 路径重新生成

# 重新启动应用
pm2 start ecosystem.config.js
pm2 save

Q4:在 Docker 容器里还要不要用 PM2?

这是个经典问题,答案取决于场景:

场景 建议
单容器单进程 不用 PM2,让 Docker 本身做进程管理和重启(--restart always
单容器需要多核利用 用 PM2 cluster 模式,在容器内 fork 多进程
单容器跑多个不同服务 可以但不推荐——Docker 哲学是一容器一进程

如果决定在容器里用 PM2,记得以 pm2-runtime(而不是 pm2)作为入口,它专为容器设计,不会后台化:

CMD ["pm2-runtime", "start", "ecosystem.config.js"]

一个判断标准:如果你需要在一个容器里跑多个 Node 实例来利用多核,PM2 cluster 是正确的选择。如果只需要单进程,Docker 自带的 restart 策略就够了。


8. 总结

PM2 的核心价值可以浓缩为四个字:守护、集群、日志、自启

能力 一命令速查
启动并守护 pm2 start app.js --name xxx
查看状态 pm2 list / pm2 monit
集群多核 pm2 start app.js -i max
查看日志 pm2 logs xxx
零停机更新 pm2 reload xxx
开机自启 pm2 startuppm2 save
配置文件 pm2 start ecosystem.config.js --env production

参考

PM2 - 主页 - PM2 进程管理器

https://juejin.cn/post/7444450350190346278

https://pm2.fenxianglu.cn/docs/start

更多推荐