StatefulSet缩容时长连接中断,根本原因是Service流量摘除与Pod终止时序未对齐:readinessProbe未动态响应SIGTERM、preStop缺失或过短、terminationGracePeriodSeconds不足,导致Endpoint未及时更新而新请求仍被路由至终止中Pod。
go 有状态长连接服务在 kubernetes 中缩容时出现连接中断、请求丢失或 connection refused,不是应用没关好,而是 service 流量摘除和 pod 终止的时序没对齐——尤其在 statefulset + 长连接场景下,问题更隐蔽。
为什么 StatefulSet 缩容时长连接会断?
StatefulSet 默认不自动触发就绪状态变更;Pod 收到 SIGTERM 后,TCP 连接不会立刻关闭,但 Service 的 Endpoints 可能仍包含该 Pod,新请求或重试请求继续打进来,而老连接又因应用未主动 close 或未完成 graceful shutdown 被丢弃。
- 就绪探针(
readinessProbe)没配置或路径没动态响应终止信号,导致 Pod 在Terminating状态下仍被流量命中 -
preStophook 缺失或执行过快,没给就绪探针失败+Endpoint 更新留出缓冲时间 - Go 应用未监听
SIGTERM并调用http.Server.Shutdown(),导致监听端口僵死、连接无法优雅 drain - StatefulSet 缩容顺序是逆序(
pod-2→pod-1),但若应用未按拓扑感知方式迁移长连接归属,数据/会话一致性可能受损
必须配的三个字段:readinessProbe + preStop + terminationGracePeriodSeconds
这三个字段要协同生效,缺一不可。只配其中一两个,大概率还是丢连接。
-
readinessProbe必须指向一个可写状态的 HTTP 接口(如/readyz),并在收到SIGTERM后立即返回503。不能只靠initialDelaySeconds和periodSeconds被动等待 -
preStop建议用exec主动触发就绪接口降级,再 sleep 等待一次 probe 周期(比如periodSeconds: 5,那就 sleep 6s):lifecycle: preStop: exec: command: ["/bin/sh", "-c", "curl -f http://localhost:8080/readyz?down=true || true; sleep 6"] -
terminationGracePeriodSeconds至少设为30(不能是默认 30 秒但被其他逻辑覆盖)。它决定了从发送SIGTERM到强制SIGKILL的总窗口,必须 ≥ preStop 执行时间 + 最后一次 readiness probe 失败 + Endpoint 更新传播延迟(通常 2–5s)
Go 应用层必须做三件事
光靠 YAML 配置拦不住长连接中断。Go 程序得自己管好连接生命周期。
- 用
http.Server而非http.ListenAndServe,并显式调用srv.Shutdown(ctx)—— 它会拒绝新连接、等待已有连接完成或超时 - 维护一个全局
isShuttingDown标志,在SIGTERMhandler 中置true,让/readyz返回503,也让长连接处理逻辑(如 WebSocketconn.ReadMessage())提前退出 - 对每个长连接 goroutine 加上 context 控制,例如
conn.SetReadDeadline(time.Now().Add(30 * time.Second)),避免阻塞在读上无法响应 shutdown
StatefulSet 缩容前务必确认 PVC 行为
有状态长连接服务常附带本地 session 存储或嵌入式数据库(如 Badger、Bolt)。缩容时如果 Pod 挂载了 PersistentVolumeClaim,Kubernetes 不会自动迁移或合并数据。
- 检查
volumeClaimTemplates中的reclaimPolicy:若为Retain,缩容后 PVC/PV 会残留,下次扩容可能挂载旧数据,造成状态错乱 - 不要依赖
preStop在容器内做跨 Pod 数据同步——网络不可靠、超时难控、且 StatefulSet 缩容不保证目标 Pod 一定在线 - 真正可靠的方案是把长连接元数据(如 client ID → node mapping)抽离到外部协调服务(如 etcd、Redis),由所有 Pod 共享并 watch 变更
最易被忽略的一点:kube-proxy 的 iptables/ipvs 规则更新不是原子的,Endpoint 删除后,部分节点上的规则可能滞后 1–3 秒。这意味着即使你把 readiness/preStop/shutdown 全配对了,仍可能有极少量请求落在正在销毁的 Pod 上——这时 Go 应用的 Shutdown 超时值(建议 15–30s)就是最后防线,必须比 terminationGracePeriodSeconds 小至少 5 秒,留出 kernel TCP TIME_WAIT 清理余量。











