3. Cluster 模块实战:多进程架构、负载均衡策略与进程间通信
说实话,单线程的 Node.js 跑得挺欢,但一旦遇到 CPU 密集型任务,或者流量突然暴涨,你就会发现它有点力不从心。这时候,Cluster 模块就是你的救星。
我最早接触 Cluster 是在一个实时聊天系统里。单进程扛不住 2000 并发,加了 Cluster 之后,直接撑到 8000+。嗯,今天我们就来聊聊这个模块怎么用,以及背后的门道。
3.1 多进程架构:为什么需要 Cluster?
Node.js 默认是单进程单线程的。这意味着,你 8 核的服务器,只有一个核在干活。其他 7 个核在旁边看热闹。你说浪费不浪费?
Cluster 模块的核心思想很简单:启动一个主进程(Master),然后 fork 出多个工作进程(Worker)。每个 Worker 独立运行,共享同一个端口。
说白了,就是让 Node.js 也能利用多核 CPU。我习惯把 Master 叫做「包工头」,Worker 叫做「工人」。包工头负责分配任务,工人负责干活。
- Master 进程:负责管理 Worker,不处理具体请求
- Worker 进程:实际处理请求的进程,每个 Worker 都是一个独立的 Node.js 实例
- 共享端口:所有 Worker 监听同一个端口,由操作系统或 Cluster 模块分发请求
我在项目中遇到过一个问题:如果 Worker 数量等于 CPU 核心数,性能最好。但如果你有 I/O 密集型任务,可以适当多开几个。为什么?因为 I/O 操作不占 CPU,多开几个 Worker 能提高吞吐量。
3.2 负载均衡策略:请求怎么分?
Cluster 模块内置了两种负载均衡策略。你想想看,多个 Worker 抢一个端口,总得有个规则吧?
| 策略 | 模式 | 特点 | 适用场景 |
|---|---|---|---|
| Round-Robin(轮询) | cluster.SCHED_RR | 按顺序分配,公平 | Windows、大多数 Linux |
| 操作系统默认 | cluster.SCHED_NONE | 由内核决定,性能高 | Linux 下性能敏感场景 |
我个人习惯在 Linux 上用 Round-Robin。虽然操作系统默认策略性能更高,但轮询策略更可控。我曾经在生产环境遇到过一个问题:某个 Worker 因为 GC 卡住了,操作系统默认策略还往它那发请求,导致部分用户响应超时。换成轮询后,至少能保证请求均匀分布。
3.3 进程间通信:Worker 之间怎么聊天?
Worker 之间不能直接通信。它们各自独立,内存不共享。那怎么办?通过 Master 进程转发。
Cluster 模块提供了两种通信方式:
- Master 向 Worker 发消息:使用
worker.send() - Worker 向 Master 发消息:使用
process.send() - Worker 之间通信:必须经过 Master 中转
来看一个实际例子。假设我们有一个计数器,所有 Worker 共享这个计数。因为内存不共享,所以得通过 Master 来维护:
// master.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
let counter = 0;
console.log(`Master ${process.pid} 启动`);
// fork 出 4 个 Worker
for (let i = 0; i < os.cpus().length; i++) {
const worker = cluster.fork();
// 监听 Worker 发来的消息
worker.on('message', (msg) => {
if (msg.type === 'increment') {
counter++;
console.log(`当前计数: ${counter}`);
}
});
}
// 定期广播计数给所有 Worker
setInterval(() => {
for (const id in cluster.workers) {
cluster.workers[id].send({ type: 'counter', value: counter });
}
}, 5000);
} else {
// Worker 进程
process.on('message', (msg) => {
if (msg.type === 'counter') {
console.log(`Worker ${process.pid} 收到计数: ${msg.value}`);
}
});
// 模拟请求,每 2 秒增加一次计数
setInterval(() => {
process.send({ type: 'increment' });
}, 2000);
}
嗯,这里要注意:消息传递是异步的,而且有序列化开销。 如果你频繁传递大量数据,性能会下降。我曾经在日志聚合系统里踩过这个坑——每个请求都发一个完整对象给 Master,结果 Master 成了瓶颈。后来改成只发关键字段,性能提升了 3 倍。
3.4 优雅退出与进程守护
Worker 进程随时可能挂掉。内存泄漏、未捕获异常、系统资源耗尽……原因很多。Cluster 模块提供了事件监听,让我们可以优雅处理:
// Master 进程监听 Worker 退出事件
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} 挂了,退出码: ${code}`);
// 自动重启
const newWorker = cluster.fork();
console.log(`新 Worker ${newWorker.process.pid} 已启动`);
});
// Worker 进程优雅退出
process.on('SIGTERM', () => {
console.log('收到 SIGTERM 信号,开始优雅退出...');
// 停止接收新请求
server.close(() => {
console.log('所有连接已关闭,进程退出');
process.exit(0);
});
// 强制退出,防止一直挂起
setTimeout(() => {
console.error('强制退出');
process.exit(1);
}, 10000);
});
我习惯在 Master 里加一个心跳检测。每隔 5 秒向 Worker 发一个 ping,如果 Worker 没回应,就认为它挂了,直接重启。这样能及时发现「假死」的 Worker。
3.5 实战:构建一个高可用的 HTTP 服务
把上面的知识点串起来,写一个完整的例子:
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const PORT = 3000;
const WORKER_COUNT = os.cpus().length;
if (cluster.isMaster) {
console.log(`Master ${process.pid} 启动,将 fork ${WORKER_COUNT} 个 Worker`);
// 创建 Worker
for (let i = 0; i < WORKER_COUNT; i++) {
const worker = cluster.fork();
// 监听 Worker 消息
worker.on('message', (msg) => {
if (msg.type === 'request_log') {
console.log(`[Master] Worker ${worker.process.pid} 处理了请求: ${msg.url}`);
}
});
}
// Worker 退出时自动重启
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} 挂了,退出码: ${code}`);
const newWorker = cluster.fork();
console.log(`新 Worker ${newWorker.process.pid} 已启动`);
});
// 优雅退出 Master
process.on('SIGTERM', () => {
console.log('Master 收到 SIGTERM,通知所有 Worker 退出');
for (const id in cluster.workers) {
cluster.workers[id].kill('SIGTERM');
}
process.exit(0);
});
} else {
// Worker 进程
const server = http.createServer((req, res) => {
// 模拟 CPU 密集型任务
const start = Date.now();
while (Date.now() - start < 100) {
// 模拟 100ms 的计算
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Worker ${process.pid} 处理完成\n`);
// 通知 Master 记录日志
process.send({ type: 'request_log', url: req.url });
});
server.listen(PORT, () => {
console.log(`Worker ${process.pid} 开始监听端口 ${PORT}`);
});
// 优雅退出 Worker
process.on('SIGTERM', () => {
console.log(`Worker ${process.pid} 收到 SIGTERM,开始优雅退出`);
server.close(() => {
console.log(`Worker ${process.pid} 所有连接已关闭`);
process.exit(0);
});
});
}
这个例子包含了:多进程架构、负载均衡、进程间通信、优雅退出、自动重启。你可以在本地跑一下,看看效果。
最后说一句:Cluster 模块不是银弹。 如果你的应用主要是 I/O 操作,单进程配合异步 I/O 已经够用。Cluster 的真正价值在于利用多核 CPU 处理 CPU 密集型任务,以及提高可用性——一个 Worker 挂了,还有其他 Worker 顶着。
嗯,下一章我们会聊聊更高级的话题:如何用 Cluster 配合 Redis 实现跨进程的 Session 共享。到时候见。