适用场景

本文适用于 Kubernetes 中业务 Pod 间歇性进入 CrashLoopBackOffRestartCount 持续增长、服务短时间不可用,但应用日志里又看不到明确业务异常的场景。常见于 Spring Boot、Django、Go HTTP 服务、Node.js 服务等 Web 应用,尤其是刚接入健康检查、上线慢启动逻辑、数据库连接初始化较慢、或者高峰期 CPU 被打满之后。

这类问题的关键点是:livenessProbe 不是流量摘除工具,而是“进程是否需要被重启”的判断条件。如果探针配置得太激进,Kubelet 会把一个仍在恢复、仍可继续服务的进程反复杀掉,最终把短暂抖动放大成持续故障。

现象描述

线上常见现象包括:

  • kubectl get pod 看到 Pod 状态反复在 RunningErrorCrashLoopBackOff 之间切换。
  • RESTARTS 持续增长,但应用日志没有 panic、异常退出或 OOM 记录。
  • 发布后前几分钟频繁重启,等待一段时间后偶尔恢复。
  • 高峰期接口延迟升高后,部分 Pod 被连续重启,反而导致剩余副本压力更大。
  • kubectl describe pod 事件中出现 Liveness probe failedKilling container

示例:

kubectl -n prod get pod -l app=order-api -o wide

输出中重点看:

NAME                         READY   STATUS             RESTARTS   AGE
order-api-6d8b9d9d7c-x7g2m   0/1     CrashLoopBackOff   12         18m
order-api-6d8b9d9d7c-pq4sm   1/1     Running            7          18m

如果多个副本同时出现重启,先不要急着重建 Deployment,要先确认重启原因是不是探针失败。

可能原因

常见原因可以分成几类:

  1. initialDelaySeconds 太短,应用还没完成启动和预热就开始检查。
  2. timeoutSeconds 太短,健康检查接口偶发超过 1 秒就被判定失败。
  3. failureThreshold 太低,短暂 GC、CPU 抖动、数据库慢连接被快速放大成重启。
  4. livenessProbe 检查了依赖项,例如数据库、Redis、第三方接口,依赖抖动时误杀应用。
  5. 健康检查接口本身太重,执行了 SQL、远程请求、复杂聚合或锁竞争。
  6. 没有配置 startupProbe,慢启动应用只能依赖 liveness 的延迟参数硬扛。
  7. CPU limit 太小导致容器被 throttling,健康检查线程无法及时响应。

排查思路

排查顺序建议如下:

  1. 先确认容器是被 Kubelet 杀掉,还是应用自己退出。
  2. 查看 Pod 事件,定位是否存在 Liveness probe failed
  3. 查看上一次容器日志,确认被杀前应用状态。
  4. 检查探针配置,重点看延迟、超时、失败阈值和检查路径。
  5. 从 Pod 内部直接访问健康检查接口,确认接口耗时和返回码。
  6. 结合资源指标判断是否存在 CPU throttling、内存压力或启动慢。
  7. 区分 startupProbereadinessProbelivenessProbe 的职责后再调整。

常用命令

1. 查看 Pod 重启情况

kubectl -n prod get pod -l app=order-api -o wide

关键字段:

  • READY:是否已经加入 Service 后端。
  • STATUS:是否出现 CrashLoopBackOffErrorRunning 等状态切换。
  • RESTARTS:容器重启次数,持续增长说明问题仍在发生。
  • AGE:如果 Pod 刚创建就重启多次,优先怀疑启动阶段探针配置。

也可以按重启次数排序:

kubectl -n prod get pod --sort-by='.status.containerStatuses[0].restartCount'

2. 查看 Pod 事件

kubectl -n prod describe pod order-api-6d8b9d9d7c-x7g2m

重点看 Events

Warning  Unhealthy  2m10s  kubelet  Liveness probe failed: HTTP probe failed with statuscode: 500
Normal   Killing    2m10s  kubelet  Container order-api failed liveness probe, will be restarted

如果看到 failed liveness probe, will be restarted,说明容器不是自己崩溃,而是被 Kubelet 根据 liveness 结果重启。

3. 查看上一次容器日志

