第8章 8.2 cluster 集群与负载均衡
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 child_process 手动创建子进程,就像开了一家只有你一个员工的店。老板(主进程)什么都干:接单、做菜、打包、收款...生意好的时候,你忙得脚不沾地,客人等太久都跑了。
痛点来了:
你有没有遇到过这种情况——网站突然爆火,一堆人同时访问,结果服务器直接卡死或者崩溃了?就像一家小店突然来了100个客人,老板的脑子(单进程单线程)根本处理不过来。
学完这章你能解决:
想象你有一家真正的大餐厅——不是只有一个员工,而是雇了 8 个服务员同时工作,每个人都能独立接待客人。这就是 cluster 模块要给你的能力:让 Node.js 利用多核 CPU,用多个进程同时「打工」,把流量均匀分配给各个进程。
简单说:上一章你学会了开店,这一章你要学会开连锁店。
🧱 基础 25 分钟:核心概念
什么是 cluster?说白了就是「分身术」
生活类比:
想象你是奶茶店老板(主进程)。每天高峰期来了,你一个人根本忙不过来。于是你雇了 4 个员工,每个员工都能独立做奶茶、收银、接待客人。
你来分配工作——客人进门,你轮流指派给不同的员工。某个员工请假了,你立刻招新人顶上。
这就是 cluster 在干的事:一个主进程(老板)管理多个工作进程(员工),共同处理请求。
为什么用 cluster?解决两大痛点
- 多核 CPU 浪费:现在电脑都是多核的,但 Node.js 默认只用一个核,其他核闲着也是闲着
- 单点故障:只有一个进程,挂了就没了,多进程可以做到「一个倒下,其他顶上」
核心概念拆解
┌─────────────────────────────────────────┐
│ Master Process │
│ (老板 - 分配任务) │
│ │
│ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
│ │ W1 │ │ W2 │ │ W3 │ │ W4 │ │
│ │员工1│ │员工2│ │员工3│ │员工4│ │
│ └────┘ └────┘ └────┘ └────┘ │
│ (Worker Processes - 真正干活的人) │
└─────────────────────────────────────────┘

主进程(Master):只负责「管理」——决定启动几个员工、分发请求、监控员工状态
工作进程(Worker):真正处理业务逻辑——接收请求、处理数据、返回结果
第一个 cluster 程序:最简版「连锁店」
// cluster基础用法.js
const cluster = require('cluster');
const os = require('os');
// 获取CPU核心数,决定开几个员工
const numCPUs = os.cpus().length;
if (cluster.isMaster) {
// ========== 主进程代码 ==========
console.log(`我是老板,今天准备雇 ${numCPUs} 个员工`);
// 循环创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork(); // 雇一个新员工
}
// 监听员工上线消息
cluster.on('online', (worker) => {
console.log(`员工 ${worker.id} 已就位,PID: ${worker.process.pid}`);
});
// 监听员工下线(挂了)
cluster.on('exit', (worker, code, signal) => {
console.log(`员工 ${worker.id} 离职了(${signal || code}),重新招聘...`);
cluster.fork(); // 立即招新人顶上
});
} else {
// ========== 工作进程代码 ==========
console.log(`员工 ${cluster.worker.id} 开始工作`);
// 模拟处理请求
const http = require('http');
http.createServer((req, res) => {
res.end(`员工 ${cluster.worker.id} 为你服务,PID: ${process.pid}\n`);
}).listen(3000);
console.log(`员工 ${cluster.worker.id} 已在端口 3000 接待客人`);
}
运行后,你会看到类似这样的输出:
我是老板,今天准备雇 8 个员工
员工 1 开始工作
员工 1 已就位,PID: 12345
员工 2 开始工作
员工 2 已就位, PID: 12346
员工 3 开始工作
...
员工 1 已在端口 3000 接待客人
员工 2 已在端口 3000 接待客人
...
每个员工都独立监听同一个端口 3000!这就是 cluster 的神奇之处——主进程帮你处理了「多个进程同时监听同一端口」这件复杂的事。
负载均衡:怎么分派任务?
现在你有 4 个员工,100 个客人来了,怎么分配?
两种策略:
| 策略 | 原理 | 适用场景 |
|---|---|---|
round-robin(轮询) |
轮流指派:客人1→员工1,客人2→员工2... | Linux/Mac 默认 |
shared socket(共享套接字) |
主进程先接收,再分发给员工 | Windows 默认 |
// 负载均衡策略设置
cluster.schedulingPolicy = cluster.SCHED_RR; // 轮询(默认,Linux/Mac)
// 或者
cluster.schedulingPolicy = cluster.SCHED_NONE; // 由操作系统决定
生活类比:
- 轮询就像叫号机,12345... 轮流来
- 操作系统决定就像随缘派单,哪个员工空就派给谁
进程间通信(IPC):员工怎么跟老板汇报?
// 进程通信示例.js
const cluster = require('cluster');
if (cluster.isMaster) {
const worker = cluster.fork();
// 接收员工的消息
worker.on('message', (msg) => {
console.log('老板收到员工消息:', msg);
});
// 主动给员工发消息
worker.send('老板通知:客人太多了,加班!');
} else {
// 员工收到老板消息
process.on('message', (msg) => {
console.log('员工收到老板消息:', msg);
});
// 员工主动汇报
process.send(`员工${cluster.worker.id}:今日已完成 10 单!`);
}
输出:
老板收到员工消息: 员工1:今日已完成 10 单!
员工1 收到老板消息: 老板通知:客人太多了,加班!

