适用场景
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: 30、periodSeconds: 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 给正常初始化留出明确窗口。把启动、就绪和存活三类健康状态拆开后,慢启动服务既不会接入过早,也不会因短暂初始化而陷入重启循环。
Discussion
评论