WebSocket实时通信架构与高并发实战指南

2026-05-05 10:10 WebSocket实时通信架构与高并发实战指南已关闭评论

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);
});


![Nginx ip_hash 保证客户端请求落到同一后端](imgs/Nginx + Node.js 多进程集群.png)

上线第二天,内存直接溢出。原因很简单:用户断线时没有清理 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 操作就会阻塞事件循环,其他所有连接都得等着。解决方案:用 wssend 方法自带 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 件事

  1. 连接数监控:用 wss.clients.size 每 10 秒打日志,接入 Prometheus。我见过最离谱的事故是连接数飙到 20 万才发现,那时候已经来不及了。
  2. 超时断开:30 秒无消息就 ping,60 秒无响应就 terminate。僵尸连接是内存泄漏的头号元凶。
  3. 消息大小限制maxPayload: 1024 * 100(100KB),防止恶意攻击。别问我怎么知道的——有人用 10MB 的 JSON 把我服务打崩过。
  4. 优雅关闭:收到 SIGTERM 时,先停止接受新连接,再逐个 close 现有连接。直接 kill -9 会导致客户端重连风暴。
  5. 降级方案:当服务端压力过大时,返回 503 并提示客户端改用轮询。不要死磕 WebSocket,有时候降级比硬撑更明智。

6. 延伸思考

  • WebSocket 和 gRPC 的流式通信:如果你们公司用微服务,gRPC 的双向流可能更合适,因为它自带服务发现和负载均衡。我最近就在迁移一个项目,体验还不错。
  • WebTransport:Chrome 已经在支持基于 QUIC 的 WebTransport,延迟更低,但生态还不成熟。我观望了一年,还是决定再等等。
  • 边缘计算:把 WebSocket 网关放到 CDN 节点,用 Cloudflare Workers 或 Fastly Compute@Edge 做连接管理,能大幅降低源站压力。这个方案我还没在生产环境试过,但测试效果很惊艳。

最后说一句:不要为了用 WebSocket 而用 WebSocket。如果你的场景只是定时拉取数据,SSE 更简单;如果是低频双向通信,HTTP 轮询加长连接池也够用。选技术栈,先看业务,再看性能。

你可能感兴趣的文章

来源:每日教程每日一例,深入学习实用技术教程,关注公众号TeachCourse
转载请注明出处: https://teachcourse.cn/4097.html ,谢谢支持!

资源分享

分类:前端 标签:, , , ,
python项目重命名后虚拟环境报错 python项目重命名后虚拟环境报错
module导入Android Studio的两种方式,你用了吧? module导入Android Studio的两
Python框架Flask开发用户登录、注册、校验功能,存储到MySQL数据库 Python框架Flask开发用户登录、
浅谈SAX 浅谈SAX

评论已关闭!