必须只让 worker 进程监听端口且通过 Redis 实现跨进程广播与连接状态共享,因 cluster 下各 worker 独立运行、无法共享内存或自动负载连接。

直接用 cluster 模块跑 WebSocket 服务,不加改造就上线,大概率会遇到连接数上不去、部分 worker 完全收不到消息、甚至 EADDRINUSE 错误——这不是配置问题,是没理解 cluster 和 WebSocket 的协作边界。
为什么 cluster.fork() 后 ws.Server.listen() 会失败?
Node.js 的 ws 模块底层基于 net.Server,而默认情况下每个 ws.Server 实例都试图独占端口。当多个 worker 进程同时调用 listen(8080),后启动的进程就会抛出 Error: listen EADDRINUSE: address already in use :::8080。
解决办法只有一个:**只让 worker 进程监听,且必须通过主进程统一调度**。cluster 模块内部已封装了 IPC 层和文件描述符传递机制,但前提是不能手动重复 listen。
- 主进程(
cluster.isMaster)不做任何ws.Server实例化或listen操作 - 所有
ws.Server创建和listen必须在cluster.isWorker分支内完成 - 确保每个 worker 都执行
new WebSocket.Server({ port: 8080 })—— cluster 会自动处理端口复用,无需额外参数
worker 进程间无法广播消息?别指望共享内存
WebSocket 连接对象(socket)是绑定在单个 worker 进程内的,server.clients 只包含本 worker 的连接。你调用 server.clients.forEach(...),永远只广播给当前进程的客户端。
要实现跨 worker 广播,必须引入外部协调层。Redis 是最轻量、最可靠的选择,不是“可选优化”,而是集群模式下的刚性依赖。
- 每个 worker 收到消息后,先
PUBLISH到 Redis 频道(如ws:messages) - 所有 worker 都
SUBSCRIBE同一频道,收到后转发给本地clients - 避免用
cluster.workers+worker.send()做进程间广播:消息序列化开销大,且无法保证顺序和可靠性
示例关键逻辑:
Node.js 可靠编程技能。避免事件循环阻塞、异步陷阱、ESM 兼容问题和内存泄漏。触发词:Node.js 开发、async/await、流处理、性能优化、安全编码。
if (cluster.isWorker) {
const wss = new WebSocket.Server({ port: 8080 });
const redis = new Redis();
redis.subscribe('ws:messages');
redis.on('message', (channel, message) => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
});
wss.on('connection', (socket) => {
socket.on('message', async (data) => {
await redis.publish('ws:messages', data.toString());
});
});
}
心跳检测和连接状态必须集中存储
单 worker 内可以用 Map 存连接,但集群下用户可能连到任意 worker。如果某个 worker 崩溃,它维护的连接状态就丢失了,导致“假在线”——客户端没断开,服务端却认为已掉线。
连接元数据(如用户 ID、登录时间、最后心跳时间)必须写入 Redis 或其他共享存储,且由所有 worker 共同维护。
- 连接建立时,worker 写入
redis.setex(`ws:client:${uid}`, 300, process.pid) - 每收到一次心跳,更新对应 key 的过期时间
- 定期用
redis.keys('ws:client:*')扫描并清理超时连接(注意生产环境慎用 keys,改用 scan) - 不要依赖
socket.on('close')清理状态——网络闪断时这个事件根本不会触发
cluster 模式下 CPU 密集型操作仍会阻塞单个 worker
很多人以为上了 cluster 就万事大吉,其实不然。一个 worker 进程仍是单线程,heavyComputation() 这类同步计算仍会卡死该 worker 的整个事件循环,导致它无法响应新连接或心跳。
真正解耦 CPU 和 I/O 的方式只有两种:
- 把重计算任务移出 worker 进程,用
child_process.fork()单独起子进程处理,再通过 IPC 返回结果 - 升级到 Node.js 12+,改用
worker_threads模块做线程级并行(注意:不能共享WebSocket实例,只能传数据)
cluster 解决的是多核利用率问题,不是单线程阻塞问题——这点最容易被忽略。










