Go长连接网关在K8s缩容时必须主动管理生命周期:收到SIGTERM后立即关闭listener拒绝新连,主动向每个WebSocket连接发送CloseMessage并等待其自然断开,用WaitGroup协调所有读写goroutine退出,readinessProbe仅控制流量接入,不参与终止流程,无法替代优雅关闭。

Go长连接网关在Kubernetes弹性伸缩时,Pod被销毁前若未正确处理存量连接,必然导致消息丢失、客户端断连重试、服务端重复推送等故障——这不是配置问题,而是生命周期管理缺失的必然结果。
为什么 readinessProbe 不能代替优雅下线
很多团队误以为只要配置了 readinessProbe,Kubernetes 就会“自动”等连接关闭后再删 Pod。事实是:readinessProbe 只影响流量是否继续打进来,完全不干预 Pod 终止流程。一旦 terminationGracePeriodSeconds 超时(默认30秒),kubelet 直接发 SIGTERM,接着是 SIGKILL,不管你的 WebSocket 连接还在不在收包。
- 现象:客户端突然收到
1006错误,或心跳超时后断连 - 根本原因:
net.Conn.Close()被粗暴中断,未完成 FIN/ACK 握手 - 关键点:必须在
SIGTERM收到后主动停止 accept 新连接,并等待所有活跃连接自然关闭或强制超时退出
Go 里实现优雅关闭的核心三步
不是加个 defer 就行,得控制监听器、连接池、业务协程三个层面的退出节奏。
- 监听器层:调用
listener.Close()后,Accept()立即返回错误,后续新连接被拒绝 - 连接层:对每个活跃
*websocket.Conn,需调用conn.WriteMessage(websocket.CloseMessage, ...)主动发关闭帧,再conn.Close() - 协程层:用
sync.WaitGroup或context.WithCancel管理所有读写 goroutine,确保它们收到信号后自行退出,而非被强制终止
示例关键逻辑:
func (s *Server) Shutdown(ctx context.Context) error {
s.listener.Close() // 停止接收新连接
close(s.shutdownCh) // 通知所有连接 goroutine 开始退出
return s.wg.Wait() // 等待所有连接处理完毕
}
HPA 触发缩容时,连接数指标必须带延迟上报
直接拿 Prometheus 的 go_conn_active_total 这类瞬时指标做 HPA 阈值,极易造成“抖动性缩容”:连接刚建立就被算进指标,还没稳定就触发缩容,结果新旧 Pod 同时承担压力又同时下线部分连接。
- 正确做法:用
rate(go_conn_active_total[5m])或更稳妥的avg_over_time(go_conn_active_total[2m]),平滑突刺 - 额外要求:在 Pod 接收
SIGTERM后,应主动将自身连接数指标设为 0 并上报一次,避免 HPA 因指标残留继续误判 - 验证点:缩容前观察
kubectl get pods -w和ws://.../metrics,确认指标下降与 Pod Terminating 状态严格同步
客户端必须配合实现重连退避 + 连接复用
服务端再优雅,也架不住客户端暴力重连。尤其在批量缩容时,若所有客户端在 SIGTERM 后 1 秒内全量重连,等于制造了一次 DDoS。
- 必须禁用无脑重连:WebSocket onclose 里不能直接
new WebSocket(...) - 推荐策略:指数退避(1s → 2s → 4s → 8s)+ 最大重试次数(如5次)+ 随机抖动(±30%)
- 连接复用:一个页面只持有一个 WebSocket 实例,不要按业务模块拆多个连接;用消息 type 字段区分文档同步、评论、弹幕等子通道
真正容易被忽略的是:客户端重连请求打到新 Pod 后,服务端必须能识别并恢复该用户的会话上下文(比如从 Redis 加载 session ID 关联的房间列表),否则即使连接不断,业务状态也是断裂的。


















