背景
刚接触Node.js那会儿,总听说它是单线程的,这句话可把我坑惨了——有次线上流量暴涨,我的服务直接卡成PPT。后来才明白,单线程不是枷锁,解锁多核性能的关键在于集群模式。今天我们就手把手拆解这个能让你服务器吞吐量翻倍的神器。
一、为什么你的Nodejs应用需要集群模式?
上周帮朋友优化一个电商系统,单核QPS只有1200,启用集群后直接冲到8600。这背后的原理很简单:现代服务器至少8核CPU,但Nodejs默认只用一个核。就像让八个工人只做一个流水线,纯属资源浪费。
我们做个压力测试对比(使用wrk工具):
wrk -t12 -c400 -d30s http://localhost:3000
Requests/sec: 1242.34
# 启用8核集群模式
wrk -t12 -c400 -d30s http://localhost:3000
Requests/sec: 8568.71
数据不会说谎——吞吐量提升近7倍。但这只是开始,后面还有更猛的操作。
二、三分钟搭建你的第一个集群服务
直接上干货,先看基础代码模板(技术栈:Nodejs 18.x原生cluster模块):
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
// 主进程负责繁殖工人
console.log(`主进程 ${process.pid} 启动`);
// 根据CPU核心数创建worker
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
// worker异常退出时自动重启
cluster.on('exit', (worker, code, signal) => {
console.log(`工人 ${worker.process.pid} 挂了,正在重启...`);
cluster.fork();
});
} else {
// 工人进程处理具体业务
http.createServer((req, res) => {
res.writeHead(200);
res.end('Hello Cluster!');
}).listen(3000);
console.log(`工人进程 ${process.pid} 就绪`);
}
这段代码实现了:
- 自动检测CPU核心数量
- Master进程管理worker生命周期
- Worker崩溃自动重启机制
- 各worker共享3000端口(底层由操作系统负载均衡)
三、生产环境必须知道的五个实战技巧
3.1 负载均衡算法调优
默认的轮询策略不适合长连接场景,我们可以改用能感知worker负载的算法:
const cluster = require('cluster');
if (cluster.isMaster) {
// 创建负载统计Map
const workerLoads = new Map();
cluster.on('message', (worker, message) => {
if (message.type === 'loadReport') {
workerLoads.set(worker.id, message.load);
}
});
// 自定义调度策略
cluster.on('request', (worker, message, handle) => {
// 找出当前最空闲的worker
const leastLoadedWorker = [...workerLoads.entries()]
.sort((a, b) => a[1] - b[1])[0];
leastLoadedWorker.send('handle', handle);
});
}
这种实现可以让请求优先分配给负载最低的worker,适合文件上传等重量级请求场景。
3.2 进程异常监听策略
基础版的崩溃重启存在雪崩风险,得加上重启频率限制:
const restartRecords = new Map();
cluster.on('exit', (worker) => {
const now = Date.now();
const lastRestart = restartRecords.get(worker.id) || 0;
// 30秒内重启超过3次则停止
if (now - lastRestart < 30000) {
worker.restartCount = (worker.restartCount || 0) + 1;
if (worker.restartCount >= 3) {
console.error('工人频繁崩溃,停止重启');
return;
}
}
restartRecords.set(worker.id, now);
cluster.fork();
});
四、别踩这些坑!集群模式的隐藏陷阱
4.1 会话存储要特殊处理
别用内存存session!不同worker内存不共享,推荐Redis方案:
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
app.use(session({
store: new RedisStore({ host: '127.0.0.1', port: 6379 }),
secret: 'mySecret'
}));
4.2 热更新要这样玩
直接重启会有服务中断,试试零停机部署:
// 主进程收到SIGUSR2信号时触发热更新
process.on('SIGUSR2', () => {
const workers = Object.values(cluster.workers);
function restartWorker(i) {
if (i >= workers.length) return;
const worker = workers[i];
// 先启新后停旧
const newWorker = cluster.fork();
newWorker.on('listening', () => {
worker.kill('SIGTERM');
restartWorker(i + 1);
});
}
restartWorker(0);
});
五、什么样的项目适合用集群?
经过三年实战,我总结出这些场景最受益:
- 用户量超过10万的中大型Web应用
- 需要处理大量并发I/O(如即时通讯)
- 需要弹性缩放的云原生应用
但遇到CPU密集型任务(如视频转码),集群就力不从心了,这时候该用Worker Thread。