Kubernetes 基础体系 · 第 71/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 工作负载排障:Pending、CrashLoop、ImagePull、OOM 和 Probe

Kubernetes 中“Pod 不工作”不是一种故障。PendingCrashLoopBackOffImagePullBackOffOOMKilled 和探针失败分别位于不同的状态层次:

  • PendingPod Phase(阶段),表示 Pod 已被 API Server 接受,但尚未进入可运行状态。
  • CrashLoopBackOff 是容器状态中常见的 等待原因(reason),表示容器反复退出,kubelet 正在执行重启退避。
  • ImagePullBackOff 是容器状态中常见的 等待原因,表示镜像拉取失败后进入重试退避。
  • OOMKilled 通常是容器上一次终止状态的 reason,表示进程因超出内存约束或节点内存压力相关机制而被杀死。
  • Probe 是 kubelet 对容器健康状态进行检查的机制。探针失败本身不一定重启容器:存活探针通常会触发重启,读取探针通常会影响 Service Endpoint,启动探针则会延迟另外两类探针。

因此,排障的第一步不是直接执行 kubectl delete pod,而是确定:

  1. 这是调度问题、镜像问题、进程问题、资源问题,还是健康检查配置问题?
  2. 故障发生在哪个组件和状态转换上?
  3. 当前看到的是根因,还是根因发生后的二次表现?

一、先建立 Kubernetes 工作负载的状态模型

1. Pod Phase 不是容器健康状态

Pod 的 .status.phase 是一个有限集合,常见值包括:

  • Pending
  • Running
  • Succeeded
  • Failed
  • Unknown

它描述的是 Pod 层面的粗粒度阶段,而不是“应用是否可用”。

例如,一个 Pod 可能处于:

phase: Running
container state: Waiting
reason: CrashLoopBackOff

也就是说,Pod Phase 仍然是 Running,但容器当前并没有正常提供服务。

另一个 Pod 可能是:

phase: Pending
container state: Waiting
reason: ImagePullBackOff

这里 Pending 只说明 Pod 尚未完成启动流程,真正原因要看容器状态和事件。

不要把下面几个概念混为一谈:

字段或状态 所属层次 含义
.status.phase Pod Pod 的粗粒度生命周期阶段
.status.conditions Pod 调度、就绪等条件及其原因
.status.containerStatuses[].state 容器 当前是 WaitingRunning 还是 Terminated
.status.containerStatuses[].lastState 容器 上一次容器状态,特别适合分析重启
restartCount 容器 容器被重启的累计次数
CrashLoopBackOff 容器等待原因 容器反复退出后的重启退避
OOMKilled 容器终止原因 上一次容器终止的原因之一
Ready Condition Pod 是否满足就绪条件,通常影响 Service 流量

2. 一个 Pod 从创建到运行的关键路径

典型路径如下:

sequenceDiagram
    participant C as 客户端
    participant A as API Server
    participant S as Scheduler
    participant K as Kubelet
    participant R as 容器运行时
    participant N as Service/EndpointSlice

    C->>A: 创建 Deployment/Pod
    A-->>C: 接受对象
    A->>S: 未绑定节点的 Pod
    S->>A: 绑定节点,设置 PodScheduled=True
    A->>K: 节点上的 Pod 期望状态
    K->>R: 创建 Sandbox、拉取镜像、创建容器
    R-->>K: 容器启动或失败
    K->>A: 更新 containerStatuses
    K->>A: 执行 startup/readiness/liveness 探针
    A->>N: 根据 Ready 状态更新 EndpointSlice

在这个过程中,故障位置不同,排查入口也不同:

  • Scheduler 无法找到节点:通常是 Pending
  • Kubelet 或运行时无法拉取镜像:常见是 ImagePullBackOff
  • 进程启动后退出:常见是 CrashLoopBackOff
  • 进程因内存限制退出:上一次状态常见为 OOMKilled
  • 进程仍在运行但探针失败:可能是重启、未就绪,或启动阶段被探针阻塞。

二、统一排障入口:先确认上下文,再确认事实

1. 先确认 kubectl 操作的是哪个集群

生产环境最危险的排障错误之一,是命令本身正确,但执行到了错误的集群或命名空间。

kubectl config current-context
kubectl config get-contexts
kubectl config view --minify
kubectl get ns

current-context 只能说明当前上下文名称;config view --minify 可以进一步查看该上下文关联的集群和用户。不要只根据上下文名称判断环境,例如 prodproductioncluster-a 都可能被人为命名。

排障时尽量显式指定命名空间:

kubectl -n payments get pods
kubectl -n payments describe pod payments-api-7f8d9c6b7d-x2kq4

如果命令会修改资源,建议同时显式指定上下文:

kubectl --context=prod-cluster -n payments rollout restart deployment/payments-api

kubectl 的参数位置可以影响可读性和脚本兼容性。常见形式是:

kubectl --context=prod-cluster -n payments get pod

2. 用四个视角获取同一个 Pod 的事实

假设 Pod 名称为 payments-api-7f8d9c6b7d-x2kq4

NS=payments
POD=payments-api-7f8d9c6b7d-x2kq4

kubectl -n "$NS" get pod "$POD" -o wide
kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o json
kubectl -n "$NS" get events \
  --field-selector involvedObject.kind=Pod,involvedObject.name="$POD" \
  --sort-by=.lastTimestamp

这四条命令分别解决不同问题:

  • get -o wide:快速查看 Phase、IP、节点、重启次数。
  • describe:汇总容器状态、探针、挂载、调度信息和事件。
  • get -o json:获取不会被表格隐藏的精确字段。
  • get events:按时间观察调度器、kubelet、镜像运行时报告的失败。