kubectl -n prod logs order-api-6d8b9d9d7c-x7g2m --previous --tail=200

--previous 用来查看上一个已退出容器实例的日志。重点搜索:

  • 应用是否主动退出,例如 panicSystemExitFatal
  • 是否有 OOM、连接池耗尽、数据库连接失败。
  • 被杀前是否仍在处理请求,只是响应变慢。
  • 是否刚完成启动就被探针打断。

如果日志最后没有明显异常,而 Pod 事件显示 liveness 失败,问题很可能在探针策略或健康检查接口本身。

4. 查看 Deployment 中的探针配置

kubectl -n prod get deploy order-api -o yaml

重点看:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 10
  timeoutSeconds: 1
  failureThreshold: 3
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 3

字段含义:

  • initialDelaySeconds:容器启动后延迟多久开始探测。
  • periodSeconds:每隔多久探测一次。
  • timeoutSeconds:单次探测超时时间。
  • failureThreshold:连续失败多少次后判定失败。
  • successThreshold:连续成功多少次后恢复成功,liveness 必须为 1。

上面这个配置的问题是:应用启动 10 秒后就开始 liveness,单次超时只有 1 秒,连续 3 次失败就会重启。对于启动慢、GC 明显或高峰期 CPU 紧张的服务,这个配置很容易误杀。

5. 在 Pod 内测试健康检查接口

kubectl -n prod exec -it order-api-6d8b9d9d7c-pq4sm -- sh

进入容器后执行:

time wget -S -O- --timeout=3 http://127.0.0.1:8080/health
time wget -S -O- --timeout=3 http://127.0.0.1:8080/ready

重点看:

  • HTTP 状态码是否稳定为 2xx。
  • 耗时是否经常超过 timeoutSeconds
  • /health 是否依赖数据库、Redis 或外部接口。
  • 容器内访问是否和探针配置的端口、路径一致。

如果 /health 偶尔需要 2 到 3 秒,但 timeoutSeconds 只有 1 秒,就会出现业务接口还能恢复、Pod 却被重启的情况。

6. 查看资源压力

kubectl -n prod top pod -l app=order-api

如果部署了 Prometheus,可以进一步看 CPU throttling:

rate(container_cpu_cfs_throttled_periods_total{namespace="prod", pod=~"order-api-.*"}[5m])

还可以看重启趋势:

increase(kube_pod_container_status_restarts_total{namespace="prod", pod=~"order-api-.*"}[15m])

如果重启和 CPU throttling 同时升高,说明探针超时可能只是表象,底层原因是 CPU limit 太紧或高峰期负载超过预期。

定位示例

某订单服务发布后开始间歇性 502。Nginx Ingress 日志显示后端连接偶发失败,业务 Pod 的 RESTARTS 不断增加。

第一步查看 Pod:

kubectl -n prod get pod -l app=order-api

发现 6 个副本中有 3 个副本重启次数超过 10 次。

第二步查看事件:

kubectl -n prod describe pod order-api-6d8b9d9d7c-x7g2m

事件中出现:

Liveness probe failed: Get "http://10.244.12.31:8080/health": context deadline exceeded
Container order-api failed liveness probe, will be restarted

第三步查看上一次日志:

kubectl -n prod logs order-api-6d8b9d9d7c-x7g2m --previous --tail=100

日志显示容器被杀前正在初始化缓存和连接池,没有应用主动退出记录。

第四步检查探针:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 15
  periodSeconds: 5
  timeoutSeconds: 1
  failureThreshold: 3

同时发现 /health 会检查 MySQL 和 Redis。高峰期 Redis 偶发慢响应时,/health 超过 1 秒,liveness 连续失败后 Kubelet 重启容器。容器重启又导致副本减少,剩余副本压力上升,形成循环。

修复方案

1. 拆分健康检查职责

推荐把接口拆成两个:

  • /livez:只判断进程是否活着,不检查外部依赖。
  • /readyz:判断实例是否可以接流量,可以检查关键依赖,但要控制耗时。

示例配置:

startupProbe:
  httpGet:
    path: /livez
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 24
livenessProbe:
  httpGet:
    path: /livez
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 6
readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 3

