基于本文回答
0
评论

如何设计一个支持 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

核心组件说明:

  1. 接入网关 (Push Gateway):
    • 职责: 仅负责维护长连接(WebSocket)、协议解析、心跳检测、鉴权。不处理复杂业务逻辑
    • 特点: 有状态(持有连接),IO 密集型。
  2. 会话管理器 (Session Manager - Redis):
    • 职责: 维护 UserID <-> GatewayIP 的映射关系。
  3. 分发服务 (Dispatcher/Router):
    • 职责: 消费业务系统的推送请求,查询 Redis 找到用户所在的网关,将消息转发给对应的网关。
  4. 消息队列 (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 发消息时,如何找到它?

  1. 连接建立:
    • User A 连接到 Gateway-01。
    • Gateway-01 在 Redis 中写入: SET user:A:location gateway-01 (设置 TTL)。
  2. 消息推送:
    • 业务方发送消息 {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 万在线:

  1. Gateway 服务器: 4C8G 或 4C16G 虚拟机。
    • 单机承载 50,000 连接。
    • 需要 20 台机器。
  2. Redis: Cluster 模式。
    • 存储 100 万个 Key,内存占用很小(几百 MB),主要压力在 QPS。
    • 3 主 3 从即可满足。
  3. 带宽:
    • 假设心跳包 50 Bytes,30s 一次。
    • 心跳流量: 1,000,000 * 50B / 30s ≈ 1.6 MB/s (非常小)。
    • 推送流量: 假设每秒推送 1000 条消息,每条 1KB。
    • 推送流量: 1MB/s。
    • 瓶颈: 瓶颈通常在广播大图片/大文本推送,需严格限制包体大小。

核心难点总结

  1. 内存优化: 使用 Go/Netty 等高性能网络库。
  2. 内核调优: 打开文件句柄和 TCP 限制。
  3. 路由准确性: Redis 维护 Session 映射。
  4. 容灾: 客户端重连机制(Jitter)防止雪崩。
右滑查看面试常问