describe 输出底部的 Events 很重要,但事件不是永久审计日志,可能因 TTL、事件聚合或集群配置而消失。因此,事件只能作为当前证据之一,不能替代应用日志、控制器历史和节点日志。

可以用 JSONPath 快速提取关键信息:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{.status.phase}{"\n"}{range .status.containerStatuses[*]}{.name}{" state="}{.state}{" lastState="}{.lastState}{" restart="}{.restartCount}{"\n"}{end}'

如果 Pod 有多个容器,必须逐个检查。一个 Sidecar 失败,也可能使整个 Pod 看起来“不正常”,但根因并不在业务容器。

3. 日志必须同时看当前实例和上一个实例

kubectl -n "$NS" logs "$POD" -c api --timestamps
kubectl -n "$NS" logs "$POD" -c api --previous --timestamps
  • logs 查看当前容器实例的标准输出和标准错误。
  • --previous 查看上一次已经终止的容器实例,特别适合 CrashLoopBackOff
  • 如果容器从未成功启动,例如镜像一直拉取失败,通常没有容器日志可看。
  • 应用把日志写到文件而不是 stdout 时,kubectl logs 可能为空,这不是“应用没有报错”的证据。

若容器正在重启,可以限制时间和行数,降低对 API Server 的负担:

kubectl -n "$NS" logs "$POD" -c api --previous --since=10m --tail=200

三、Pending:先区分“未调度”和“已调度但未启动”

1. Pending 的形式化含义

从 Pod Phase 看,Pending 大致表示:

Pod 已被 API Server 接受
且 Pod 尚未完成启动所需的全部步骤

这里的“启动所需步骤”不只包括调度,还可能包括:

  • 等待调度器选择节点;
  • 等待 init container 执行完成;
  • 等待镜像拉取;
  • 等待网络 Sandbox 或存储卷创建;
  • 等待容器真正启动。

因此:

Pending ≠ 一定是 Scheduler 问题

需要首先查看:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{.status.phase}{"\n"}{.status.conditions}{"\n"}'

最关键的调度条件通常是:

type: PodScheduled
status: "False"
reason: Unschedulable

如果 PodScheduled=True,说明 Scheduler 已经绑定了节点,此时继续盯着调度器通常没有意义,应转向 kubelet、存储、网络或镜像问题。

2. 未调度:Scheduler 如何判断节点可行

简化地说,Scheduler 要为 Pod 找到一个满足约束的节点:

nNodes,Feasible(n,Pod)=true\exists n \in Nodes,\quad Feasible(n, Pod)=true

Feasible 通常需要同时满足多个条件:

Feasible=ResourceFitTaintsToleratedNodeSelectorMatchAffinityMatchVolumeConstraintsTopologyConstraintsFeasible = ResourceFit \land TaintsTolerated \land NodeSelectorMatch \land AffinityMatch \land VolumeConstraints \land TopologyConstraints

这些变量的含义是:

  • ResourceFit:节点可分配资源能够满足 Pod 的 requests;
  • TaintsTolerated:Pod 的 toleration 能容忍节点 taint;
  • NodeSelectorMatch:节点标签满足 nodeSelector 或 node affinity;
  • AffinityMatch:Pod 与其他 Pod 的亲和或反亲和约束满足;
  • VolumeConstraints:卷的拓扑、挂载和访问模式允许;
  • TopologyConstraints:拓扑分布约束没有被违反。

其中资源调度使用的是 requests,而不是 limits。对某个资源 rr,调度器通常比较:

pnoderequest(p,r)+request(newPod,r)allocatable(node,r)\sum_{p \in node} request(p,r) + request(newPod,r) \le allocatable(node,r)

allocatable 是节点可供 Pod 使用的容量,不等于物理机总容量。系统组件和 kubelet 预留可能已经占用一部分容量。

检查调度事件:

kubectl -n "$NS" describe pod "$POD"
kubectl get nodes
kubectl describe node <node-name>

常见事件及其因果关系包括:

0/5 nodes are available: 5 Insufficient memory

表示没有节点满足 requests 资源要求,不表示容器已经发生 OOM。

0/5 nodes are available: 3 node(s) didn't match Pod's node affinity

表示标签或亲和约束排除了节点。

0/5 nodes are available: 2 node(s) had untolerated taint

表示节点 taint 存在,但 Pod 没有对应 toleration。

3. 一个可验证的 Pending 算例

假设三个节点的可分配内存分别为:

node-a: 4 GiB,已有请求 3 GiB
node-b: 8 GiB,已有请求 7.5 GiB
node-c: 4 GiB,已有请求 3.5 GiB

新 Pod 的内存 request 为 1Gi

node-a 剩余可调度内存 = 4 - 3 = 1Gi
node-b 剩余可调度内存 = 8 - 7.5 = 0.5Gi
node-c 剩余可调度内存 = 4 - 3.5 = 0.5Gi

仅从内存看,node-a 可行。但如果 Pod 还有:

nodeSelector:
  workload: batch

node-a 没有该标签,那么最终仍然没有可行节点:

ResourceFit(node-a) = true
NodeSelectorMatch(node-a) = false
Feasible(node-a) = false

此时盲目降低内存 limit 没有帮助,因为调度判断使用的是 request。正确的修复方向可能是:

  • 修改不正确的标签选择器;
  • 为符合条件的节点增加容量或标签;
  • 调整 requests;
  • 重新评估反亲和或拓扑约束。

4. 已调度但仍 Pending

如果:

kubectl -n "$NS" get pod "$POD" -o wide

已经显示 NODE,并且:

PodScheduled: True

