适用场景

Java、Django、Node.js 等服务在 Kubernetes 中滚动发布后,新 Pod 长时间处于 CrashLoopBackOff,Deployment 一直无法完成;而在开发或低负载环境中启动正常。日志常只来得及打印数据库连接、缓存预热或迁移检查的前几行。

本文以“应用冷启动约 90 秒,但存活探针从第 20 秒就开始检查”为例,说明如何取证、修复和防止再次发生。

现象描述

发布后先确认控制器和 Pod 的实际状态:

kubectl -n payments rollout status deploy/api --timeout=180s
kubectl -n payments get pod -l app=api -w
kubectl -n payments describe pod api-7d6c8f7c9d-xm2qk

典型的 describe 事件如下:

Warning  Unhealthy  Liveness probe failed: HTTP probe failed with statuscode: 503
Normal   Killing    Container api failed liveness probe, will be restarted
Warning  BackOff     Back-off restarting failed container

这里的关键不是只看 503,而是看 第一次探测时间、失败次数和容器重启时间 是否早于应用正常可服务的时间。restartCount 持续增长,且每次日志都停在相似的初始化阶段,通常就是探针在启动完成前终止了进程。

可能原因

livenessProbe 的职责是判断“已运行的进程是否卡死”。把它同时当作启动检测会形成循环:应用尚未完成初始化,探针失败;kubelet 杀掉容器;下一次又从零开始初始化。

常见触发项包括:

  • 冷缓存预热、加载大模型或建立大量数据库连接,启动耗时突然增加;
  • 数据库迁移、依赖服务抖动使启动阶段短暂返回 503;
  • 仅设置 initialDelaySeconds,却没有按最坏启动时间计算失败窗口;
  • readiness 与 liveness 共用一个会在依赖未就绪时失败的接口。

排查步骤

1. 还原容器被杀前的日志

kubectl -n payments logs api-7d6c8f7c9d-xm2qk -c api --previous --timestamps
kubectl -n payments get pod api-7d6c8f7c9d-xm2qk \
  -o jsonpath='{.status.containerStatuses[?(@.name=="api")].restartCount}{"\n"}'

--previous 读取上一次已经退出的容器;没有它往往只能看到新一轮、尚未写完的日志。把最后一条“服务监听完成”日志的时间与事件中首次 Unhealthy 的时间对比,能证明是否是启动窗口不足。

2. 查看生效的探针配置

kubectl -n payments get deploy api -o jsonpath='{range .spec.template.spec.containers[?(@.name=="api")]}{.livenessProbe}{"\n"}{.readinessProbe}{"\n"}{.startupProbe}{"\n"}{end}'

需要计算最大容忍启动时间:

startupProbe 允许窗口 = failureThreshold × periodSeconds

例如 failureThreshold: 30periodSeconds: 5,最多允许 150 秒。窗口要覆盖压测、镜像刚拉起、依赖恢复等最慢的正常启动,而不是只覆盖日常平均值。

3. 区分就绪失败与存活失败

kubectl -n payments get endpointslice -l kubernetes.io/service-name=api \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses}{" ready="}{.conditions.ready}{"\n"}{end}'

readiness 失败会使 Pod 暂时不进入 Service 后端,通常不会重启容器;liveness 失败才会触发重启。因此预发布阶段应优先让 readiness 拦住流量,而不是让 liveness 终止一个仍在正常初始化的进程。

修复方案

为慢启动服务增加专用的 startupProbe。在它成功前,Kubernetes 不执行 liveness 与 readiness 探测:

containers:
  - name: api
    image: registry.example.com/payments/api:2026.08.21
    ports:
      - containerPort: 8080
    startupProbe:
      httpGet:
        path: /healthz/startup
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 30
    readinessProbe:
      httpGet:
        path: /healthz/ready
        port: 8080
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3
    livenessProbe:
      httpGet:
        path: /healthz/live
        port: 8080
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3

三个接口的语义应明确:/startup 只表示初始化是否完成;/ready 表示当前实例能否接收业务流量;/live 只检查事件循环、主线程等进程自身是否仍健康。不要在 /live 中同步调用数据库、Redis 或第三方 API,否则外部短暂故障会被放大为全量重启。

应用配置后执行:

kubectl -n payments apply -f api-deployment.yaml
kubectl -n payments rollout status deploy/api --timeout=5m
kubectl -n payments get pods -l app=api
kubectl -n payments get events --sort-by='.lastTimestamp' | tail -n 20

最后一条命令输出最新 20 条事件;在 PowerShell 中可改用 Select-Object -Last 20。验收标准是新 Pod 的 RESTARTS 为 0、Deployment 成功完成,且 EndpointSlice 中新地址为 ready=true

预防措施

  • 在 CI 的发布后检查中加入 kubectl rollout status,超时即保存 describe--previous 日志作为构建产物;
  • 监控 kube_pod_container_status_restarts_total 的增量,并按 Deployment 版本聚合告警;
  • 为每个服务记录 P95/P99 启动耗时,调整 startupProbe 窗口时基于最慢正常值留出余量;
  • 将健康检查作为接口契约:版本变更、依赖策略变化时同时评审探针语义;
  • 用预发布环境的冷缓存与限速依赖进行一次滚动发布演练,避免只验证热启动。

总结

滚动发布卡住不一定是镜像或代码崩溃。先用事件、上一次容器日志和重启次数确认是不是探针过早终止进程,再用 startupProbe 给正常初始化留出明确窗口。把启动、就绪和存活三类健康状态拆开后,慢启动服务既不会接入过早,也不会因短暂初始化而陷入重启循环。