PM2 登场:让集群管理更省心
手动管理进程有个问题——每次改代码都要重启,线上服务就断了。
这就需要 PM2(进程管理器):
# 安装 PM2
npm install -g pm2
# 启动集群模式(自动按CPU核心数启动多个进程)
pm2 start app.js -i 0 # 0 = 自动检测CPU核心数
# 其他常用命令
pm2 list # 查看所有进程
pm2 restart app # 重启(零停机!)
pm2 logs # 查看日志
pm2 stop app # 停止
PM2 的好处:
- 零停机重启:替换旧代码时,新进程先启动,流量切换过去后再关旧进程
- 自动负载均衡:不用自己写 cluster 代码
- 进程监控:某个进程挂了自动重启
🔥 实战 35 分钟:3 个递进的小项目
项目 1(5 分钟):用 cluster 改造你的第一个 HTTP 服务器
需求: 把一个普通 HTTP 服务器改成集群模式
// 1_cluster_server.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');
const numCPUs = os.cpus().length;
// 主进程:负责管理和监控
if (cluster.isMaster) {
console.log(`🎯 主进程 ${process.pid} 启动`);
console.log(`📦 即将创建 ${numCPUs} 个工作进程...\n`);
// 创建工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 监听退出事件,自动重启
cluster.on('exit', (worker, code, signal) => {
console.log(`⚠️ 工作进程 ${worker.id} 退出,正在重启...`);
cluster.fork();
});
} else {
// 工作进程:处理实际请求
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`你好!我是工作进程 ${cluster.worker.id},处理你的请求 (PID: ${process.pid})`);
});
server.listen(3000, () => {
console.log(`✅ 工作进程 ${cluster.worker.id} 正在端口 3000 服务`);
});
}
运行:
node 1_cluster_server.js
测试(另一个终端):
curl http://localhost:3000
curl http://localhost:3000
curl http://localhost:3000
预期输出:你会看到不同的工作进程 ID 在轮流服务你:
你好!我是工作进程 1,处理你的请求 (PID: 12345)
你好!我是工作进程 2,处理你的请求 (PID: 12346)
你好!我是工作进程 3,处理你的请求 (PID: 12347)
一句话解释: cluster.isMaster 判断当前是否为主进程,只有主进程负责 fork() 创建新的工作进程。
项目 2(15 分钟):带状态统计的集群服务器
需求: 每个工作进程记录自己处理了多少请求,老板定期汇总
// 2_cluster_with_stats.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isMaster) {
console.log(`🎯 主进程启动,PID: ${process.pid}\n`);
const workers = {};
let totalRequests = 0;
// 创建 4 个工作进程
const numWorkers = 4;
for (let i = 0; i < numWorkers; i++) {
const worker = cluster.fork();
workers[worker.id] = { id: worker.id, count: 0 };
}
// 收集工作进程的统计数据
cluster.on('message', (worker, message) => {
if (message.type === 'stats') {
workers[worker.id].count = message.count;
totalRequests = Object.values(workers).reduce((sum, w) => sum + w.count, 0);
}
});
// 每 5 秒打印一次统计报告
setInterval(() => {
console.log('========== 📊 工作统计报告 ==========');
Object.values(workers).forEach(w => {
console.log(`员工 ${w.id}: 已处理 ${w.count} 个请求`);
});
console.log(`总计: ${totalRequests} 个请求\n`);
}, 5000);
} else {
// 工作进程:处理请求并统计
let requestCount = 0;
const http = require('http');
const server = http.createServer((req, res) => {
requestCount++;
res.writeHead(200);
res.end(`工作进程 ${cluster.worker.id} 为你服务`);
});
server.listen(3000, () => {
console.log(`✅ 工作进程 ${cluster.worker.id} 已启动`);
});
// 每秒向主进程汇报一次
setInterval(() => {
process.send({ type: 'stats', count: requestCount });
}, 1000);
}
运行:
node 2_cluster_with_stats.js
预期输出:
✅ 工作进程 1 已启动
✅ 工作进程 2 已启动
✅ 工作进程 3 已启动
✅ 工作进程 4 已启动
========== 📊 工作统计报告 ==========
员工 1: 已处理 12 个请求
员工 2: 已处理 8 个请求
员工 3: 已处理 15 个请求
员工 4: 已处理 10 个请求
总计: 45 个请求
一句话解释: 用 process.send() 和 cluster.on('message') 实现进程间通信,主进程汇总所有工作进程的统计数据。
项目 3(15 分钟):一个命令行「进程监控工具」
需求: 写一个工具,启动集群后可以实时查看各工作进程的 CPU 和内存使用情况
// 3_cluster_monitor.js
const cluster = require('cluster');
const os = require('os');
const readline = require('readline');
if (cluster.isMaster) {
console.log('🎯 集群监控工具启动\n');
console.log('命令: status | restart <id> | quit\n');
const workers = new Map();
// 创建工作进程
for (let i = 0; i < os.cpus().length; i++) {
const worker = cluster.fork();
workers.set(worker.id, { worker, cpu: 0, memory: 0 });
}
// 每 2 秒更新一次状态
setInterval(() => {
console.clear();
console.log('┌────────────────────────────────────┐');
console.log('│ 🖥️ 集群状态监控 │');
console.log('├──────┬───────┬──────────┬──────────┤');
console.log('│ ID │ PID │ CPU │ 内存 │');
console.log('├──────┼───────┼──────────┼──────────┤');
workers.forEach((data, id) => {
const cpu = data.cpu.toFixed(1) + '%';
const mem = (data.memory / 1024 / 1024).toFixed(1) + ' MB';
console.log(`│ ${id.toString().padStart(2)} │ ${data.worker.process.pid} │ ${cpu.padStart(8)} │ ${mem.padStart(8)} │`);
});
console.log('└──────┴───────┴──────────┴──────────┘');
}, 2000);
// 监听工作进程的消息(包含 CPU 和内存使用情况)
cluster.on('message', (worker, message) => {
if (message.type === 'health') {
const data = workers.get(worker.id);
if (data) {
data.cpu = message.cpu;
data.memory = message.memory;
}
}
});
// 监听工作进程退出
cluster.on('exit', (worker) => {
console.log(`\n⚠️ 工作进程 ${worker.id} 退出,重新启动...`);
const newWorker = cluster.fork();
workers.set(newWorker.id, { worker: newWorker, cpu: 0, memory: 0 });
});
// 命令行交互
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('line', (input) => {
const [cmd, arg] = input.trim().split(' ');
if (cmd === 'quit') {
console.log('👋 关闭所有工作进程...');
workers.forEach((data) => {
data.worker.kill();
});
process.exit(0);
} else if (cmd === 'status') {
console.log('\n📋 当前工作进程列表:');
workers.forEach((data, id) => {
console.log(` - 进程 ${id}: PID ${data.worker.process.pid}`);
});
} else if (cmd === 'restart') {
const targetId = parseInt(arg);
if (workers.has(targetId)) {
console.log(`🔄 重启工作进程 ${targetId}...`);
workers.get(targetId).worker.kill();
} else {
console.log('❌ 无效的工作进程 ID');
}
}
});
} else {
// 工作进程:定期上报自己的健康状态
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200);
res.end(`工作进程 ${cluster.worker.id} 在线`);
});
server.listen(3000);
// 每秒上报一次健康状态
setInterval(() => {
process.send({
type: 'health',
cpu: process.cpuUsage().user / 1000000, // 转换为百分比
memory: process.memoryUsage().heapUsed
});
}, 1000);
}
运行:
node 3_cluster_monitor.js
预期输出:
┌────────────────────────────────────┐
│ 🖥️ 集群状态监控 │
├──────┬───────┬──────────┬──────────┤
│ ID │ PID │ CPU │ 内存 │
├──────┼───────┼──────────┼──────────┤
│ 1 │ 12345 │ 0.2% │ 32.5MB │
│ 2 │ 12346 │ 0.1% │ 31.8MB │
│ 3 │ 12347 │ 0.3% │ 33.1MB │
│ 4 │ 12348 │ 0.1% │ 30.9MB │
└──────┴───────┴──────────┴──────────┘
一句话解释: 工作进程用 process.send() 定时向主进程发送自己的资源使用情况,主进程汇总后展示成表格。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:端口被重复绑定?
❌ 错误写法:
// 每个工作进程都调用 listen(),可能出问题
cluster.fork();
cluster.fork();
// ... 每个进程都 server.listen(3000)
✅ 正确写法:
// 主进程统一管理,工作进程只处理请求
if (cluster.isMaster) {
cluster.fork();
cluster.fork();
} else {
server.listen(3000); // 只有工作进程监听端口
}
坑 2:共享状态不一致?
❌ 错误例子:
// 主进程定义了一个计数器
let requestCount = 0;
if (cluster.isMaster) {
// 修改计数器
requestCount++;
} else {
// 工作进程读取,可能读到过期的值
console.log(requestCount); // 每个工作进程都有自己的 requestCount!
}
✅ 正确做法:
// 每个工作进程维护自己的状态,最后汇总到主进程
if (cluster.isMaster) {
// 收集各工作进程的数据后再汇总
cluster.on('message', (worker, msg) => {
totalCount += msg.count; // 主进程统一汇总
});
} else {
// 工作进程维护自己的计数器
let count = 0;
// ... 处理请求
process.send({ count }); // 定期上报
}
坑 3:退出时没清理资源?
❌ 错误:
// 直接 process.exit(),可能导致连接中断
process.exit(1);
✅ 正确:
// 优雅退出
cluster.worker.disconnect();
// 或者主进程优雅关闭
cluster.disconnect(() => {
console.log('所有工作进程已关闭');
process.exit(0);
});
坑 4:fork 太多进程?
❌ 错误:
// 创建 1000 个工作进程 —— 系统会崩溃
for (let i = 0; i < 1000; i++) {
cluster.fork();
}
✅ 正确:
// 根据 CPU 核心数合理创建,通常 1-2 倍核心数即可
const numCPUs = os.cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// 如果是 IO 密集型任务,可以适当多一些
// const workerCount = numCPUs * 2;
坑 5:Windows 上 cluster 行为不同?
✅ 兼容性写法:
// 设置负载均衡策略(Windows 需要显式设置)
cluster.schedulingPolicy = cluster.SCHED_RR;
// 或者直接用 PM2,PM2 帮你处理了兼容性问题
性能小贴士:Nginx 配合 cluster
cluster 模块自带的负载均衡适合小规模场景,生产环境推荐用 Nginx 做前置负载均衡:
┌──────────────┐
│ Nginx │
│ (负载均衡) │
└──────┬───────┘
│
┌────────┼────────┐
│ │ │
┌────▼───┐ ┌──▼───┐ ┌──▼───┐
│ Node.js│ │Node.js│ │Node.js│
│Cluster │ │Cluster│ │Cluster│
│Worker 1│ │Worker 2│ │Worker 3│
└────────┘ └──────┘ └──────┘
Nginx 配置示例:
upstream node_cluster {
least_conn; # 最少连接优先
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
location / {
proxy_pass http://node_cluster;
}
}
调试技巧:查看工作进程日志
// 在工作进程里打印日志时标注自己的 ID
console.log(`[Worker ${cluster.worker.id}] 处理请求`);
// 主进程统一格式化输出
cluster.on('message', (worker, msg) => {
console.log(`[Master] 收到来自 Worker ${worker.id} 的消息:`, msg);
});
✏️ 练习题 + 作业题
练习题(5 道,10 分钟内完成)
练习 1(2 分钟):改改核心数
- 输入:把项目 1 的工作进程数固定改成 2 个
- 预期输出:只有 2 个工作进程启动
- 提示:把 os.cpus().length 改成具体数字
练习 2(2 分钟):加个条件判断
- 输入:在项目 1 的工作进程中,判断请求 URL 是 /health 时返回「健康」
- 预期输出:访问 /health 返回「健康」,其他返回默认消息
- 提示:检查 req.url 的值
练习 3(3 分钟):换个端口
- 输入:把项目 2 的监听端口从 3000 改成 8080
- 预期输出:curl 访问 http://localhost:8080 能正常响应
- 提示:改 server.listen() 的参数
练习 4(5 分钟):合并两个项目
- 输入:在项目 2 的统计功能基础上,给工作进程添加重启功能(worker.kill() 然后 cluster.fork())
- 预期输出:输入 restart 命令后,指定 ID 的工作进程被重启
- 提示:参考项目 3 的命令行交互逻辑
练习 5(5 分钟):分析报错
- 输入:运行以下代码,分析为什么报错
const cluster = require('cluster');
if (cluster.isMaster) {
cluster.fork();
} else {
const http = require('http');
http.createServer((req, res) => {
res.end('Hello');
}).listen(3000);
}
cluster.fork(); // 在主进程又调用了一次
- 预期输出:解释报错原因
- 提示:
cluster.fork()只能在主进程调用
作业题(30 分钟-2 小时)
作业:做一个「迷你服务监控仪表盘」
-
需求描述:用 cluster 实现一个 HTTP 服务,监控各工作进程的 CPU、内存、请求数,并用 Web 页面展示
-
功能点:
1. 启动集群,每个工作进程处理 HTTP 请求
2. 每个工作进程每秒计算自己的 CPU 使用率和内存占用
3. 访问/stats返回 JSON 格式的汇总统计
4. 访问/health返回所有工作进程的在线状态 -
加分项:
1. 访问/kill/:id可以杀掉指定的工作进程(模拟故障)
2. 页面每 3 秒自动刷新显示最新数据 -
验收标准:
- 能跑起来(
node app.js) - 访问
http://localhost:3000/stats能看到 JSON 统计 -
工作进程被 kill 后能自动重启
-
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学到的 3 个核心点:
- cluster 模块让 Node.js 利用多核 CPU —— 通过主进程 + 工作进程的架构,分担计算压力
- 进程间通信(IPC)实现状态同步 —— 工作进程用
process.send()发送数据,主进程用cluster.on('message')接收 - PM2 是生产环境的好帮手 —— 一行命令实现集群管理、零停机重启、进程监控
延伸学习资源:
- 📖 Node.js 官方文档 - cluster 模块 —— 最权威的参考
- 📖 PM2 官方文档 —— 进阶集群管理
- 🎥 Nginx 负载均衡配置教程 —— 生产环境必备
互动钩子:
你在项目里用过 cluster 或 PM2 吗?有没有遇到过「工作进程集体罢工」的诡异 bug?评论区聊聊,老粉优先回复!
下章预告:
学会了用多个进程「分身」,但进程毕竟是重量级的——创建和销毁都开销不小。下一章我们要介绍一个更轻量的方案:Worker Threads(工作线程),它让单个进程里也能有多个线程并行执行任务,适合 CPU 密集型计算。敬请期待!

评论(0)