那么要继续看容器状态和事件:

kubectl -n "$NS" describe pod "$POD"

可能的原因包括:

  • ImagePullBackOff:镜像拉取失败;
  • ErrImagePull:本次拉取失败;
  • ContainerCreating:卷、网络 Sandbox 或容器创建尚未完成;
  • init container 尚未完成;
  • Secret、ConfigMap 或 PVC 挂载失败;
  • CNI、CSI 或容器运行时故障。

检查 init container:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{range .status.initContainerStatuses[*]}{.name}{" state="}{.state}{" last="}{.lastState}{" restart="}{.restartCount}{"\n"}{end}'

init container 不是普通并行容器。只要某个 init container 没有成功退出,业务容器就不会启动。此时查看业务容器日志往往为空,应查看 init container:

kubectl -n "$NS" logs "$POD" -c init-config --previous

四、ImagePull:镜像拉取失败发生在哪里

1. ErrImagePullImagePullBackOff 的区别

这两个状态经常一起出现,但含义不同:

  • ErrImagePull:某次镜像拉取操作失败;
  • ImagePullBackOff:拉取失败后,kubelet 暂停一段时间再重试。

这和 CrashLoopBackOff 的区别在于:

ImagePullBackOff:容器通常还没有成功创建或启动
CrashLoopBackOff:容器已经启动过,但反复退出

查看精确原因:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{" state="}{.state}{"\n"}{end}'

还要查看事件,因为运行时经常把 registry 返回的错误写在那里:

kubectl -n "$NS" describe pod "$POD"

2. 镜像拉取的故障链

容器镜像拉取一般经过如下路径:

Pod spec 中的 image
        ↓
kubelet 根据 imagePullPolicy 请求容器运行时
        ↓
运行时解析 registry/repository/tag 或 digest
        ↓
认证 registry
        ↓
解析 manifest/index
        ↓
下载各层 blob
        ↓
校验并解压
        ↓
创建容器

每一步都可能失败:

阶段 典型原因
名称解析 repository 拼写错误、registry 地址错误
认证 imagePullSecrets 缺失、Secret 类型错误、凭据过期
manifest tag 不存在、架构不匹配、私有仓库权限不足
blob 下载 网络超时、代理配置错误、registry 限流
解压 节点磁盘不足、镜像层损坏
创建容器 runtime、CNI、挂载或安全策略问题

检查 Pod 使用的镜像和拉取策略:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{range .spec.containers[*]}{.name}{" image="}{.image}{" imagePullPolicy="}{.imagePullPolicy}{"\n"}{end}'

注意 imagePullPolicy 的默认推导规则具有版本和镜像写法相关性。常见规则是:

  • 使用显式 digest 时通常按本地已有内容处理;
  • 使用 :latest 时通常默认 Always
  • 使用非 latest tag 时通常默认 IfNotPresent
  • 显式配置 imagePullPolicy 后,以显式值为准。

生产部署不应依赖浮动 tag。image: app:latest 既不便于审计,也可能导致不同节点实际运行不同内容。更可验证的形式是固定不可变 tag 或 digest:

image: registry.example.com/payments/api@sha256:...

digest 必须来自可信发布流程,不能随意手工填写。

3. 私有仓库认证

检查 Pod 是否引用了拉取凭据:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{.spec.imagePullSecrets}{"\n"}'

imagePullSecrets 必须位于同一个命名空间。命名空间 payments 中的 Secret 不能直接被 orders 中的 Pod 使用。

查看 Secret 元数据而不是直接输出内容:

kubectl -n "$NS" get secret regcred \
  -o jsonpath='{.type}{"\n"}'

通常 Docker Registry 凭据的类型为:

kubernetes.io/dockerconfigjson

不要在排障记录、终端共享或工单中执行:

kubectl get secret regcred -o yaml

因为这可能暴露经过 Base64 编码的凭据。Base64 不是加密。

如果确认是凭据问题,修复 Deployment 的模板,而不是只修改当前 Pod:

kubectl -n "$NS" get deployment payments-api \
  -o jsonpath='{.spec.template.spec.imagePullSecrets}{"\n"}'

因为 Deployment 会持续创建新的 Pod。只修当前 Pod 可能在下一次滚动更新或节点故障后再次失败。

4. 节点侧问题不能只看 Pod

当事件显示:

failed to pull and unpack image:
no space left on device

应检查节点,而不是修改应用配置:

kubectl describe node <node-name>
kubectl get node <node-name> -o jsonpath='{.status.conditions}{"\n"}'

节点上的镜像缓存、容器可写层、日志和临时目录都可能占满磁盘。节点级修复通常需要节点管理员或云厂商工具,不能通过删除业务 Pod 自动解决。


五、CrashLoopBackOff:容器为什么反复启动和退出

1. 它不是一个应用错误码

CrashLoopBackOff 的含义是:

容器退出
→ kubelet 根据 RestartPolicy 尝试重启
→ 容器再次退出
→ kubelet 增加重启间隔

它描述的是 kubelet 当前的重启行为,不说明应用为什么退出。根因可能是:

  • 进程参数错误;
  • 配置文件或 Secret 缺失;
  • 依赖服务连接失败后主动退出;
  • 监听端口失败;
  • 启动脚本执行错误;
  • 进程收到 SIGTERM、SIGKILL 或其他信号;
  • 探针失败导致 kubelet 重启;
  • 内存超限导致 OOMKilled

首先查看当前和上一次状态:

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"\n"}{"state: "}{.state}{"\n"}{"lastState: "}{.lastState}{"\n"}{"restartCount: "}{.restartCount}{"\n\n"}{end}'

