问题背景
业务核心交易服务完成容器化改造,整体迁移到自建 Kubernetes 集群(v1.24,kubelet 运行时 containerd)。某次版本发布后,运维在发布系统里看到该服务的 Deployment 长时间处于 Progressing 状态,副本数始终为 0/3。发布流水线反复重试仍无法拉起实例,上游网关已开始零星返回 502。由于当天还有其它变更在并行进行,最初的怀疑点被分散到了网络策略、镜像仓库和节点资源上,排查一度走偏。
故障现象
kubectl get pod -n trade 显示 3 个 Pod 全部处于 CrashLoopBackOff 状态,RESTARTS 列以肉眼可见的速度从 3、5、8 一路上涨,最短的十几秒就被重启一次。kubectl describe pod 的 Events 里反复出现:
|
|
Pod 的 Conditions 中 Ready 一直是 False。有意思的是,用 kubectl logs 看容器标准输出,发现应用进程其实在跑、日志里还在打印 loading config / connecting to mysql 这类启动日志,并没有真正崩溃退出——它只是还没"活"过来就被杀掉了。
排查过程
第一步,区分"进程崩溃"和"被探针杀掉"。因为 RESTARTS 持续上涨但日志显示进程停在启动中途,基本可以排除 OOMKilled(OOM 会有 OOMKilled 事件且通常不是稳定周期)。kubectl describe 明确指向 Liveness probe failed,方向锁定在探针。
第二步,看探针配置。该 Deployment 的容器定义了 livenessProbe:
|
|
第三步,理解 /healthz 的语义。这个端点并不是"进程还活着"的简单探测,而是开发在代码里把它实现成了"依赖全部就绪才算健康"——它内部会去连 MySQL、Redis 和配置中心 Nacos,任何一项没连上就返回 503。也就是说,/healthz 实际上是一个 readiness 语义的端点被错误地用作了 liveness。
第四步,核对应用真实启动耗时。应用是 Spring Boot 单体,冷启动到连上全部中间件、/healthz 返回 200,平均需要 90 秒左右;而首次发布那天因为配置中心刚扩容、连接更慢,实测接近 120 秒。探针在容器启动后仅 10 秒就开始探活,连续 3 次(30 秒)失败就触发 kubelet 发 SIGTERM 杀容器。应用永远停在被 kill 在启动到一半的状态,下一轮重启又从 0 开始,陷入死循环。
第五步,确认没有 readinessProbe。该容器只配了 liveness,没有 readinessProbe。即使某次侥幸启动完成,在 /healthz 真正就绪前流量也会被 Service 转发进去,加剧不稳定。
解决方案
- 纠正探针语义:livenessProbe 改成只探"进程是否还活着、能否自救",使用极轻量的端点(如独立的
/health,仅返回 200,不依赖任何外部依赖),并把initialDelaySeconds提高到 60,failureThreshold提到 5,给足启动宽容度。 - 补上 readinessProbe:使用
/healthz(依赖就绪才返回 200),periodSeconds10、failureThreshold3,确保只有真正就绪才接流量。 - 引入 startupProbe 兜底慢启动:K8s 1.16+ 支持 startupProbe,在它探测成功之前,liveness 和 readiness 都不会生效。配置
startupProbehttpGet /healthz,failureThreshold30、periodSeconds5,相当于给应用最多 150 秒的启动窗口,期间不会被 liveness 误杀。 - 开发侧拆分端点:明确
/health(存活)、/ready(就绪)两个语义不同的端点,避免把"就绪"逻辑塞进"存活"端点。 - 滚动更新策略配合:把
maxSurge/maxUnavailable调成1/0,避免新 Pod 没起来就把旧 Pod 摘掉造成容量空窗。
改完重新发布,Pod 在约 100 秒后进入 Running/Ready,RESTARTS 不再上涨,服务恢复。
根因分析
根因是探针语义错位 + 初始延迟远小于真实启动时间。livenessProbe 的职责是"发现进程已死且无法自愈时重启它",应该用最宽松的存活判断;而本例把"依赖全部就绪"这种 readiness 语义放进了 liveness,又只给 10 秒 initialDelay,导致应用在依赖就绪前就被反复处决。本质是把"是否准备好接流量"和"进程是否还活着"两件事混为一谈。
预防措施
- 给所有对外提供服务的容器同时配置 liveness / readiness / startup 三件套,语义严格区分。
- liveness 端点保持"零依赖",只证明进程在跑;依赖就绪交给 readiness。
- 启动慢的应用(连多数据源、拉配置中心)一律用 startupProbe 兜启动窗口,而不是无脑调大
initialDelay。 - 把探针配置纳入上线 review 清单和 Chart 模板默认值,避免每个团队各写各的。
- 在监控里对 Pod 的
RESTARTS速率、CrashLoopBackOff状态做告警,比等用户投诉早发现。
总结
CrashLoopBackOff 不一定意味着代码崩了,很多时候是探针在"帮倒忙"。liveness 要宽松、readiness 管流量、startup 管慢启动,三者各司其职,容器才稳。把"存活"和"就绪"混为一谈,是容器化迁移里最高频、也最隐蔽的坑之一。