认识
一、认识
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
机制 在 Master
和 Worker
之间传递消息。
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 Plus
和 Prometheus
),实现自定义监控和告警。
六、文件监听与热重载
文件监听与热重载: PM2
支持 文件变更监听,在代码变更后自动重启服务,提升开发效率。具体实现为: 使用 chokidar
库监听文件变动,当检测到变更时,重启进程
const chokidar = require('chokidar');
chokidar.watch('./').on('change', () => {
process.exit(0); // 触发重启
});