对于典型的进程崩溃,可能看到:

lastState:
  terminated:
    exitCode: 1
    reason: Error
    startedAt: ...
    finishedAt: ...

对于 OOM,可能看到:

lastState:
  terminated:
    exitCode: 137
    reason: OOMKilled

137 通常等于 128 + 9,表示进程最终对应 SIGKILL,但不要只根据数字断言一定是容器内存超限。应结合 reason: OOMKilled、容器限制、节点事件和监控判断。

2. RestartPolicy 决定“退出后是否重启”

Pod 的 restartPolicy 有:

  • Always
  • OnFailure
  • Never

对于 Deployment、ReplicaSet、StatefulSet、DaemonSet 等长期运行工作负载,通常使用 Always,并且控制器会维持副本数。Job 常使用 OnFailureNever,由 Job 控制器根据成功或失败决定是否创建后续 Pod。

restartPolicy 作用在 Pod 内的容器退出上,不是“整个 Pod 被删除重建”的开关。容器可以在同一个 Pod 内被 kubelet 重启,同时 Pod UID 不变。

如果一个容器:

exitCode = 0

restartPolicy=Always,它仍然会被重启。这对一个本应执行一次就结束的进程非常重要:把一次性任务错误地放进长期运行的 Deployment,可能产生看似异常的重启。

3. 重启退避如何影响观察

kubelet 通常对反复失败的重启采用指数退避,常见观察序列近似为:

立即重启
约 10 秒
约 20 秒
约 40 秒
...
直到约 5 分钟上限

这是 kubelet 的常见实现行为,具体细节可能随 Kubernetes 版本和运行时实现变化,不应把时间间隔作为业务契约。

退避的直接后果是:

  • kubectl logs 可能刚好错过上一次启动;
  • 容器短暂运行时,实时 logs -f 不一定稳定;
  • --previous 往往比当前日志更有价值;
  • 删除 Pod 只会重置观察对象,不会修复导致进程退出的配置或代码。

4. 用退出码和信号缩小范围

常见退出码只能作为线索:

表现 可能方向
exitCode=0,仍被重启 restartPolicy=Always 或进程设计不匹配
exitCode=1 应用主动报告一般错误,需要看日志
exitCode=2 参数、命令行或 shell 使用错误,具体含义取决于程序
exitCode=126/127 无执行权限或命令不存在,常见于镜像/command 配置
exitCode=137 SIGKILL,结合 OOMKilled 和节点状态判断
exitCode=143 常见为 SIGTERM,可能是正常终止、滚动更新或优雅关闭

查看 Pod 的终止状态:

kubectl -n "$NS" get pod "$POD" -o jsonpath='{range .status.containerStatuses[*]}{.name}{" exit="}{.lastState.terminated.exitCode}{" reason="}{.lastState.terminated.reason}{" signal="}{.lastState.terminated.signal}{"\n"}{end}'

如果应用没有把启动错误写到 stdout,可以通过临时调试容器或本地复现确认实际命令、环境变量和文件系统。

5. 找到控制器,而不是只处理单个 Pod

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{.metadata.ownerReferences}{"\n"}'

也可以:

kubectl -n "$NS" describe pod "$POD" | sed -n '/Controlled By:/p'

如果 Pod 由 Deployment 管理,应检查模板:

kubectl -n "$NS" get deployment payments-api -o yaml
kubectl -n "$NS" rollout history deployment/payments-api

直接编辑或删除单个 Pod 可能只是让控制器创建一个同样错误的新 Pod。修复应发生在 Deployment、StatefulSet 或 Job 的 Pod 模板上。


六、OOM:区分容器超限、节点压力和业务内存增长

1. OOM 的三个不同层次

“内存不够”至少有三种含义:

  1. 容器超过自身 memory limit
    cgroup 对容器施加内存上限,进程可能被内核 OOM 机制杀死,容器状态常见 reason: OOMKilled

  2. 节点整体内存压力
    节点可用内存不足,kubelet 可能触发 Eviction,驱逐 Pod。此时 Pod 可能以 EvictedFailed 或相关事件表现出来,和单个容器超限不是同一条路径。

  3. 调度器认为没有足够 requests
    Pod 可能一直 Pending,因为没有节点的可分配内存满足 request。此时容器甚至尚未运行,不可能已经发生容器内 OOM。

这三个路径可表示为:

Pod memory request 过大
        ↓
Scheduler 找不到可行节点
        ↓
Pending

容器实际使用量超过 memory limit
        ↓
cgroup/内核终止进程
        ↓
OOMKilled
        ↓
RestartPolicy 触发重启
        ↓
可能表现为 CrashLoopBackOff

节点整体内存压力
        ↓
kubelet Eviction 或节点级 OOM
        ↓
Pod 被驱逐或节点上的进程受影响

2. 查看 OOM 证据

kubectl -n "$NS" get pod "$POD" \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{" current="}{.state}{" last="}{.lastState.terminated.reason}{" exit="}{.lastState.terminated.exitCode}{"\n"}{end}'

检查资源配置:

kubectl -n "$NS" get pod "$POD" -o jsonpath='{range .spec.containers[*]}{.name}{" requests="}{.resources.requests}{" limits="}{.resources.limits}{"\n"}{end}'

检查节点压力:

kubectl describe node <node-name>
kubectl get events --field-selector involvedObject.kind=Node,involvedObject.name=<node-name> --sort-by=.lastTimestamp

如果集群安装了 Metrics Server,可以观察当前使用量:

kubectl top pod "$POD" -n "$NS" --containers
kubectl top node <node-name>

