跳到主要内容

认识

一、认识


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 的两种进程启动模式:

3.1 Fork 模式

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

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

3.2 Cluster 模式

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'); // 启动服务
}

扩展点一、进程间通信(IPC:解耦 CLI 与守护进程,便于管理。

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, 父进程!' });
});

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');
});

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);
}

二、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) 或其他更复杂的调度算法。

四、自动故障恢复与重启机制


自动故障恢复与重启机制: 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); // 触发重启
});