凌晨 2 点,生产集群的支付核心服务告警频发。运维监控显示该服务的 Pod 每隔 3 分钟就被 Kubernetes 重新拉起一次,Service 后端的 Endpoint 列表闪烁不断,直接导致下游交易请求大量超时。查看应用日志,未发现未捕获的异常,但 K8s 层面却频繁判定探针失败并强制重启容器。最终定位到根本原因:Liveness Probe 的 `failureThreshold` 设置过严,且 `timeoutSeconds` 小于 JVM Full GC 停顿时间,导致偶发的 GC 停顿被误判为服务死亡。本文复盘这一排查过程,剖析探针参数间的耦合关系,并提供一套经过验证的 YAML 配置修复方案。
一、故障现象与初步排查
排查初期,`kubectl get pods` 显示 Pod 状态在 `Running` 和 `CrashLoopBackOff` 之间快速切换。通过 `kubectl describe pod` 查看 Events 列表,关键信息是 `Liveness probe failed` 和 `Killing container`。这表明 kubelet 判定容器健康检查失败,触发了重启逻辑。
应用日志记录显示,每次重启前约 30 秒,JVM 都会发生一次长达 15 秒的 Full GC。应用使用 G1 垃圾收集器,且堆内存设置较大,高负载下 GC 停顿时间(STW)有时超过 10 秒。此时,HTTP 探针的 `timeoutSeconds` 默认为 1 秒,`periodSeconds` 为 10 秒。当 GC 发生时,探针请求无法在 1 秒内返回 `200 OK`,kubelet 累计失败次数达到 `failureThreshold`(设为 3),立即触发重启。重启后内存释放,服务短暂恢复,但不久后再次触发 GC,形成死循环。
二、K8s 探针机制深度解析
理解故障需先厘清 Kubernetes 三种探针的区别。Liveness Probe(存活探针)判断容器是否正在运行,若失败,kubelet 会杀死并重启容器;Readiness Probe(就绪探针)判断容器是否准备好接收流量,若失败,Service 会将其从 Endpoints 中移除,但不重启容器;Startup Probe(启动探针)针对慢启动应用,在成功前替代 Liveness 和 Readiness 探针。
| 探针类型 | 失败后果 | 默认行为 | 适用场景 |
|---|---|---|---|
| Liveness | 杀死容器并重启 | 若未设置,则禁用 | 死锁检测、服务假死恢复 |
| Readiness | 移除 Service 后端,不重启 | 若未设置,则默认就绪 | 数据库连接池未就绪、静态资源未加载 |
| Startup | 杀死容器并重启 | 若未设置,则禁用 | 慢启动应用、大体积镜像解压 |
三、参数陷阱:Timeout 与 GC 停顿的冲突
许多工程师直接沿用 Helm Chart 或脚手架生成的默认探针配置,忽略了底层运行时的特性。JVM 应用、Node.js 高内存服务或 Python 复杂依赖加载过程,其响应延迟波动极大。若 `timeoutSeconds` 设置过短(如 1s 或 2s),极易在正常抖动中被误杀。
此外,`failureThreshold` 和 `periodSeconds` 共同决定了探针的总容忍窗口。例如,`periodSeconds: 10` 和 `failureThreshold: 3` 意味着仅有 30 秒的缓冲期。若服务启动耗时 45 秒,且没有 Startup Probe,Liveness Probe 会在启动阶段就开始倒计时,导致启动即失败。对于此类场景,参考 Spring Boot 线程池配置不当导致服务雪崩 中的资源预热思路,在探针层面通过 Startup Probe 给予足够的 `failureThreshold` 缓冲是更优解。
四、修复方案:YAML 配置实战
针对上述 JVM 应用,我们重构了 Deployment 的探针配置。核心调整包括:引入 Startup Probe 覆盖启动期,增大 Liveness 的超时阈值,并拉长容错窗口。
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-service
spec:
template:
spec:
containers:
- name: payment-app
image: registry.example.com/payment-service:v2.1.0
ports:
- containerPort: 8080
# 1. 启动探针:允许 300 秒内启动完成,超时 10 秒
startupProbe:
httpGet:
path: /health/startup
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 10
failureThreshold: 30 # 30 * 10s = 300s 缓冲
# 2. 存活探针:针对 GC 停顿优化
livenessProbe:
httpGet:
path: /health/liveness
port: 8080
initialDelaySeconds: 0 # Startup 成功后,Liveness 开始工作
periodSeconds: 20
timeoutSeconds: 15 # 大于最大 GC 停顿时间
failureThreshold: 3 # 3 * 20s = 60s 容忍窗口
# 3. 就绪探针:流量入口控制
readinessProbe:
httpGet:
path: /health/readiness
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
successThreshold: 1
注意,`timeoutSeconds` 必须显著小于 `periodSeconds` 以避免探针执行重叠。在 Liveness 中,我们将 `periodSeconds` 调整为 20,`timeoutSeconds` 保持 15。这意味着即使一次探针超时,下一次探针也会在足够间隔后触发,确保 kubelet 不会因探针队列堆积而误判服务状态。
五、健康检查端点设计规范
探针不仅依赖 K8s 参数,更依赖应用提供的健康检查端点。许多团队直接复用 `/actuator/health` 作为所有探针的入口,这是隐患。该端点默认检查数据库连接、磁盘空间等,任何依赖项抖动都会导致探针失败。
最佳实践是将健康检查分层:
1. **Liveness 端点**:仅检查进程是否存活、内部线程池是否未死锁,不涉及外部依赖(DB、Redis)。例如,仅返回 HTTP 200,或检查本地 JMX 指标。
2. **Readiness 端点**:检查核心依赖连通性。如果数据库短暂不可用,Readiness 失败,K8s 停止向该 Pod 发流量,但不重启,等待依赖恢复。
3. **Startup 端点**:检查应用初始化关键步骤(如 Hibernate 元数据构建、外部服务注册)是否完成。
如果端点实现不当,即使 K8s 参数再完美,也会误判。可参考 Redis 集群脑裂导致数据丢失复盘 中关于依赖状态一致性监控的方法,将探针端点设计为轻量级的状态机检查。
六、GC 调优与探针联动
探针参数调整是“治标”,JVM 参数优化是“治本”。在支付服务案例中,后续团队通过调整 GC 策略,将 Full GC 频率从每周一次降低到每月一次,并缩短 STW 时间至 500ms 以内,从根源上降低了探针误杀概率。
# JVM 启动参数建议
java -jar app.jar \
-Xms512m -Xmx1024m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=4m \
-XX:+DisableExplicitGC \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/tmp/heapdump.hprof
其中 `-XX:MaxGCPauseMillis=200` 设定目标停顿时间,`-XX:+DisableExplicitGC` 防止应用代码中误调用 `System.gc()` 触发额外停顿。结合探针 `timeoutSeconds: 15`,即便出现异常长 GC,也有足够缓冲。
七、监控与告警完善
修复后,需在 Prometheus 中采集 K8s 探针指标。关键指标包括 `kube_pod_container_status_restarts_total`(容器重启次数)、`probe_duration_seconds`(探针执行耗时)、`probe_success`(探针成功状态)。
设置告警规则:若单个 Pod 在 5 分钟内重启次数 ≥ 2 次,触发 `PodRestartFrequent` 告警。同时监控 `kube_pod_status_phase` 为 `Failed` 或 `Unknown` 的情况。通过 Grafana 面板可视化探针耗时分布,识别 GC 长尾延迟,为后续 JVM 调优提供数据支撑。
八、多语言应用的探针差异
不同运行时对探针的响应特性不同。Node.js 为单线程事件循环,若主线程被同步计算阻塞,探针请求会排队,此时 `timeoutSeconds` 需放宽至 10s 以上。Python 服务若使用 GIL,同样存在阻塞风险。Go 服务因协程调度灵活,通常 `timeoutSeconds: 2` 即可满足需求。但无论语言,启动阶段的依赖初始化(如 DNS 解析、服务发现)都可能引入长延迟,建议所有服务统一配备 Startup Probe,并参考 Kafka 消息积压排查实战 中的消费滞后定位方法,优化启动期的资源加载顺序。
结语
K8s 探针配置并非“一次性设置”,而是需要随应用负载特性、JVM 参数、依赖架构持续迭代的过程。核心原则是:Liveness 探针要宽松以避免误杀,Readiness 探针要精准以保护流量,Startup 探针要充足以覆盖冷启动。建立探针耗时监控、结合 JVM GC 指标、分层设计健康端点,才能构建出稳定可靠的容器化服务。在后续生产中,建议将探针参数纳入 Helm Values 统一管理,并在 CI/CD 流水线中增加探针配置校验步骤,防止默认值回归。




