如何设计一个支持 100 万在线用户的 WebSocket 推送系统?
设计一个支持 100 万(1M)并发在线用户的 WebSocket 推送系统,是一个典型的C1000K问题。这不仅考验代码逻辑,更考验架构设计、系统调优和运维能力。
核心挑战在于:连接维持资源消耗、消息路由准确性、高并发下的稳定性以及网络带宽压力。
以下是分层架构设计方案:
1. 总体架构设计 (High-Level Architecture)
我们将系统拆分为三层:接入层(Gateway)、逻辑层(Logic)、路由与存储层(State & Broker)。
plaintext
graph TD
Client[客户端 (Mobile/Web)] -->|WebSocket| LB[负载均衡 (LVS/Nginx)]
LB -->|分发连接| GW1[接入网关 Node 1]
LB -->|分发连接| GW2[接入网关 Node 2]
LB -->|...| GWN[接入网关 Node N]
GW1 <-->|注册/心跳| Redis[Redis (Session存储)]
GW2 <-->|注册/心跳| Redis
Service[业务系统 (API/Backend)] -->|推送请求| MQ[消息队列 (Kafka/RocketMQ)]
MQ --> Dispatcher[分发服务 (Router)]
Dispatcher -->|查询位置| Redis
Dispatcher -->|RPC/gRPC| GW1
Dispatcher -->|RPC/gRPC| GW2
核心组件说明:
- 接入网关 (Push Gateway):
- 职责: 仅负责维护长连接(WebSocket)、协议解析、心跳检测、鉴权。不处理复杂业务逻辑。
- 特点: 有状态(持有连接),IO 密集型。
- 会话管理器 (Session Manager - Redis):
- 职责: 维护
UserID<->GatewayIP的映射关系。
- 职责: 维护
- 分发服务 (Dispatcher/Router):
- 职责: 消费业务系统的推送请求,查询 Redis 找到用户所在的网关,将消息转发给对应的网关。
- 消息队列 (Message Queue):
- 职责: 削峰填谷,解耦业务系统和推送系统。
2. 详细设计与技术选型
2.1 接入网关 (Gateway) 选型
要维持 100 万连接,内存和上下文切换是瓶颈。
- 语言: 推荐 Go (Goroutine 协程极轻量,几 KB 内存) 或 Java (Netty, NIO 模型)。Node.js 也可以,但在多核利用上不如 Go/Java 方便。
- 单机容量: 假设单机 16GB 内存,4核 CPU。
- 一个 WebSocket 连接消耗约 4KB-10KB 内存(Go)。
- 100 万连接理论只需 ~10GB 内存。
- 保守策略: 单机承载 5万-10万 连接。100万用户需要 10-20 台服务器。
2.2 操作系统内核调优 (Kernel Tuning)
Linux 默认配置无法支持高并发连接,必须修改 /etc/sysctl.conf:
- 文件句柄限制:
fs.file-max = 1000000(系统级),ulimit -n 1000000(进程级)。 - TCP 参数:
net.ipv4.tcp_mem,net.ipv4.tcp_rmem,net.ipv4.tcp_wmem: 调整 TCP 读写缓冲区大小,防止 OOM。net.ipv4.tcp_max_tw_buckets: 增加 TIME_WAIT 桶大小。net.core.somaxconn: 增加握手队列长度,防止连接风暴时拒绝连接。
2.3 消息路由策略 (核心逻辑)
当业务后台要给 User A 发消息时,如何找到它?
- 连接建立:
- User A 连接到 Gateway-01。
- Gateway-01 在 Redis 中写入:
SET user:A:location gateway-01(设置 TTL)。
- 消息推送:
- 业务方发送消息
{uid: A, content: "hello"}到 MQ。 - Dispatcher 消费消息,查询 Redis
GET user:A:location-> 得到gateway-01。 - Dispatcher 通过 gRPC 调用 Gateway-01 的接口,将消息投递过去。
- Gateway-01 通过内存中的 Map 找到 User A 的 WebSocket 连接,写入数据。
- 业务方发送消息
2.4 广播与群推 (Broadcast)
如果是全员广播(如系统公告):
- 不要遍历 Redis 查找所有用户。
- 方案: 所有的 Gateway 节点订阅一个公共的 MQ Topic(或 Redis Pub/Sub Channel)。
- 消息发到 Topic,所有 Gateway 收到后,遍历自己本机内存中的所有连接进行下发。
3. 稳定性与可靠性设计
3.1 心跳检测 (Heartbeat)
- 目的: 检测死链(如拔网线、移动网络切换 IP),释放服务器资源。
- 实现: 客户端每 30s 发送
PING,服务端回复PONG。 - 策略: 服务端如果 60s-90s 未收到心跳,强制断开连接并清除 Redis 映射。
3.2 消息必达 (QoS)
WebSocket 本身是 TCP,保证传输层可靠,但应用层可能丢消息(如发消息瞬间用户断线)。
- ACK 机制: 服务端下发消息带
msg_id,客户端收到后回复ACK(msg_id)。 - 重试队列: 服务端维护一个“已发送未确认”队列,超时未收到 ACK 则重发。
- 离线消息: 如果用户不在线,将消息存入数据库(MySQL/HBase),用户下次登录时拉取。
3.3 避免连接风暴 (Connection Storm)
如果一台承载 10 万用户的 Gateway 宕机,这 10 万用户会瞬间尝试重连,可能压垮负载均衡或 Redis。
- 退避算法: 客户端重连必须使用 指数退避 + 随机抖动 (Jitter)。
- 例如:第一次 1s 重连,第二次 3s,第三次 7s... 且加上随机数,避免所有客户端在同一毫秒发起请求。
3.4 负载均衡策略
- LVS/F5 (4层): 处理入口流量。
- Nginx (7层): 可以做 WebSocket 代理,但对于 100 万长连接,Nginx 可能成为瓶颈。
- 推荐: 客户端先请求一个 HTTP 接口
/get-server,该接口根据当前各 Gateway 节点的负载情况,返回一个可用的 Gateway IP,客户端再直连该 IP。这样可以实现更精细的负载均衡。
4. 协议与数据格式
- 传输协议: WebSocket (基于 TCP)。
- 数据格式:
- JSON: 开发简单,可读性好,但流量大。
- Protobuf: 推荐。二进制压缩,体积小,省带宽(100万用户每人省 1KB 就是 1GB 流量)。
5. 监控与运维
- 指标:
- 当前在线连接数 (Gauge)。
- 消息吞吐量 (Counter)。
- 消息投递延迟 (Histogram)。
- 连接断开率/错误率。
- 工具: Prometheus + Grafana。
6. 总结:容量规划示例
假设目标 100 万在线:
- Gateway 服务器: 4C8G 或 4C16G 虚拟机。
- 单机承载 50,000 连接。
- 需要 20 台机器。
- Redis: Cluster 模式。
- 存储 100 万个 Key,内存占用很小(几百 MB),主要压力在 QPS。
- 3 主 3 从即可满足。
- 带宽:
- 假设心跳包 50 Bytes,30s 一次。
- 心跳流量: 1,000,000 * 50B / 30s ≈ 1.6 MB/s (非常小)。
- 推送流量: 假设每秒推送 1000 条消息,每条 1KB。
- 推送流量: 1MB/s。
- 瓶颈: 瓶颈通常在广播或大图片/大文本推送,需严格限制包体大小。
核心难点总结
- 内存优化: 使用 Go/Netty 等高性能网络库。
- 内核调优: 打开文件句柄和 TCP 限制。
- 路由准确性: Redis 维护 Session 映射。
- 容灾: 客户端重连机制(Jitter)防止雪崩。
右滑查看面试常问