WebSocket实时通信架构与高并发实战指南
结论先行:WebSocket 不是银弹,单机支持 10 万并发连接不难,但真实场景下的内存泄漏、连接风暴、消息积压才是杀死系统的元凶。本文直接给出一套经过压测验证的架构方案和代码。
1. 为什么我放弃轮询和 SSE
三年前我接手一个实时看板项目,最初用的 HTTP 轮询(3 秒一次),QPS 到 200 时数据库直接打满。后来换成 SSE,但客户端需要双向通信时又得额外开接口,维护成本一点没降。
WebSocket 的核心优势就一句话:一次握手,全双工通信。但很多人只看到它好的一面,没注意它带来的运维复杂度。今天我就把自己踩过的坑和总结的经验全盘托出。
2. 基础架构:从单机到集群的踩坑记录
2.1 单机版(别用,会炸)
第一版代码长这样:
// 错误示范:用全局 Map 存连接
const connections = new Map();
ws.on('connection', (socket) => {
const userId = parseToken(socket);
connections.set(userId, socket);
});

上线第二天,内存直接溢出。原因很简单:用户断线时没有清理 Map,连接越积越多,最后撑爆了内存。
2.2 加心跳和清理(能跑,但不稳)
// 正确姿势:定时清理僵尸连接
const HEARTBEAT_INTERVAL = 30000;
const CONNECTION_TIMEOUT = 60000;
ws.on('connection', (socket) => {
socket.isAlive = true;
socket.on('pong', () => { socket.isAlive = true; });
});
const timer = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, HEARTBEAT_INTERVAL);
注意:
ws.isAlive是自定义属性,terminate()会立即关闭底层 TCP 连接,比close()更暴力但有效。我一般直接用 terminate,省得等关闭握手。

加了心跳之后,系统稳定多了,但单机端口有限,还是撑不住大规模并发。
2.3 集群版(生产就这个)
单机 WebSocket 有端口限制(Linux 默认 65535),必须上集群。我用的是 Nginx + Node.js 多进程:
# nginx 配置:关键在 ip_hash
upstream ws_backend {
ip_hash;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
listen 80;
location /ws {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 3600s;
}
}
注意:
ip_hash保证同一个客户端的请求落到同一台后端,避免跨进程通信。如果不用 hash,就得用 Redis 做连接状态同步,复杂度会高一个数量级。
3. 高并发核心:消息分发架构
当你有 100 万在线用户,消息广播是怎么做的?我踩过两个大坑,今天一并分享。
3.1 坑一:直接在进程内广播
// 错误:广播时遍历所有连接,阻塞事件循环
function broadcast(message) {
wss.clients.forEach(client => {
client.send(message); // 如果客户端慢,这里会卡住
});
}
这个写法太天真了。想象一下,如果某个客户端网络特别慢,send 操作就会阻塞事件循环,其他所有连接都得等着。解决方案:用 ws 的 send 方法自带 backpressure 机制,但需要配合 bufferedAmount 检查:
function safeSend(ws, data) {
if (ws.readyState !== WebSocket.OPEN) return;
if (ws.bufferedAmount > 1024 * 1024) { // 超过 1MB 缓存则丢弃
ws.terminate();
return;
}
ws.send(data);
}
3.2 坑二:跨进程广播用 Redis Pub/Sub
// 用 Redis 做消息中转
const redis = require('redis');
const publisher = redis.createClient();
const subscriber = redis.createClient();
subscriber.subscribe('chat_channel');
subscriber.on('message', (channel, message) => {
broadcast(message); // 收到其他进程的消息,再广播给本进程的连接
});
性能数据:单台 8 核机器,Redis 单机版能支撑约 5 万 QPS 的消息转发。超过这个量,需要用 Redis Cluster 或 Kafka。我在生产环境实测,4 个 Node 进程配一个 Redis 实例,消息吞吐能到 3 万/s 左右。
4. 实战:一个实时弹幕系统的完整代码
// server.js
const WebSocket = require('ws');
const redis = require('redis');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// 连接管理
const connections = new Map(); // userId -> ws
wss.on('connection', (ws, req) => {
const userId = new URL(req.url, 'http://localhost').searchParams.get('userId');
connections.set(userId, ws);
// 加入房间
ws.roomId = req.url.split('?')[0].split('/').pop();
ws.on('message', (data) => {
const msg = JSON.parse(data);
// 限制频率:每秒最多 5 条
if (msg.type === 'danmaku') {
rateLimiter.check(userId, () => {
broadcastToRoom(ws.roomId, msg);
});
}
});
ws.on('close', () => {
connections.delete(userId);
});
});
// 房间广播
function broadcastToRoom(roomId, message) {
connections.forEach((ws) => {
if (ws.roomId === roomId && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
});
}
server.listen(3000);
压测结果(用 autocannon 测试):
- 单进程:1.2 万连接,消息吞吐 8000/s
- 4 进程 + Nginx:4.5 万连接,消息吞吐 3.2 万/s
- 瓶颈在 CPU,不在内存
5. 生产环境必做的 5 件事
- 连接数监控:用
wss.clients.size每 10 秒打日志,接入 Prometheus。我见过最离谱的事故是连接数飙到 20 万才发现,那时候已经来不及了。 - 超时断开:30 秒无消息就 ping,60 秒无响应就 terminate。僵尸连接是内存泄漏的头号元凶。
- 消息大小限制:
maxPayload: 1024 * 100(100KB),防止恶意攻击。别问我怎么知道的——有人用 10MB 的 JSON 把我服务打崩过。 - 优雅关闭:收到 SIGTERM 时,先停止接受新连接,再逐个 close 现有连接。直接 kill -9 会导致客户端重连风暴。
- 降级方案:当服务端压力过大时,返回 503 并提示客户端改用轮询。不要死磕 WebSocket,有时候降级比硬撑更明智。
6. 延伸思考
- WebSocket 和 gRPC 的流式通信:如果你们公司用微服务,gRPC 的双向流可能更合适,因为它自带服务发现和负载均衡。我最近就在迁移一个项目,体验还不错。
- WebTransport:Chrome 已经在支持基于 QUIC 的 WebTransport,延迟更低,但生态还不成熟。我观望了一年,还是决定再等等。
- 边缘计算:把 WebSocket 网关放到 CDN 节点,用 Cloudflare Workers 或 Fastly Compute@Edge 做连接管理,能大幅降低源站压力。这个方案我还没在生产环境试过,但测试效果很惊艳。
最后说一句:不要为了用 WebSocket 而用 WebSocket。如果你的场景只是定时拉取数据,SSE 更简单;如果是低频双向通信,HTTP 轮询加长连接池也够用。选技术栈,先看业务,再看性能。

评论已关闭!