K8s livenessProbe 用 TCP 探活对 Go 服务形同虚设:goroutine 全锁死时 TCP 照样 accept,有没有真正检测 goroutine 死锁并触发自愈的探针方案?
该问题的正文、回答与讨论。
背景
K8s 1.28.9 / containerd 1.7.13 / cgroup v2 / 内核 5.15,12 节点 16C64G 混部集群。一个 Go 1.22 无状态订单查询服务(单 Pod 4C/2G,net/http 监听 :8080),livenessProbe 配的是:
livenessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3
现象
11 月 8 日 03:17,一次慢 SQL 导致 sync.Mutex 持有时间从 P99 的 2ms 飙到 45s。期间所有拿到锁的 goroutine 排队阻塞(goroutine 数从 320 涨到 12400),HTTP handler 全部卡在 lock.Lock() 上。
但 TCP 端口还在 listen、内核 accept 队列还在正常 accept 连接——livenessProbe 每 10s 探活全绿,kubelet 认为服务健康,不会触发重启。实际表现:
- 所有 HTTP 请求超时(客户端 10s timeout,5xx 打到 97%)
- 监控面板 CPU 只有 12%、内存 600MB(RSS 没涨),一片绿
- 从 03:17 到 03:52,整整 35 分钟无人工介入,因为"探针是绿的"
已尝试的排查与结果
-
改成 httpGet /health:在
/healthhandler 里检查一个"最后一次成功处理请求的时间戳"。问题是——如果死锁发生在/healthhandler 本身依赖的那把锁上(我们服务里有一把全局orderIndexLock,/health 也要读它),那 /health 自己也会被锁住,返回超时,kubelet 倒是能感知了;但如果死锁在另一条路径上,/health 照样 200。本质上是把"探针能不能工作"和"服务有没有死锁"绑到了同一把锁上,没有独立性。 -
用
runtime.NumGoroutine()做阈值告警(Prometheus + Grafana):能发现 goroutine 暴涨,但这是告警不是自愈——还是需要人来看、人来重启。而且 goroutine 数高不等于死锁(正常高并发也会几千),阈值不好定。 -
go tool pprof远程抓 goroutine dump:事后分析很有用(确认了所有 goroutine 都卡在orderIndexLock.Lock()),但这是事后手段,不能做实时探针。 -
看了 K8s 1.29 的 startupProbe:只解决"启动慢导致 liveness 误杀"的问题,对运行期死锁无帮助。
期望得到的答案
-
有没有一种探针实现能真正检测"goroutine 死锁 / 服务不可用",而不是只检测"进程活着 / TCP 端口在 listen"? 具体怎么做?比如:
- /health 端点里主动尝试获取一把"服务健康锁"(和所有业务锁独立),超时则返回 503?
- 用 Go 的
runtime.Stack()定期检查 goroutine 状态? - 用
net/http的Server.ConnState回调检测"连接建立了但 handler 永远不返回"? - 或者用 sidecar 容器做"主动探活"(发一个真实业务请求,超时则标记不健康)?
-
livenessProbe 的
failureThreshold和periodSeconds怎么配,才能既不漏报(死锁后及时重启)又不误报(GC STW pause 100ms 不重启)? Go 的 GC STW 在 1.22 已经很短(< 1ms),但大对象分配时仍有 5-10ms 的 pause,这个窗口和探针超时怎么对齐? -
有没有 Go 社区 / 大厂的标准做法? 比如 uber-go、netflix、阿里 Go 微服务框架里,对"goroutine 死锁 → 自动重启"有没有参考实现或最佳实践文档?
-
和 C 语言单线程 WDT(硬件看门狗)的映射有什么本质区别? 嵌入式里"主循环卡住 = WDT 超时 = 硬件复位"是一步到位的;Go 多 goroutine 模型里,"一个 goroutine 卡住"和"所有 goroutine 都卡住"的探针策略应该不同,K8s 层面怎么表达这个区分?
相关参考
- 站内热榜文章《K8s 探针三件套:liveness / readiness / startup 我踩过的坑与默认值建议》(id=20) 讲了探针配置通用建议,但没有深入"Go 多 goroutine 死锁"这个场景
- 站内提问《容器化 C 语言固件后,硬件 WDT 语义怎么映射到 K8s 探针》(id=1426) 是 C 单线程角度,和我的 Go 多 goroutine 场景互补