kubectl top 依赖 Metrics Server 或兼容的 metrics API。它提供的是当前或近期采样,不是历史峰值,也不能替代内核 OOM 证据。

3. request、limit 与 QoS

对内存而言:

  • request 主要影响调度;
  • limit 主要形成容器运行时可用的资源上限;
  • 实际内存使用可以低于 request,也可以暂时接近或达到 limit;
  • limit 不是性能保证,也不是应用应该长期使用的目标值。

Pod QoS 类别会影响节点压力下的驱逐排序:

  • Guaranteed:每个容器对 CPU 和内存都设置 request,且 request 等于 limit;
  • Burstable:至少设置了部分 request 或 limit,但不满足 Guaranteed;
  • BestEffort:没有设置 CPU 和内存 request/limit。

QoS 影响 Eviction 优先级,但不能简单推导为“Guaranteed 永远不会被杀”。例如节点发生不可恢复的内核级 OOM、节点故障或管理员主动删除时,Guaranteed Pod 仍可能终止。

4. 完整 OOM 算例

假设容器配置:

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"

业务进程使用:

heap             380Mi
native memory     90Mi
线程栈            30Mi
文件缓存/其他      40Mi
总计              540Mi

即使应用的堆只使用 380Mi,容器总内存仍可能达到:

380+90+30+40=540Mi380 + 90 + 30 + 40 = 540Mi

因为:

540Mi>512Mi540Mi > 512Mi

所以容器可能被杀死并显示 OOMKilled。只把 JVM、Node.js 或 Go 的堆上限设置为 512Mi,并不代表进程总内存可以安全使用 512Mi

修复时不能机械地“把 limit 调大”。应先判断:

  • 是真实流量增长还是内存泄漏;
  • 是 heap 增长还是 native memory、线程、缓存或 mmap 增长;
  • 节点是否有足够容量;
  • 增大 limit 后是否会造成更多 Pod 被调度到同一节点;
  • 是否需要配合 request 调整,否则调度模型与运行时风险可能失配。

对于使用 emptyDir 的应用,如果配置了内存介质:

volumes:
- name: cache
  emptyDir:
    medium: Memory

写入该卷的数据会消耗节点内存,并可能计入容器或 Pod 的内存使用。把大量缓存从堆移到内存盘并不会绕过内存约束。


七、Probe:健康检查如何改变 Pod 的生命周期

1. 三种探针分别解决不同问题

Startup Probe:应用是否完成启动

启动探针用于启动较慢的容器。只要 startup probe 尚未成功,kubelet 不会执行 liveness 和 readiness 探针。

startup 成功
    ↓
开始执行 liveness/readiness

startup 失败达到阈值
    ↓
容器被认为启动失败,通常按存活失败处理并重启

Readiness Probe:是否接收业务流量

readiness probe 失败时,容器通常不会被杀死,而是 Pod 不再满足 Ready 条件。Service 控制器据此从 EndpointSlice 中移除对应端点。

因此:

readiness 失败 ≠ 容器退出
readiness 失败 ≈ 暂停向该 Pod 发送 Service 流量

这适合数据库连接未建立、缓存未预热、依赖暂时不可用等情况,但前提是应用能继续运行并等待恢复。

Liveness Probe:进程是否需要被重启

liveness probe 失败达到阈值后,kubelet 会按照 Pod 的重启策略处理容器。它适合检测进程卡死、死锁、无法恢复的内部状态,不适合把所有外部依赖故障都定义为“重启”。

2. 探针的执行条件

探针可以使用:

  • httpGet
  • tcpSocket
  • exec
  • grpc

示例:

startupProbe:
  httpGet:
    path: /startup
    port: 8080
  periodSeconds: 5
  failureThreshold: 24

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  periodSeconds: 5
  timeoutSeconds: 2
  failureThreshold: 2

livenessProbe:
  httpGet:
    path: /live
    port: 8080
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

每次探针有成功、失败或超时结果。连续失败次数达到 failureThreshold 后才执行相应动作;偶发一次失败不一定触发状态变化。

探针检测时间可以粗略估算为:

TdetectinitialDelaySeconds+(failureThreshold1)×periodSeconds+timeoutSecondsT_{detect} \approx initialDelaySeconds + (failureThreshold - 1)\times periodSeconds + timeoutSeconds

这只是便于推理的近似,因为 kubelet 的调度、探针执行和容器状态更新存在实现细节。对上面的 liveness 配置,理论上大约需要多个探测周期才会触发重启,而不是第一次失败就立即重启。

3. HTTP Probe 的常见边界

HTTP 探针请求的是 Pod IP 和指定端口。它不等价于:

  • 从集群外部访问负载均衡器;
  • 通过 Service DNS 访问自身;
  • 验证完整业务链路;
  • 验证所有下游依赖都健康。

如果应用只监听 127.0.0.1,而 kubelet 从 Pod 网络地址访问 Pod IP,HTTP Probe 可能失败。通常应用应监听适合容器网络的地址,例如 0.0.0.0,但是否这样配置要结合应用安全边界判断。

探针端口可以是数字或容器端口名。端口名修改后,探针引用失效可能导致 connection refused,而容器自身仍然正常。

4. exec Probe 的风险

readinessProbe:
  exec:
    command:
    - /bin/sh
    - -c
    - test -f /tmp/ready

exec 探针在容器内执行命令,依赖镜像中存在的 shell 或工具。精简镜像中可能没有 /bin/shcurlwget,导致探针永远失败。

还要注意:

  • 探针命令会消耗 CPU 和进程资源;
  • 复杂命令可能制造额外负载;
  • 命令中的凭据可能泄露到进程或配置;
  • 探针不应执行破坏性操作;
  • 探针脚本成功退出码必须是 0

