主题
Pod 生命周期与探针
一句话:Pod 的
status.phase只描述生命周期的大阶段,真正的故障原因藏在容器的state.waiting.reason里;探针则决定这个容器是「被摘流量」还是「被重启」,两者混着看就会修错方向。
适用版本:Kubernetes 1.29+。探针默认值以官方文档为准。
为什么需要
kubectl get pods 的 STATUS 列会把不同层级的信息混在一起:Pending 是 Pod 阶段,CrashLoopBackOff 是容器等待原因,Evicted 是 Pod 被驱逐的原因。只看到 CrashLoopBackOff 就去重启,或者看到 Running 就以为服务可用,都会漏掉真正的问题。
把层级拆开,排障才有顺序:先看 Pod 阶段,判断是调度、启动还是运行;再看容器等待原因,判断是镜像、配置还是进程退出;最后看探针事件,判断是流量问题还是存活问题。
核心机制
三个层级:phase、reason、container state
Pod 的 status.phase 只有五个值:
| phase | 含义 |
|---|---|
Pending | 已接受,但还没调度成功或容器还没全部启动 |
Running | Pod 已绑定节点,至少一个容器在运行 |
Succeeded | 所有容器正常退出(exit 0),不会再重启 |
Failed | 所有容器已终止,至少一个非零退出 |
Unknown | 无法从节点获取状态,通常是节点失联 |
Evicted、NodeLost、Shutdown 不是 phase,而是 status.reason。CrashLoopBackOff、ImagePullBackOff、ErrImagePull、CreateContainerConfigError 也不是 phase,而是 status.containerStatuses[].state.waiting.reason。
容器状态有三态:
yaml
status:
phase: Running
containerStatuses:
- name: app
ready: false
restartCount: 5
state:
waiting:
reason: CrashLoopBackOff
lastState:
terminated:
reason: Error
exitCode: 1这条 Pod 的 phase 是 Running,但容器没 Ready、在退避重启。只看 phase 会误判为正常。
探针:三种检查,三种动作
| 探针 | 检查什么 | 失败后的真实动作 | 典型用途 |
|---|---|---|---|
startupProbe | 启动完成了吗 | kubelet 杀掉容器并按重启策略重启;成功前 liveness/readiness 不生效 | 慢启动应用 |
readinessProbe | 能接流量吗 | 从 Service Endpoint 摘除,容器不重启 | 预热、依赖未就绪 |
livenessProbe | 进程还活着吗 | kubelet 杀掉容器,按 restartPolicy 重启 | 死锁、无响应 |
探针的默认值:initialDelaySeconds=0、periodSeconds=10、timeoutSeconds=1、failureThreshold=3、successThreshold=1(liveness 和 startup 的 successThreshold 只能是 1)。这些默认值意味着:不配 initialDelaySeconds 时,容器一启动就开始探测;timeoutSeconds=1 对慢接口很苛刻。
几个容易混淆的点:
- liveness 失败不会删除 Pod,只会杀掉容器并按
restartPolicy重启;Pod 名称、IP 不变。 - readiness 失败不会重启,Pod 仍是
Running,只是READY变成0/1,Endpoint 里没有它。 - startup 失败会重启容器,而且在它成功之前,liveness 不会介入,这正是它保护慢启动应用的方式。
- 重启有退避:10s 起,逐次翻倍,最长 5 分钟;退避期间状态显示
CrashLoopBackOff。
Init Container 与重启策略
Init Container 串行执行,全部成功后才启动主容器。任何一个失败,Pod 会停在 Init:Error 或 Init:CrashLoopBackOff,主容器根本不会创建。排查时要看 initContainerStatuses 和对应容器日志,而不是主容器。
restartPolicy 决定容器退出后的行为:Always(默认)总是重启,适合长期服务;OnFailure 只在非零退出时重启,适合 Job;Never 不重启。
实战场景
一、phase=Running 的 Pod 一个都不 Ready,告警却全绿
现象:凌晨一次配置变更后,order-service 的 12 个 Pod 里 9 个在反复重启。值班看板只显示“Running 12/12”,直到用户投诉才发现服务实际无可用实例。事后确认:状态是 phase: Running + containerStatuses[].state.waiting.reason: CrashLoopBackOff,ready: false。
排查:三个层级不能混用——status.phase、status.reason、containerState.reason。
| 层级 | 取值 | 常见误读 |
|---|---|---|
status.phase | 只有 Pending/Running/Succeeded/Failed/Unknown 五个值 | Evicted、NodeLost、Shutdown 不是 phase,是 status.reason |
| 容器状态 reason | CrashLoopBackOff、ImagePullBackOff、ErrImagePull、CreateContainerConfigError | 这些都不是 phase,按 phase 统计永远看不到 |
ready | 由 Readiness 探针决定 | Running 不等于 ready,Service 只转发 ready 的 Pod |
bash
# 正确的可用性口径:Ready 数,而不是 Running 数
kubectl get pods -l app=order-service \
-o 'jsonpath={range .items[*]}{.metadata.name} phase={.status.phase} ready={.status.containerStatuses[0].ready} reason={.status.containerStatuses[0].state.waiting.reason}{"\n"}{end}'
kubectl get endpoints order-service # 真实承接流量的后端数量对症做法:
yaml
# 1. 监控指标换成 Ready 比例与 restartCount,而不是 Pod phase
rules:
- alert: ServiceReadyRatioLow
expr: |
sum(kube_pod_status_ready{condition="true", namespace="prod", label_app="order-service"})
/ sum(kube_pod_labels{label_app="order-service"}) < 0.8
for: 2m
- alert: PodRestartStorm
expr: increase(kube_pod_container_status_restarts_total{label_app="order-service"}[10m]) > 3
for: 5mtext
2. 值班手册里写清“三个层级各看什么”:
phase 看整体阶段,reason 看为什么卡住,ready 看是否接流量。
3. restartCount 增长速度是比“有没有重启”更有用的信号:
一次重启可能是抖动,每 5 分钟一次是 CrashLoop 退避周期。
4. CrashLoopBackOff 的第一现场是容器日志与退出码:
kubectl logs --previous 看上一次崩溃的输出,137 是内存 OOM、1 是应用异常。复盘要点:判据是把所有服务级告警从“Pod 状态”改成“Ready 比例 + endpoints 数量”,并保留 restartCount 增速告警。这条改动的价值在“无可用实例但看起来正常”这类误报上——它是发布与变更期间最容易致命的假绿。
二、Java 应用启动 90 秒,被 liveness 反复杀死
现象:一个 Spring Boot 服务迁移到新集群后进入死循环:Pod 起来约 30 秒被杀,重启次数不断累加,日志显示启动流程每次都停在同一处;运维误判为“应用启动崩溃”。
排查:启动阶段与运行阶段用同一套探针阈值。
| 定位层 | 取证据 | 判定 |
|---|---|---|
| 探针配置 | 是否有 startupProbe;liveness 的 initialDelaySeconds/periodSeconds/failureThreshold | 无 startupProbe 且窗口小于启动耗时即命中 |
| 实际启动时长 | Started ... in Xs 日志与 Pod 首次 ready 时间 | 90s 远大于 30s 判定窗口 |
| 杀死原因 | kubectl describe pod 的 Liveness probe failed 事件与退出信号 | 探针杀的,不是应用崩的 |
| 探针端点内容 | liveness 是否包含 DB/Redis 检查 | 下游抖动会造成整批重启(另一类事故) |
| 依赖耗时 | 启动期是否同步拉配置/预热 | 让启动时长不可控 |
yaml
# 用 startupProbe 保护慢启动,成功之前不执行 liveness/readiness
startupProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
periodSeconds: 5
failureThreshold: 30 # 最多容忍 150s,覆盖冷启动 P99 并留余量
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 } # 只回答“进程是否该重启”
periodSeconds: 10
failureThreshold: 6
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 } # 依赖就绪、预热完成放这里
periodSeconds: 5
failureThreshold: 2text
1. 不要用巨大的 initialDelaySeconds 掩盖真实启动时间:
它把启动期变成一个固定空白,应用变快也不会更早接流量,变慢则直接失败。
2. liveness 只放 ping(进程存活);依赖检查放 readiness,
并判断“依赖挂了是否所有实例都该下线”。
3. 预热完成才能 ready:自定义 HealthIndicator 加入 warmup 状态,
归入 readiness 组,避免冷实例被放进满流量。
4. startupProbe 的上限要有依据:
failureThreshold × periodSeconds ≥ 冷启动 P99 × 1.5(经验值,需校准)。复盘要点:判据是“liveness 触发的重启次数应长期为 0”,出现即说明探针阈值或应用健康检查内容有错。同时把“Pod 创建到首次 ready 的时长”做成服务级指标,它是容量规划与故障恢复时间的输入,不该靠 initialDelay 猜。探针与服务上下线的配合见 优雅上下线与启动检查。
三、CreateContainerConfigError 被当成应用 Bug 排查了两小时
现象:某次发布后 Pod 一直不进入 Running,kubectl get pods 显示 CrashLoopBackOff/Error,开发查了应用日志毫无输出(应用根本没启动)。最终原因是 ConfigMap 里删掉了一个 key,而容器 envFrom 引用它并配了必填的 env 项——这是容器创建阶段就失败。
排查:先确定失败发生在哪一层,再看具体原因。
| 失败层 | 典型 reason | 判定证据 |
|---|---|---|
| 镜像拉取 | ImagePullBackOff、ErrImagePull | describe pod 的 Events + imagePullSecrets/标签是否存在 |
| 配置/密钥 | CreateContainerConfigError | Events 里 configmap "x" not found / key not found |
| 存储卷 | FailedMount、FailedAttachVolume | PVC 状态与存储类可用区 |
| 调度 | Pending + 0/N nodes are available | 见 K8s 核心概念与调度 |
| 容器启动 | 退出码 1 / 137 | 应用日志(--previous)或 OOMKilled |
| 探针 | Liveness probe failed | 事件时间线与探针配置 |
bash
kubectl describe pod <pod> | sed -n '/Events:/,$p' # 第一现场,不要先看应用日志
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[0]}' | jq
kubectl get events --field-selector involvedObject.name=<pod> --sort-by=.lastTimestamp | tail -15对症做法:
yaml
# 1. 可选引用与必填分开:缺 key 时不要让容器创建失败
env:
- name: FEATURE_FLAG
valueFrom:
configMapKeyRef: { name: app-config, key: feature.flag, optional: true }
# 2. 配置与消费方一起发布:ConfigMap 变更走同一 PR / 同一发布单
# 3. 关键配置在应用启动期显式校验,失败信息要能指回缺哪个 key(而不是靠 K8s 事件猜)text
4. 发布顺序:先应用新配置(向后兼容),再发布使用新配置的版本;
回滚时反过来。配置删除类变更必须先确认无人引用。
5. 镜像类问题与配置类问题的定位手段不同:
前者看 Events 里的拉取错误与 tag 是否存在,后者看 not found 与 optional 设置。
混在一起排查就会像本例一样查两小时应用日志。复盘要点:判据是“发布流水线里包含 ConfigMap/Secret 的引用一致性检查”——把 Pod 卡在创建层的错误挡在部署阶段;另外值班流程要固化“先看 describe pod 的 Events,再看应用日志”,因为容器没起来时应用日志一定是什么都没有,从这里入手会绕远路。多集群场景下的配置分发与校验见 多集群容器平台。
最小上手
先看一个 kubectl get 只显示等待原因、phase 仍是 Pending 的例子:
bash
kubectl run bad-image --image=nginx:does-not-exist
kubectl get pod bad-image
# STATUS: ErrImagePull 或 ImagePullBackOff
kubectl get pod bad-image -o jsonpath='{.status.phase}{" | "}{.status.containerStatuses[0].state.waiting.reason}{"\n"}'
# → Pending | ErrImagePull
kubectl describe pod bad-image | grep -A5 Events
kubectl delete pod bad-image再验证 liveness 失败的真实动作:
bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: liveness-demo
spec:
containers:
- name: web
image: nginx:1.27
lifecycle:
postStart:
exec:
command: ["/bin/sh", "-c",
"echo OK > /usr/share/nginx/html/health; sleep 20; echo FAIL > /usr/share/nginx/html/health"]
livenessProbe:
httpGet: {path: /health, port: 80}
periodSeconds: 5
failureThreshold: 2
EOF
kubectl get pod liveness-demo -w
# READY 1/1 → 0/1,RESTARTS 增长;Pod 名称和 IP 不变
kubectl describe pod liveness-demo | grep -A5 Events
# → Liveness probe failed ... will be restarted
kubectl delete pod liveness-demo生产动作
- 慢启动应用必配 startupProbe:窗口设为 P99 启动时间,例如
periodSeconds: 10、failureThreshold: 30给 300s;不要把initialDelaySeconds当成启动保护,它只延迟第一次探测。 - liveness 只判「死没死」:不要用它检查下游依赖或慢查询,否则下游抖动会让所有容器一起重启,放大故障。
- readiness 用
failureThreshold: 1:快速摘流量,避免坏实例继续接请求。 - 排障顺序:
kubectl get pod -o wide→kubectl describe pod看 Events →kubectl logs --previous看上次退出 → 需要时kubectl exec进容器手动请求探针路径。 - 看退出码:
OOMKilled是内存超 limits;Error+ 非零退出是应用自己退出;Completed是正常结束但被restartPolicy: Always反复拉起。
边界
Running不等于可用:phase 只看容器是否在运行,不看 readiness;面板上要同时看READY和 Endpoint。- readiness 不能修复进程:它只摘流量,死锁的容器仍然占着资源,需要 liveness 或人工介入。
- liveness 阈值太紧会制造故障:GC 停顿、慢请求都可能超过
timeoutSeconds,连续失败就重启,形成「越重启越慢」的循环。 - startupProbe 只保护启动阶段:启动成功后它不再执行,运行期的慢响应要靠 liveness 的合理阈值或独立健康检查。
- exec 探针有开销:每次探测都在容器里起进程,高频探测会消耗 CPU;能用 HTTP 就用 HTTP。
讲清楚
Pod 状态要分三层看:phase 是 Pod 的大阶段,只有 Pending、Running、Succeeded、Failed、Unknown;reason 是 Pod 被驱逐或失联的原因,比如 Evicted;容器的 waiting.reason 才是 CrashLoopBackOff、ImagePullBackOff 这类具体原因。
kubectl get pods的 STATUS 列把它们混在一起,排障时要用 jsonpath 拆开。三种探针的动作不同:startup 失败重启,readiness 失败只摘流量,liveness 失败重启容器但不删 Pod。重启有 10s 起步、最长 5 分钟的退避。
最常见的配错是把 liveness 当业务健康检查。下游一抖,所有容器一起重启,故障被放大。liveness 只该判断进程是否死锁或无响应。