【背景】
做 K8s 容器化运维 5 年,探针配置是每个集群都绕不开、但几乎没人认真讨论过的配置项。我复盘过两次线上事故,恰好踩在两个极端:
事故一(重启风暴):一个 Java 订单服务,livenessProbe 配的是 httpGet /health,timeout=1s,failureThreshold=1,periodSeconds=5。某次 Full GC 持续 3.2s,kubelet 判定 liveness 失败 → kill Pod → 新 Pod 冷启动又 GC → 连续重启 4 轮,3 个副本全灭,服务断流 6 分钟。事后看 logs,GC log 里那次 Full GC 的触发原因是 Metaspace 膨胀,和 liveness 检测的 /health 接口毫无关系——探针检测的是「进程能不能应答」,不是「进程健不健康」。
事故二(流量黑洞):一个 Go 微服务,readinessProbe 配的是 httpGet /ready(查 DB + Redis),timeout=30s,failureThreshold=2。某次节点网卡 flapping,Pod 网络中断 12s,但 readiness 还在 timeout 等待窗口内没翻红,Service Endpoint 没摘除,流量继续打进来 → 502 雪崩,持续 40s。readiness 的「慢」比 liveness 的「快」更致命,因为 liveness 误杀最多丢一个副本,readiness 漏摘是整条链路的问题。
【常见分歧 / 几种做法】
我观察到社区里大致分四种流派:
- 「探针只判进程存活」派:liveness 只做 TCP 端口检测(
tcpSocket),readiness 才做业务健康。超时给足(liveness 10s × 3 次),宁可多活 30 秒不要误杀。代表论据:「K8s 文档原话,liveness 是 for detecting a failure of the container」。 - 「探针就是健康检查」派:readiness 里查 DB、下游 RPC、缓存,任何一项不健康就摘。超时要短(3~5s),快速摘除是核心价值。代表论据:「readiness 的设计意图就是告诉 kube-proxy 该不该导流」。
- 「探针只是兜底」派:K8s 探针是最后防线,平时靠 Envoy sidecar / HAProxy / LB 做主动 TCP 层探测,业务层健康靠 APM 告警驱动人工介入。代表论据:「把探针当唯一健康信号,等于把 3 秒的 timeout 当成 SLO」。
- 「JVM / 解释器应用特殊对待」派:JVM 冷启动 + JIT + 首次 GC 的启动曲线和 Go/Rust 完全不同,liveness 超时至少 10s、failureThreshold 至少 3 次,startupProbe 必配,否则启动期必挂。代表论据:「JVM 应用前 30 秒的 /health 返回 200 不代表它能扛流量」。
【我的倾向与理由】
我倾向于「分层 + 非对称保守」,核心原则是 liveness 误判的代价 > readiness 误判的代价,所以 liveness 要保守(慢),readiness 要激进(快):
# liveness:只判进程存活,保守
livenessProbe:
tcpSocket:
port: 8080 # 不查业务接口,只查端口
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3 # 最多容忍 30s 无响应才 kill
# readiness:判业务健康,激进
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 3
timeoutSeconds: 3
failureThreshold: 2 # 6s 内摘流量
# startupProbe:JVM 应用必配
startupProbe:
httpGet:
path: /health
port: 8080
failureThreshold: 60 # 最多 10min 启动窗口
periodSeconds: 10
理由:
- liveness 用
tcpSocket而不是httpGet:避免把「GC 卡了 3 秒」等同于「进程死了」。TCP 端口能 accept 就说明内核网络栈活着,JVM 层的问题交给应用自己解决(优雅降级、请求排队)。 - readiness 用
httpGet /ready查业务依赖:这是 readiness 的设计意图,快速摘流量比「等 30 秒确认」好。 - startupProbe 给足窗口:JVM 应用前 30~60 秒的
/health返回 200 不代表 JIT 热了,但也不能无限等。
但有一个我至今没想清楚的问题:K8s 探针是「单点」检测(kubelet → Pod IP),而容器网络是「多跳」的(Pod → CNI → Service/Endpoint → LB → 用户)。如果中间某跳(CNI bridge、iptables 规则、LB 健康检查)出现抖动,探针结果和实际用户体感可能完全不一致。我在事故二里就碰到了:kubelet 的 readiness 说「活着」,但 LB 那头已经 502 了。你们线上是怎么处理这种「探针与用户体感脱节」的?
【想听大家在什么场景下怎么选】
- 你们的 liveness 到底检测什么?纯
tcpSocket还是httpGet?有没有人因为 liveness 误杀过 Pod? - JVM 应用启动期探针怎么配才不炸?
startupProbe你们用了吗?K8s 版本够不够(1.28+ 才稳定)? - 有没有遇到过「探针说活着但用户已经 502」的情况?最后怎么定位的?是 CNI 层、LB 层还是应用层?
- 滚动更新时,
maxSurge/maxUnavailable和探针配置有没有联动考虑过?比如maxUnavailable=0+ liveness 太激进 = 更新期间副本被杀 → 流量全压到旧副本 → 旧副本过载 → 更多 Pod 被杀。这个正反馈回路你们踩过吗? - 你们有没有把「探针配置」纳入 SLO 管理?比如定义「探针误判率 < 0.1%」作为平台级指标?