5. 探针失败与 CrashLoop 的因果链

一种常见故障链是:

应用启动较慢
        ↓
未配置 startupProbe
        ↓
livenessProbe 过早执行
        ↓
连续失败达到 failureThreshold
        ↓
kubelet 杀死并重启容器
        ↓
容器永远无法完成启动
        ↓
CrashLoopBackOff

这时容器日志可能显示“启动中”,而 describe pod 的事件显示:

Liveness probe failed: HTTP probe failed with statuscode: 503
Container failed liveness probe, will be restarted

修复应先验证应用真实启动时间和端点语义,再考虑:

  • 用 startup probe 覆盖启动窗口;
  • 增大合理的 initialDelaySeconds
  • 调整 timeoutSecondsperiodSecondsfailureThreshold
  • 将 liveness 设计为轻量的“进程是否还能自我恢复”检查;
  • 将依赖可用性放进 readiness,而不是 liveness。

6. 通过事件和状态确认探针故障

kubectl -n "$NS" describe pod "$POD"

检查:

kubectl -n "$NS" get pod "$POD" -o yaml

重点看:

spec:
  containers:
  - name: api
    livenessProbe: ...
    readinessProbe: ...
    startupProbe: ...
status:
  conditions: ...
  containerStatuses: ...

Ready=False 只能说明 Pod 当前未就绪,不能单独证明是 readiness probe。还要查看 conditions[].reason、事件和容器状态。


八、终止、删除和 Eviction:为什么“重启 Pod”可能改变证据

1. Pod 终止是一个有序过程

删除 Pod 时,API Server 通常先设置删除时间戳,kubelet 观察到后执行终止流程:

  1. Pod 进入终止阶段;
  2. 执行 preStop(如果配置);
  3. 向容器主进程发送 SIGTERM;
  4. 等待 terminationGracePeriodSeconds
  5. 超时后发送 SIGKILL;
  6. kubelet 更新状态并清理容器。

应用需要正确处理 SIGTERM。若应用只在 SIGTERM 后继续阻塞,最终可能被 SIGKILL 终止,表现为非正常退出。

查看终止信息:

kubectl -n "$NS" get pod "$POD" -o jsonpath='{.metadata.deletionTimestamp}{"\n"}{.spec.terminationGracePeriodSeconds}{"\n"}'

强制删除:

kubectl -n "$NS" delete pod "$POD" --grace-period=0 --force

这是高风险操作。它可能绕过正常的 kubelet 确认流程,使 API Server 很快移除对象,但节点上的进程或外部资源清理可能滞后。对有状态服务、持久卷、分布式锁和网络连接尤其危险。除非 Pod 已经卡在终止状态并经过确认,否则不应把强制删除作为普通排障手段。

2. Eviction 不是 OOMKilled

Eviction 是 kubelet 在节点资源压力下主动驱逐 Pod 的机制,常见信号包括:

  • memory.available
  • nodefs.available
  • nodefs.inodesFree
  • imagefs.available

驱逐前,kubelet 可能根据 Pod 的 QoS、优先级、资源使用情况等因素选择对象。被驱逐的 Pod 往往在事件中出现:

The node was low on resource: memory

或者对象原因显示为 Evicted

对比:

OOMKilled:
  某个容器/进程被杀死,通常与容器内存约束或内核 OOM 路径有关。

Evicted:
  kubelet 因节点资源压力驱逐 Pod,属于节点级资源管理行为。

二者可能连续发生,但证据不同。不要看到 exitCode=137 就直接断言是业务容器 limit 不够,也不要看到 Pending 就断言节点发生 OOM。


九、从症状到根因的完整排障流程

下面流程适用于单个工作负载,但每一步都应保留输出,避免边查边破坏现场。

第一步:确认对象、控制器和节点

kubectl --context="$CTX" -n "$NS" get pod "$POD" -o wide
kubectl --context="$CTX" -n "$NS" get pod "$POD" \
  -o jsonpath='{.metadata.uid}{"\n"}{.metadata.ownerReferences}{"\n"}'

要确认:

  • 是否看的是正确 Pod;
  • Pod 是否被控制器管理;
  • 是否已经绑定节点;
  • 是否有多个容器;
  • 重启次数是否持续增加。

第二步:查看状态、事件和日志

kubectl --context="$CTX" -n "$NS" describe pod "$POD"
kubectl --context="$CTX" -n "$NS" logs "$POD" -c "$CONTAINER" --previous --timestamps --tail=300
kubectl --context="$CTX" -n "$NS" logs "$POD" -c "$CONTAINER" --timestamps --tail=300

选择日志参数时要注意:

  • 未启动过的容器没有 --previous 日志;
  • 正在频繁重启的容器优先看 --previous
  • Sidecar 需要分别查看;
  • 日志中可能包含 Secret、Token、用户数据,复制到工单前要脱敏。

第三步:按状态分流

Pending

先看:

kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.conditions}{"\n"}'
  • PodScheduled=False:检查 requests、taint/toleration、affinity、selector、PVC 和拓扑约束;
  • PodScheduled=True:检查镜像、init container、卷、CNI、CSI 和 runtime;
  • ContainerCreating:重点查看 Events 和节点 kubelet/runtime;
  • init container 未完成:查看 init container 日志。

ImagePullBackOff

检查:

kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.spec.imagePullSecrets}{"\n"}'

按事件中的具体错误区分:

  • not found:镜像名、仓库、tag 或 digest 错误;
  • unauthorized:权限或凭据错误;
  • no matching manifest:架构或平台不匹配;
  • x509:证书或信任链问题;
  • timeout:节点到 registry 的网络问题;
  • no space left:节点磁盘或镜像文件系统问题。

