WebSocket 连接池优化:百万并发的连接治理
WebSocket 不是「长连接就完事」。本文分享一次从单机 3 万连接到 35 万的压测调优过程,包括 fd 限制、握手风暴、心跳分组、压缩算法等问题的工程方案。
为什么 WebSocket 是个不一样的挑战
HTTP 是「请求 - 响应」模型,每个连接服务一个请求。WebSocket 则是「长连接 - 双工」模型,每个连接要占用服务端 fd 与内存直到连接关闭。这种差别决定了 WebSocket 服务的容量规划与 HTTP 完全不同。
我在做某 IM 推送系统时,目标是「单机房承载 80 万 WebSocket 连接」。这是一个相当大的体量,对每一层都有挑战:
- 内核:fd 上限、TCP 半连接队列。
- 运行时:协程调度、内存占用、握手 storm。
- 协议:心跳、分片、压缩。
文件描述符:机器上限的硬伤
默认 Linux 单进程 fd 上限是 1024,这是历史包袱。要承接 80 万连接,必须改:
cat /proc/sys/fs/file-max # 系统级 fd 上限
ulimit -n # 单进程 fd 上限
# 修改 /etc/security/limits.conf
* soft nofile 1048576
* hard nofile 1048576
# 注意:必须改 systemd.conf 的 LimitNOFILE
接下来要让进程 fd 上限真的生效,还需要重新登录或重启后由 systemd 注入。这是很多团队踩过的坑。
握手风暴:连接建立那 200ms
真实世界的 80 万连接不是「慢慢增长」而是「秒杀瞬间涌入」。这带来握手风暴:
- TLS 握手成本(每次 2–5ms CPU)。
- 升级协议 Upgrade 请求的握手。
- 认证(JWT / Cookie / 风控)。
一次 80 万的连接涌入在 5 分钟内完成,相当于每秒 2700 个。优化方案:
- TLS 会话票据复用(Session Ticket)。
- 用 ECDHE + ChaCha20 减少握手计算。
- 认证降级为「连接后 first packet」承载,而不是握手阶段。
协程模型:每连接 1 协程 vs N:M
Go 的 goroutine 看起来很轻量,但每个连接叠加 net/conn 缓冲区还是几十 KB。80 万 × 50KB ≈ 40GB 内存,这是不可接受的。
优化手段:
- 使用 epoll / io_uring 单线程多连接模型。
- 协程池(goroutine pool)限制单实例 goroutine 数量。
- 在 Rust / C++ 服务里用 tokio 的 per-thread reactor。
// Go 中用 worker pool 限制协程数
var sem = make(chan struct{}, 1000) // 最多 1000 个写协程
func write(conn *websocket.Conn, msg []byte) {
sem <- struct{}{}
defer func() { <-sem }()
conn.WriteMessage(websocket.BinaryMessage, msg)
}
心跳:最容易踩的几个坑
WebSocket 必须有心跳,否则 NAT 表老化或代理超时会让连接「假死」。但心跳本身有几个坑:
- 心跳太频繁:手机端耗电与流量突增。
- 心跳没分组:所有连接同一时刻 PING,服务端瞬时 CPU 高峰。
- 心跳协议不一致:服务端发 PING、客户端回 PONG,但丢失时客户端不退出。
心跳设计的本质是「统计学」:让客户端的失活事件能在 P95 时间内被服务端识别。
压缩:permessage-deflate
WebSocket 子协议支持 permessage-deflate 压缩。文本消息压缩后体积可降 70%。代价是:
- CPU 增加。
- 延迟增加(压缩 / 解压)。
- 不适合高频小包。
经验值:内容平均 > 256 字节建议开启;< 64 字节建议关闭。
连接分配策略:把 80 万均匀分布
80 万连接不是一台机器能扛的,必须 N 台机器做集群。三类分配:
| 策略 | 特点 | 适用 |
|---|---|---|
| Hash by user_id | 同用户落到同机器 | 用户粘性场景 |
| 一致性 hash | 扩容迁移少 | 通用推送 |
| 按地域 / ISP | 延迟低 | 实时游戏 |
我们最终选择「一致性 hash + 备用机器」组合。扩容时只迁移 ~1/10 连接,业务无感知。
灰度与回滚
WebSocket 的版本发布比 HTTP 难,因为它不能「keep old + new together」太久。常见做法:
- 所有握手带
version=...。 - 网关识别 version,路由到对应实例。
- 老连接维持不动,新连接走新版本,老连接空闲过期后掉。
这种「冷静」回滚保证升级过程不强行切断存量用户。
监控:WebSocket 的核心指标
- 当前连接数:每实例的活跃连接。
- 握手成功率:5xx、TLS 错误、Auth 错误分类。
- Ping/Pong 失效率:代表 NAT 老化频次。
- 流量进出:每秒消息数、平均消息大小。
- 半连接 / full connection ratio。
容量规划:从数字到带宽
把 80 万连接换算到每个组件的容量:
- CPU:单实例 8 核可服务 8–10 万连接。
- 内存:每连接 ~10KB(粗算),80 万 ≈ 8GB。
- 带宽:每连接 1KB/s 心跳,80 万 ≈ 800 MB/s。
这意味着至少需要 10 个 8 核实例、千兆内网、专门的边缘节点。
一些非技术但很关键的事
- 连接治理的「命门」是断开能力。要能在 5 秒内批量踢掉指定用户的所有连接。
- 心跳协议需要在客户端、服务端、SDK 三方对齐。
- 突发事件可能要短时间强切,必须有「主动 close」通道。
WebSocket 是个看似简单、实际比 HTTP 更复杂的协议。它每个连接都像「管理一条小专线」,是另一种工程哲学。做好它,才能让实时交互成为常态。