WebSocket网关需连接粒度路由、协议状态感知和可插拔会话存储,Nginx仅适合TLS终结与初级路由;APISIX适合快速上线,Envoy适合服务网格化场景,高并发定制需求应自研Netty网关。

WebSocket 网关不是“配个 proxy_set_header Upgrade $http_upgrade 就能扛百万连接”的事。选错或设计失当,轻则连接数上不去、消息乱序丢包,重则 CPU 扛不住、Redis 成瓶颈、监控完全失焦。
为什么 Nginx 做不好 WebSocket 接入层?
它能透传,但不“管理”——Nginx 的 upstream 是进程级负载,一个 worker 进程挂了,它上面所有 WebSocket 连接就断;它不感知连接生命周期,无法做心跳续约、连接迁移、会话广播。
常见症状包括:
-
101 Switching Protocols成功,但 30 秒后无声断连(Nginx 默认proxy_read_timeout60s,但没心跳保活逻辑) - 客户端反复重连,
ConnectionManager在业务侧自己实现,却和 Nginx 负载策略冲突 - 用
Redis Pub/Sub做跨节点广播,但消息时序无法保证,文档协同场景出现“A 看到删行,B 还显示旧内容”
真正需要的是:连接粒度路由 + 协议状态感知 + 可插拔的会话存储。Nginx 只适合做 TLS 终结 + 初级路由,后面必须跟一层有状态网关。
APISIX vs Envoy vs 自研 Netty 网关怎么取舍?
看你的控制粒度需求和团队能力边界:
- 要快速上线、已有 Lua 生态、运维不想碰 C++:选
APISIX。它的websocket-proxy插件支持连接级限流、自定义子协议协商,且etcd配置热更新不 reload,实测单节点稳住10w+连接没问题 - 已深度集成 Istio、服务网格化成熟:用
Envoy。它对WebSocket的upgrade处理更细(比如可配置max_frame_size、idle_timeout),但调试成本高,envoy.yaml写错一行可能整个连接池卡死 - 连接峰值 >50w、需定制心跳压缩/二进制帧解析/与内部鉴权中心强耦合:必须自研。石墨文档 2.0 和爱奇艺都走了这条路,核心是用
Netty的ChannelGroup管理连接,用Redis+Kafka拆分“连接状态”和“业务事件”,避免把ConnectionManager和MessageRouter塞进同一个模块
连接管理模块最容易被忽略的三个细节
不是存个 Channel 引用就完事。真实压测中,80% 的内存泄漏和 CPU 尖刺出在这三处:
-
Channel关闭后,没从ChannelGroup或本地ConcurrentHashMap中移除引用,GC 不掉——务必在channelInactive()和exceptionCaught()里双重清理 - 心跳超时判定依赖系统时间(
System.currentTimeMillis()),但高并发下该方法有性能抖动;应改用HashedWheelTimer或io.netty.util.HashedWheelTimer做精度可控的延迟检测 - 用户会话 ID(如
userId:deviceId)作为 key 存 Redis,但没设 TTL 或没做连接重建时的 key 覆盖逻辑,导致“旧连接未下线,新连接被拒绝”
消息广播路径别绕远路
别让每条业务消息都走一遍 “业务服务 → Redis Pub → 所有网关节点 Sub → 本节点查本地连接表 → 发送”。这是石墨文档 1.0 的坑。
优化方向很明确:
- 单机广播:用
ChannelGroup.writeAndFlush(),别逐个channel.write() - 跨节点广播:用
Kafka替代Redis Pub/Sub,靠 topic partition 保证同用户消息顺序,再由各网关节点按userIdhash 订阅对应 partition - 精准推送:把连接路由信息注册到
etcd或Consul,业务服务先查“userId当前在哪台网关”,再直连推送,绕过广播链路
最后提醒一句:网关的“连接数”指标最骗人。真正卡脖子的,往往是 ChannelHandler 里一行同步阻塞调用,或者 Redis pipeline 没批量写,而不是你选的网关类型本身。

