CrashLoopBackOff

检查:

kubectl -n "$NS" logs "$POD" -c "$CONTAINER" --previous
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.containerStatuses}{"\n"}'
kubectl -n "$NS" describe pod "$POD"

重点寻找:

  • lastState.terminated.reason
  • exit code 和 signal;
  • liveness/startup 失败事件;
  • 配置、挂载、权限和依赖错误;
  • 容器 command/args 是否覆盖了镜像默认启动命令。

OOMKilled

检查:

kubectl -n "$NS" get pod "$POD" -o jsonpath='{.status.containerStatuses}{"\n"}'
kubectl -n "$NS" get pod "$POD" -o jsonpath='{.spec.containers[*].resources}{"\n"}'
kubectl describe node <node-name>
kubectl top pod "$POD" -n "$NS" --containers

然后区分:

  • 容器使用量超过 limit;
  • 节点整体内存压力;
  • 内存泄漏或峰值;
  • sidecar 或内存盘消耗;
  • limits/request 与实际工作负载不匹配。

Probe 失败

检查事件和 YAML:

kubectl -n "$NS" describe pod "$POD"
kubectl -n "$NS" get pod "$POD" -o yaml

验证:

  • 探针路径和端口是否真实存在;
  • 应用监听地址是否可被 kubelet 访问;
  • startupProbe 是否覆盖真实启动时间;
  • readiness 和 liveness 是否承担了错误职责;
  • exec 命令是否存在;
  • 探针超时是否过短;
  • 探针自身是否造成高负载。

第四步:确认修复的是源对象

如果 Pod 由 Deployment 管理:

kubectl -n "$NS" get deployment payments-api -o yaml
kubectl -n "$NS" rollout status deployment/payments-api

修改镜像、资源、探针或 Secret 引用时,应更新 Deployment 的 Pod 模板,使用版本控制和发布流程。不要把手工 kubectl edit pod 当作长期修复,因为控制器会重新生成 Pod。


十、Debug:当日志不足时如何建立安全的现场

1. 临时查看文件系统和进程

对于仍在运行但无法通过网络访问的容器,可以使用:

kubectl -n "$NS" exec -it "$POD" -c "$CONTAINER" -- sh

前提是镜像中有 sh,且容器尚未退出。

如果镜像很精简,没有 shell,可以考虑临时调试容器:

kubectl -n "$NS" debug -it "$POD" \
  --image=busybox:1.36 \
  --target="$CONTAINER" \
  -- sh

kubectl debug 的具体行为依赖集群版本、容器运行时和节点配置。--target 通常请求调试容器加入目标容器的进程命名空间,但不保证所有环境都能看到目标进程。调试镜像还可能带来:

  • 额外的镜像拉取问题;
  • 更高权限;
  • 对生产 Pod 的侵入;
  • Secret、进程参数和文件内容暴露;
  • 临时容器对象残留在审计和状态中。

调试容器不应默认使用特权模式,也不应把生产 Secret 复制到调试环境。

2. 创建临时副本进行复现

对已被控制器持续重建、无法稳定观察的工作负载,可以复制配置到隔离命名空间,或者创建专门的 Debug Pod。复制时要清理:

  • 生产 ServiceAccount;
  • Secret;
  • hostPath;
  • 特权安全上下文;
  • 持久卷;
  • 生产网络入口;
  • 不必要的云厂商身份绑定。

调试的目标是验证命令、镜像、配置和探针行为,而不是扩大生产权限。


十一、Patch 和恢复操作:先知道修改的对象和副作用

1. 使用 Patch 修改探针时要谨慎

例如,给 Deployment 增加一个 startup probe:

kubectl -n "$NS" patch deployment payments-api \
  --type='strategic' \
  -p '{
    "spec": {
      "template": {
        "spec": {
          "containers": [{
            "name": "api",
            "startupProbe": {
              "httpGet": {
                "path": "/startup",
                "port": 8080
              },
              "periodSeconds": 5,
              "failureThreshold": 24
            }
          }]
        }
      }
    }
  }'

这个命令的成立条件是:

  • 资源类型是 Deployment;
  • 容器名确实是 api
  • /startup 在容器内可访问;
  • 端口 8080 是正确的容器端口;
  • 当前用户有修改权限;
  • 集群 API 版本支持该字段。

修改 Pod 模板通常会触发新的 ReplicaSet 和滚动更新。执行后必须验证:

kubectl -n "$NS" rollout status deployment/payments-api
kubectl -n "$NS" get rs
kubectl -n "$NS" get pods -l app=payments-api -o wide

如果 Patch 只为了临时恢复,仍应把最终变更提交到配置仓库,否则下一次发布可能覆盖它。

2. 不要用修改 liveness 的方式掩盖真实故障

临时删除 liveness probe 可能让 Pod 不再重启,但并不代表应用健康。若应用已经死锁或无法服务,结果可能变成:

容器 Running
Pod Ready
但请求持续失败

更安全的顺序是:

  1. 先收集 --previous 日志和事件;
  2. 判断探针是否错误;
  3. 如果探针确实过早或语义错误,修改为符合应用生命周期的检查;
  4. 用 rollout 验证新版本;
  5. 观察 Ready 状态和实际业务请求,而不是只看 Pod 是否 Running。

3. 删除 Pod 什么时候有意义

删除单个 Pod 适合以下场景:

  • 修复已经写入控制器模板,需要让新 Pod 使用新配置;
  • Pod 处于不可恢复的异常状态;
  • 节点或运行时问题已处理,需要重新调度;
  • 需要验证控制器是否能正常重建副本。

