第8章 8.2 cluster 集群与负载均衡

🎯 开场 3 分钟:为什么要学这个?

上一章我们学会了用 child_process 手动创建子进程,就像开了一家只有你一个员工的店。老板(主进程)什么都干:接单、做菜、打包、收款...生意好的时候,你忙得脚不沾地,客人等太久都跑了。

痛点来了:

你有没有遇到过这种情况——网站突然爆火,一堆人同时访问,结果服务器直接卡死或者崩溃了?就像一家小店突然来了100个客人,老板的脑子(单进程单线程)根本处理不过来。

学完这章你能解决:

想象你有一家真正的大餐厅——不是只有一个员工,而是雇了 8 个服务员同时工作,每个人都能独立接待客人。这就是 cluster 模块要给你的能力:让 Node.js 利用多核 CPU,用多个进程同时「打工」,把流量均匀分配给各个进程。

简单说:上一章你学会了开店,这一章你要学会开连锁店。


🧱 基础 25 分钟:核心概念

什么是 cluster?说白了就是「分身术」

生活类比:

想象你是奶茶店老板(主进程)。每天高峰期来了,你一个人根本忙不过来。于是你雇了 4 个员工,每个员工都能独立做奶茶、收银、接待客人。

你来分配工作——客人进门,你轮流指派给不同的员工。某个员工请假了,你立刻招新人顶上。

这就是 cluster 在干的事:一个主进程(老板)管理多个工作进程(员工),共同处理请求。

为什么用 cluster?解决两大痛点

  1. 多核 CPU 浪费:现在电脑都是多核的,但 Node.js 默认只用一个核,其他核闲着也是闲着
  2. 单点故障:只有一个进程,挂了就没了,多进程可以做到「一个倒下,其他顶上」

核心概念拆解

┌─────────────────────────────────────────┐
│            Master Process               │
│         (老板 - 分配任务)                │
│                                         │
│   ┌────┐  ┌────┐  ┌────┐  ┌────┐       │
│   │ W1 │  │ W2 │  │ W3 │  │ W4 │       │
│   │员工1│  │员工2│  │员工3│  │员工4│       │
│   └────┘  └────┘  └────┘  └────┘       │
│   (Worker Processes - 真正干活的人)     │
└─────────────────────────────────────────┘
![配图1 - 配图1](https://blog.xxyye.com/wp-content/uploads/2026/06/32695af027f7dc9.jpg)

主进程(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 收到老板消息: 老板通知:客人太多了,加班!

配图2 - 配图2

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 个核心点:

  1. cluster 模块让 Node.js 利用多核 CPU —— 通过主进程 + 工作进程的架构,分担计算压力
  2. 进程间通信(IPC)实现状态同步 —— 工作进程用 process.send() 发送数据,主进程用 cluster.on('message') 接收
  3. PM2 是生产环境的好帮手 —— 一行命令实现集群管理、零停机重启、进程监控

延伸学习资源:

互动钩子:

你在项目里用过 cluster 或 PM2 吗?有没有遇到过「工作进程集体罢工」的诡异 bug?评论区聊聊,老粉优先回复!


下章预告:

学会了用多个进程「分身」,但进程毕竟是重量级的——创建和销毁都开销不小。下一章我们要介绍一个更轻量的方案:Worker Threads(工作线程),它让单个进程里也能有多个线程并行执行任务,适合 CPU 密集型计算。敬请期待!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。