跳到主要内容

PM2

2024年12月10日
柏拉文
越努力,越幸运

一、认识


PM2 是一个功能强大的 Node.js 进程管理工具,可以实现 Node.js 服务的 守护进程进程管理负载均衡日志管理 等。在实际应用中,PM2 主要用于:

  1. 守护进程PM2 确保 Node 服务在意外退出时自动重启,保证服务的稳定性和高可用性。

  2. 多进程管理PM2 可以通过集群模式(Cluster Mode)启动多个进程,提高服务的并发能力,充分利用多核 CPU

  3. 进程管理:提供启动、重启、停止、删除进程的操作,并支持进程列表查询、资源使用监控。

  4. 日志管理:自动收集标准输出(stdout)和错误输出(stderr)的日志,支持日志文件管理、分割。

  5. 部署与环境管理:支持配置文件(ecosystem.config.js),可以通过环境变量来管理多个环境(如生产环境、开发环境)。

1.1 PM2 工作机制

PM2 的核心工作机制是基于 进程管理守护进程 的实现,主要包括以下几个方面:

一、守护进程(Daemon Process: PM2 通过启动一个独立的 守护进程(Daemon 来管理所有的 应用进程。守护进程常驻后台,负责 监控重启管理 所有子进程。当执行 pm2 start app.js 命令时,PM2 会首先检查守护进程是否存在,如果不存在,则启动守护进程。PM2 CLI 将用户的启动指令通过 IPC 通信 发送给 守护进程。守护进程收到指令后,通过 child_process.fork 启动一个新的子进程运行 app.js守护进程用户应用进程 分离,保证应用进程异常退出后,守护进程可以检测到并自动重启。守护进程 会保存应用进程的状态到文件中(~/.pm2/dump.pm2),即使服务器重启也能恢复进程状态。

const { spawn } = require('child_process');
spawn('pm2 daemon', { detached: true });

二、PM2 的两种进程启动模式:

2.1 Fork 模式(单进程模式): PM2 使用 child_process.fork 启动一个子进程

const { fork } = require('child_process');
const child = fork('app.js', { silent: true });

2.2 Cluster 模式(多进程模式): 适合多核 CPU 的场景。PM2 支持 Cluster 模式,利用 Node.js 提供的 cluster 模块实现多进程并发。PM2 会启动一个 Master 进程,再 fork 出多个 Worker 子进程(基于 IPC 机制与 Master 通信),每个子进程都会运行同一个服务脚本(app.js), 所有子进程共享同一个端口。主进程通过 Round-Robin(轮询)调度算法实现将请求分发到不同的子进程中,实现负载均衡。利用多核 CPU,提高服务的性能和并发能力。通过 IPC 机制MasterWorker 之间传递消息。

const cluster = require('cluster');
const os = require('os');
const numCPUs = os.cpus().length;

if (cluster.isMaster) {
for (let i = 0; i < numCPUs; i++) cluster.fork();
} else {
require('./app'); // 启动服务
}

三、自动故障恢复与重启机制: PM2 内置故障检测机制,当应用进程崩溃或退出时,守护进程会自动重启它。确保服务持续运行,减少人工干预。可以通过 --max-memory-restart 限制进程内存使用,超过阈值后自动重启,防止内存泄漏。具体实现为: 守护进程监听子进程的退出事件, 实现如下

const { fork } = require('child_process');
const child = fork('app.js');

child.on('exit', () => {
console.log('Child process exited, restarting...');
fork('app.js'); // 自动重启
});

四、日志管理与监控: PM2 自动捕获应用的标准输出(stdout)和错误输出(stderr),并将日志保存在文件中。PM2 提供 pm2 monit 命令监控 CPU、内存等资源使用情况。与外部监控工具集成(如 PM2 PlusPrometheus),实现自定义监控和告警。

五、文件监听与热重载: PM2 支持 文件变更监听,在代码变更后自动重启服务,提升开发效率。具体实现为: 使用 chokidar 库监听文件变动,当检测到变更时,重启进程

const chokidar = require('chokidar');
chokidar.watch('./').on('change', () => {
process.exit(0); // 触发重启
});

六、PM2 的设计思想: 主要围绕 高可用性易维护性

6.1 守护进程设计:通过守护进程保证服务稳定运行,实现自动恢复。

6.2 多进程架构:通过 ForkCluster 模式充分利用多核 CPU

6.3 进程间通信(IPC:解耦 CLI 与守护进程,便于管理。

6.3.1 child_process IPC 通信: Node.js 提供了 child_process 模块来创建子进程,并允许父进程和子进程进行消息传递。这是 Node.js 中最常见的 IPC 机制:父进程使用 fork() 创建了一个子进程,并通过 send() 向子进程发送一个消息。子进程接收到消息后,通过 process.on('message') 监听消息,并在处理完消息后通过 process.send() 向父进程发送回复。父进程通过 on('message') 监听子进程返回的消息。

父进程代码

const { fork } = require('child_process');

// 创建子进程,执行子进程脚本
const child = fork('child.js');

// 监听子进程发送的消息
child.on('message', (msg) => {
console.log('父进程接收到消息:', msg);
});

// 向子进程发送消息
child.send({ from: '父进程', data: 'Hello, 子进程!' });

子进程代码

// 监听父进程发送的消息
process.on('message', (msg) => {
console.log('子进程接收到消息:', msg);

// 向父进程发送回复消息
process.send({ from: '子进程', data: 'Hello, 父进程!' });
});

6.3.2 spawn() IPC 通信机制: spawn() 方法用于创建子进程,它不会直接通过 IPC 发送消息,而是通过 标准输入(stdin)、标准输出(stdout) 和 标准错误输出(stderr) 进行数据交换。通常用于启动和控制外部进程。父进程通过 spawn() 启动一个 Node.js 子进程,并通过标准输入(stdin.write())发送数据。子进程接收到输入后,打印并将结果通过标准输出(stdout.write())发送回父进程。父进程监听标准输出,接收来自子进程的返回数据。

父进程代码

const { spawn } = require('child_process');

// 启动子进程
const child = spawn('node', ['child.js']);

// 向子进程写入数据
child.stdin.write('Hello from parent!\n');

// 从子进程读取输出
child.stdout.on('data', (data) => {
console.log(`从子进程接收到的数据: ${data}`);
});

子进程代码

process.stdin.on('data', (data) => {
console.log(`子进程接收到数据: ${data}`);
process.stdout.write('Hello from child!\n');
});

6.3.3 cluster IPC 多进程通信机制: cluster 模块使得 Node.js 可以利用多核 CPU,进行多进程处理。每个工作进程都可以通过 IPC 通道与主进程通信。在 cluster 模块中,主进程通过 fork() 创建工作进程,每个工作进程都会通过 process.send() 向主进程发送消息。进程通过 cluster.on('message') 监听来自工作进程的消息。

父进程代码

const cluster = require('cluster');
const http = require('http');

if (cluster.isMaster) {
// 主进程创建工作进程
cluster.fork();

// 监听工作进程发送的消息
cluster.on('message', (worker, message) => {
console.log('主进程接收到消息:', message);
});
} else {
// 工作进程发送消息给主进程
process.send({ msg: 'Hello from worker!' });

// 模拟工作进程的 HTTP 服务器
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello, World!');
}).listen(8000);
}

6.4 Round-Robin(轮询)调度算法: 在 Cluster 模式下, 主进程通过 Round-Robin(轮询)调度算法实现将请求分发到不同的子进程中,实现负载均衡。Round-Robin 调度算法 是一种 公平分配任务 的调度方法,它按照顺序(轮询)将任务分配给可用资源,确保每个资源接收到的任务数量大致相等,不会出现资源长期空闲或过载的情况, 每个资源按顺序被依次调度,不偏向任何一方, 适用于任务轻量、处理时间较短、资源处理能力相对均衡的场景。Round-Robin 调度算法 的核心原理为: 维护一个资源列表(例如服务器、任务队列等),并从头到尾顺序分配任务。当资源分配到最后一个时,调度器会回到列表的第一个资源,继续分配任务。调度器通常需要维护一个索引(或指针),记录上一次分配到哪个资源,下次调度时从下一个资源开始。简单来说: 定义一个资源列表,使用一个指针记录当前调度到哪个资源,每次分配任务时,更新指针,循环访问资源列表。

class RoundRobinScheduler {
constructor(servers) {
this.servers = servers; // 资源列表,如服务器
this.currentIndex = -1; // 当前分配的索引位置
}

// 获取下一个资源
getNextServer() {
// 更新索引,轮询选择下一个服务器
this.currentIndex = (this.currentIndex + 1) % this.servers.length;
return this.servers[this.currentIndex];
}

// 分配任务
assignTasks(tasks) {
tasks.forEach((task, index) => {
const server = this.getNextServer(); // 获取下一个服务器
console.log(`任务 ${index + 1} 分配给服务器: ${server}`);
});
}
}

// 示例使用
const servers = ['Server-A', 'Server-B', 'Server-C']; // 服务器列表
const tasks = [1, 2, 3, 4, 5, 6, 7]; // 待分配的任务

const scheduler = new RoundRobinScheduler(servers);
scheduler.assignTasks(tasks);

// 输出结果:

任务 1 分配给服务器: Server-A
任务 2 分配给服务器: Server-B
任务 3 分配给服务器: Server-C
任务 4 分配给服务器: Server-A
任务 5 分配给服务器: Server-B
任务 6 分配给服务器: Server-C
任务 7 分配给服务器: Server-A

Round-Robin 调度算法 算法逻辑清晰,代码量少。每个资源都会得到相同的任务分配机会。不需要复杂的数据结构,适合轻量级场景。但是, 如果不同资源的处理能力差异较大,Round-Robin 可能导致高负载的资源处理不过来,而低负载的资源处于空闲状态。如果任务的大小或耗时不均匀,轮询分配可能导致任务处理效率低。当资源数量动态变化时(如新增或移除服务器),需要重新初始化资源列表,影响性能。

在实际应用中,Round-Robin 非常适合资源能力相当、任务轻量的场景。如果任务或资源存在差异,可以引入 权重轮询(Weighted Round-Robin) 或其他更复杂的调度算法。

1.2 如何使用 PM2 部署 Node 服务?

使用配置文件(ecosystem.config.js)部署 Node 服务: PM2 支持 pm2 start 入口文件 快速启动服务。 PM2 也支持支持通过配置文件来管理服务,方便处理更复杂的场景,比如多个服务、不同环境配置。通过配置 ecosystem.config.js , 比如: script 入口、instances 多进程模式,max 表示根据 CPU 核心数自动配置。exec_mode: fork(默认)单进程,cluster 多进程模式。max_memory_restart 超过内存阈值时自动重启,防止内存泄漏。watch 启用文件监听,文件变更时自动重启服务。在生产环境中,为了保证服务随系统启动,PM2 提供了 开机自启动 的功能。

1.3 为什么使用 PM2 部署 Node 服务?

使用 PM2 部署 Node 服务主要是因为 PM2 能够解决以下关键问题,提升服务的 高可用性稳定性性能,同时降低运维的复杂性。

  1. 进程守护与自动重启: Node.js 的单线程特性意味着,如果服务崩溃或者异常退出,进程就会终止,服务中断。PM2 提供了进程守护功能,自动监控和管理服务进程。Node.js 服务意外崩溃时,PM2 能自动重启进程,避免服务中断,减少人工介入。同时 PM2 能够实时监控服务的运行状态,并记录日志,便于后续问题排查。

  2. 多进程支持与集群模式(Cluster Mode: Node.js 是单线程运行的,单个实例无法充分利用多核 CPU 资源。PM2 通过 Cluster Mode,利用 Node.jscluster 模块,启动多个进程,实现多核 CPU 负载均衡,提高并发处理能力。PM2 使用 轮询(Round-Robin)算法 将请求均匀分发给不同的进程。

  3. 资源管理与故障恢复: PM2 提供资源管理功能,可以限制单个进程的内存和 CPU 使用量,防止资源耗尽。避免内存泄漏导致服务器崩溃。在故障发生时自动恢复服务,提升服务稳定性。

  4. 文件变更监听与热重启: PM2 提供 文件变更监听 功能,可以在代码发生变更时自动重启服务,避免手动重启带来的麻烦。

  5. 服务日志管理: PM2 提供了完善的日志管理功能,自动记录进程的输出、错误信息和资源使用情况。记录服务运行日志,便于快速排查故障。统一管理日志,方便运维和监控。

1.4 Kubernetes 部署 Node Vs PM2 部署 Node

PM2 是一个流行的进程管理工具,专门用于管理 Node.js 应用程序。虽然 PM2 对于 Node.js 应用的部署与管理非常有效,但相比于 KubernetesPM2 更适用于单机环境或者较小的服务集群。以下是二者的主要区别和优势对比:

PM2PM2 主要用于单节点的进程管理,虽然支持多进程模式(通过 pm2 scale 命令),但其本质上是管理本地机器上的 Node.js 进程,不能自动扩展到多个机器,也无法进行跨节点的服务管理。

KubernetesKubernetes 是一个分布式系统平台,能够管理跨多个节点的服务部署、扩展和管理。通过 PodsDeploymentsReplicaSets,可以轻松实现自动扩容、滚动更新、负载均衡等功能,适用于大规模分布式应用。

如果你的应用规模较小,PM2 是一个简单有效的进程管理工具,而如果你面临的是大规模、分布式的 Node 服务部署,Kubernetes 提供的自动化管理、高可用性、资源管理和扩展性优势将显得更加突出。

二、构建服务


三、部署服务


使用配置文件(ecosystem.config.js)部署 Node 服务: PM2 支持 pm2 start 入口文件 快速启动服务。 PM2 也支持支持通过配置文件来管理服务,方便处理更复杂的场景,比如多个服务、不同环境配置。

3.1 创建配置文件

在项目根目录下创建 ecosystem.config.js 文件:

3.2 编写配置文件

ecosystem.config.js 配置示例:

module.exports = {
apps: [
{
name: "my-node-service", // 应用名称
script: "app.js", // 启动脚本路径
instances: "max", // 启动实例数 (max 表示根据 CPU 核心数启动)
exec_mode: "cluster", // 启用集群模式 (多进程)
watch: true, // 开启文件变更监听,自动重启服务
max_memory_restart: "300M", // 内存超过 300M 自动重启
env: { // 默认环境变量
NODE_ENV: "development",
PORT: 3000
},
env_production: { // 生产环境变量
NODE_ENV: "production",
PORT: 8000
}
}
]
};
  • instances: 多进程模式,max 表示根据 CPU 核心数自动配置。

  • exec_mode: fork(默认)单进程,cluster 多进程模式。

  • watch: 启用文件监听,文件变更时自动重启服务。

  • max_memory_restart: 超过内存阈值时自动重启,防止内存泄漏。

  • env / env_production: 环境变量设置。

3.3 操作 Node 服务

启动服务:

pm2 start ecosystem.config.js --env production

重启服务

pm2 restart ecosystem.config.js

停止服务

pm2 stop ecosystem.config.js

删除服务

pm2 delete app.js

查看进程表

pm2 list

监控进程资源

pm2 monit

日志查看

pm2 logs

在生产环境中,为了保证服务随系统启动,PM2 提供了 开机自启动 的功能。

# 1. 生成开机启动脚本
pm2 startup

# 2. 根据输出的命令执行,例如在 Linux 系统中
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u your-user --hp /home/your-user

# 3. 保存当前服务列表: pm2 save 会将当前 PM2 管理的进程列表保存到磁盘,重启后自动恢复。
pm2 save