删除 Pod 不适合用来解决:

  • 错误的镜像地址;
  • 错误的环境变量;
  • 缺失的 Secret;
  • 代码启动崩溃;
  • 资源 limit 过小;
  • 永远失败的探针。

这些问题会在新 Pod 中重现。


十二、常见误判及其反例

误判一:Pending 就是“没有资源”

反例:

PodScheduled=True
container state=Waiting
reason=ImagePullBackOff

这个 Pod 已经完成调度,资源不是当前根因。应该检查镜像事件和 registry,而不是继续调整节点容量。

误判二:Running 就是应用正常

反例:

phase: Running
Ready: False
readiness probe failed

Pod 进程可以存在,但 Service 不应把流量发给它。Running 与“可接收请求”是不同维度。

误判三:CrashLoopBackOff 一定是代码崩溃

反例:

lastState.terminated.reason: OOMKilled

这里的根因可能是内存限制或内存增长,而不是业务异常退出。

另一个反例是:

Events:
Liveness probe failed
Container failed liveness probe, will be restarted

容器可能没有自行崩溃,而是被 kubelet 根据探针结果重启。

误判四:退出码 137 一定是容器 limit 超了

反例:

  • 节点内核在全局 OOM 时杀死了进程;
  • 运行时或外部系统发送了 SIGKILL;
  • Pod 被强制终止。

因此必须联合检查 reason、节点事件、容器资源和监控。

误判五:给所有探针都访问 /health

如果 /health 同时检查数据库、消息队列、第三方 API,并被 liveness 使用,那么外部依赖短暂故障可能导致所有应用实例被重启,形成级联故障。

更准确的划分是:

/live   :进程和内部状态是否还能自我恢复
/ready  :此刻是否适合接收流量
/startup:启动过程是否完成

这不是 Kubernetes 强制规定的路径,而是应用端点语义的工程约定。Kubernetes 只关心探针结果,不理解路径名称本身。


十三、把状态、事件、日志和指标拼成因果证据

一个可靠的故障结论至少应回答四个问题:

  1. 对象状态是什么?
    查看 Phase、Conditions、ContainerStatuses、RestartCount。

  2. 最后一次状态转换由谁触发?
    查看 Events、终止原因、探针失败记录和控制器历史。

  3. 底层组件看到了什么?
    调度问题看 Scheduler 事件;拉取问题看 kubelet/runtime 与 registry;节点资源问题看 Node Conditions 和 Eviction 事件。

  4. 修复后如何证明恢复?
    查看新 Pod 的 UID、Ready Condition、EndpointSlice、滚动发布状态和实际业务请求。

例如,下面是一条完整的 OOM 因果链:

容器 memory limit = 512Mi
        ↓
监控显示工作集持续超过 500Mi
        ↓
lastState.reason = OOMKilled
        ↓
exitCode = 137
        ↓
restartCount 增加
        ↓
容器启动后再次达到峰值
        ↓
状态显示 CrashLoopBackOff

这里 CrashLoopBackOff 是最终外显状态,OOMKilled 才是更接近根因的证据。若只执行 delete pod,新 Pod 仍会沿同一链路失败。

相反,若证据是:

PodScheduled=False
reason=Unschedulable
event=Insufficient memory

则容器尚未启动,不应分析应用日志或 liveness probe。


十四、生产环境中的恢复验证

修复完成后,验证不能只看一条命令。

1. 验证控制器发布

kubectl -n "$NS" rollout status deployment/payments-api
kubectl -n "$NS" get deployment payments-api

2. 验证 Pod 层状态

kubectl -n "$NS" get pods -l app=payments-api -o wide
kubectl -n "$NS" get pod <new-pod> \
  -o jsonpath='{.status.phase}{" Ready="}{range .status.conditions[?(@.type=="Ready")]}{.status}{end}{"\n"}'

3. 验证容器没有继续重启

kubectl -n "$NS" get pods -l app=payments-api \
  -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,READY:.status.containerStatuses[*].ready,RESTARTS:.status.containerStatuses[*].restartCount'

需要连续观察一段覆盖启动、探针和业务初始化的时间。单次看到 Running 不能证明故障已恢复。

4. 验证 Service 实际端点

kubectl -n "$NS" get endpointslice \
  -l kubernetes.io/service-name=payments-api -o yaml

如果 Pod 为 Running 但 readiness 失败,它可能不会出现在可用端点中。只有 Pod Ready、Service 选择器匹配且 EndpointSlice 更新后,流量路径才真正恢复。


结语:先定位状态层,再沿故障路径取证

这几类状态可以用一张简化表收束:

现象 先看什么 常见根因
Pending PodScheduled、Events、init container 调度约束、资源 request、卷、镜像
ImagePullBackOff Events、镜像名、Secret、节点网络 镜像不存在、认证、架构、registry 或磁盘
CrashLoopBackOff --previous、lastState、退出码、探针事件 应用退出、配置错误、OOM、liveness
OOMKilled limit、使用量、节点压力、历史指标 容器超限、泄漏、峰值、节点 OOM
Probe 失败 探针定义、事件、应用端点语义 路径/端口错误、启动过慢、超时、依赖故障

正确的排障不是把状态清零,而是重建状态变化的因果链:

API 对象
  → 调度条件
  → 节点与 kubelet 事件
  → 容器当前/上次状态
  → 日志与退出原因
  → 探针和资源约束
  → 控制器恢复与业务端点验证

只有当修复后的 Pod 能持续运行、满足正确的 Ready 条件,并在实际流量路径上可用时,才可以认为工作负载真正恢复。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。