解释:

  • startupProbe 最多允许启动阶段等待 5 * 24 = 120 秒,启动成功前 liveness 不会生效。
  • livenessProbe 连续 6 次失败才重启,避免短暂抖动导致误杀。
  • readinessProbe 失败只会从 Service 后端摘除 Pod,不会重启容器。

2. 调整健康检查接口逻辑

/livez 应尽量轻量:

返回 200:HTTP 服务线程还能响应,请求处理主循环正常。
返回 500:进程内部已经进入不可恢复状态,例如主线程死锁、关键后台线程退出。

不要在 /livez 中做这些事情:

  • 查询 MySQL。
  • 访问 Redis。
  • 请求第三方接口。
  • 扫描大目录或读取大文件。
  • 等待锁或执行复杂业务逻辑。

/readyz 可以检查依赖,但建议使用短超时和缓存结果。例如每 5 秒后台刷新一次依赖状态,接口只读取内存中的最近结果,避免探针请求本身打爆依赖。

3. 根据启动耗时设置 startupProbe

先用日志或指标确认真实启动耗时:

kubectl -n prod logs deploy/order-api --since=30m | grep -E "Started|listening|ready"

如果应用冷启动常见耗时为 60 秒,建议给 startupProbe 留出 2 倍左右缓冲。例如:

startupProbe:
  httpGet:
    path: /livez
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 24

这样应用有 120 秒启动窗口,不需要把 liveness 的 initialDelaySeconds 配得很大。

4. 资源不足时不要只改探针

如果 CPU throttling 明显,需要同时调整资源配置:

resources:
  requests:
    cpu: "500m"
    memory: "512Mi"
  limits:
    cpu: "1"
    memory: "1Gi"

探针超时不是永远通过加大 timeoutSeconds 解决。若服务本身已经因为 CPU 不足无法响应,应该扩容副本、提高 CPU limit、优化慢请求或降低启动阶段负载。

发布和验证

修改 Deployment 后执行:

kubectl -n prod apply -f order-api-deployment.yaml
kubectl -n prod rollout status deploy/order-api

观察新 Pod:

kubectl -n prod get pod -l app=order-api -w

确认 15 到 30 分钟内重启次数不再增长:

kubectl -n prod get pod -l app=order-api

查看事件中是否还存在 liveness 失败:

kubectl -n prod get events --sort-by='.lastTimestamp' | grep -E "order-api|Liveness|Killing"

如果有监控,建议同时确认:

increase(kube_pod_container_status_restarts_total{namespace="prod", pod=~"order-api-.*"}[30m])

结果应接近 0。还要观察 Ingress 5xx、业务接口 P95/P99 延迟和实例 ready 数量,避免只看 Pod 状态而忽略用户侧影响。

预防措施

  1. 新服务上线时同时定义 /livez/readyz,不要一个 /health 负责所有检查。
  2. 慢启动服务优先使用 startupProbe,不要依赖很大的 initialDelaySeconds
  3. liveness 不检查外部依赖,依赖抖动应通过 readiness 摘流,而不是重启进程。
  4. 健康检查接口必须轻量、稳定、短路径,避免访问慢资源。
  5. kube_pod_container_status_restarts_total 配置告警,重启次数短时间增长要及时处理。
  6. 监控 CPU throttling 和内存使用,避免资源不足被误判为探针配置问题。
  7. 发布后观察 Pod 事件,Liveness probe failed 不应被当作普通重启噪声忽略。

总结

Kubernetes 中的 livenessProbe 适合处理进程卡死、不可恢复的内部异常,不适合承载数据库、缓存、第三方接口等依赖健康判断。依赖不可用时,正确动作通常是 readiness 失败并从 Service 后端摘除实例,而不是让 Kubelet 重启容器。

排查这类问题时,先看 kubectl describe pod 里的事件,再看 --previous 日志和探针配置。修复时把启动、存活、就绪三类检查拆开:startupProbe 负责慢启动保护,livenessProbe 负责不可恢复故障重启,readinessProbe 负责是否接收流量。这样才能避免一次短暂抖动被探针放大成连续重启事故。