了解如何在 Node.js 中高效扩展WebSockets,通过集群、负载均衡、Redis 发布/订阅(pub/sub)及实战模式实现,避免 CPU 飙升。
若你曾启动 Node.js WebSocket 服务器并进行数千连接压力测试,定会深有体会:
CPU 负载飙升,内存泄漏频发。转眼间,你的聊天应用仿佛在泥泞地上运行。
WebSockets 看似简单——直到你尝试扩展它。
本文将详细解析如何将 Node.js WebSockets 扩展至数百万并发连接,而不会导致 CPU 崩溃。
WebSockets 有何不同?
与 REST API 不同,WebSockets 不是请求/响应模式,而是长连接。这意味着:
- 每个连接都保持状态。
- 服务器必须维持心跳机制(ping/pong)。
- 广播消息可能压垮事件循环。
其扩展性更多取决于架构设计与效率优化,而非单纯计算能力。
朴素的方法
大多数教程是这样开始的:
import WebSocket, { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws) => {
console.log("Client connected");
ws.on("message", (msg) => {
console.log(`Received: ${msg}`);
// 回显
ws.send(`You said: ${msg}`);
});
});
对于 50 个用户运行良好,或许 500 个用户也行。但若扩展到 50,000 个连接时:
- CPU 因心跳检测而飙升。
- 单个低效客户端即可阻塞事件循环。
- 广播变成 O(n) 操作。
扩展模式 #1:集群化
Node.js 是单线程的,要充分利用所有 CPU 核心,需要采用集群化方案。
import cluster from "cluster";
import { cpus } from "os";
import { WebSocketServer } from "ws";
import http from "http";
if (cluster.isPrimary) {
const numCPUs = cpus().length;
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
} else {
const server = http.createServer();
const wss = new WebSocketServer({ server });
wss.on("connection", (ws) => {
ws.on("message", (msg) => {
ws.send(`Worker ${process.pid} got: ${msg}`);
});
});
server.listen(8080, () => {
console.log(`Worker ${process.pid} listening`);
});
}
现在每个 CPU 核心处理其专属的连接片段。
问题:worker 不共享状态。这导致……
扩展模式#2:基于 Redis 的 Pub/Sub
要跨工作者(或服务器)进行广播,请使用 Redis 这样的 Pub/Sub 系统。
import { createClient } from "redis";
import { WebSocketServer } from "ws";
const pub = createClient();
const sub = createClient();
await pub.connect();
await sub.connect();
const wss = new WebSocketServer({ port: 8080 });
sub.subscribe("broadcast", (msg) => {
wss.clients.forEach((client) => {
if (client.readyState === 1) client.send(msg);
});
});
wss.on("connection", (ws) => {
ws.on("message", (msg) => {
pub.publish("broadcast", msg.toString());
});
});
现在所有消息都通过 Redis 路由,确保每个工作进程(及每个服务器实例)都能进行广播。
扩展模式 #3:Offload 心跳
WebSocket 心跳检测在大规模环境中会消耗大量 CPU 资源。与其使用 setInterval ping,不如让 nginx 或负载均衡器自动断开 dead sockets。
Nginx 配置片段:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream websocket {
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
server {
listen 80;
location /ws {
proxy_pass http://websocket;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
让基础架构处理 dead sockets 可降低 Node.js 的开销。
扩展模式 #4:事件驱动广播
向数千客户端广播时,避免使用简单的 for 循环。采用批处理或消息队列机制。
function broadcast(wss, data) {
const message = JSON.stringify(data);
for (const client of wss.clients) {
if (client.readyState === 1) {
setImmediate(() => client.send(message));
}
}
}
使用 setImmediate 可避免在向数千个客户端发送数据时阻塞事件循环。
扩展模式 #5:基于 Sticky Sessions 的水平扩展
负载均衡器通常随机路由客户端,这会破坏 WebSocket 连接。你需要 Sticky Sessions(粘滞会话)机制,确保客户端始终连接到同一服务器。
在 Kubernetes 中:
service:
spec:
sessionAffinity: ClientIP
在 Nginx 中:
upstream websocket {
ip_hash;
server 127.0.0.1:8080;
server 127.0.0.1:8081;
}
现在每个连接都保持固定,避免状态丢失。
真实案例研究:聊天应用的扩展实践
在某金融科技公司,我们通过以下方式将实时交易聊天系统扩展至 100 万并发连接:
- 采用 Node.js 集群充分利用所有CPU核心。
- 使用 Redis 发布/订阅机制实现跨工作进程通信。
- 将心跳卸载至负载均衡器
- 采用 sticky sessions 实现水平扩展
- 通过Prometheus + Grafana监控CPU使用率
成果:百万活跃用户下 CPU 稳定维持在 ~60% 左右
调试性能瓶颈
常见陷阱:
- 阻塞式 JSON 解析 → 使用流解析器如
JSONStream。 - 内存泄漏 → 监控
wss.clients.size,清理闲置套接字。 - GC 压力 → 尽可能复用对象。
- 大有效载荷 → 使用
permessage-deflate压缩。
压缩示例:
const wss = new WebSocketServer({
port: 8080,
perMessageDeflate: {
zlibDeflateOptions: { level: 3 },
clientNoContextTakeover: true,
},
});
结论
WebSocket 的扩展并非仅仅依靠堆砌硬件,而是需要采用智能模式:集群化、发布/订阅、粘性会话以及工作负载卸载。
通过合理配置,Node.js 能够处理数百万个并发 WebSocket 连接,同时避免 CPU 资源耗尽。
作者:Nikulsinh Rajput
原文:https://medium.com/@hadiyolworld007/node-js-scaling-websockets-without-burning-cpu-e9834b4e65c3
本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/im/61316.html