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

Kubelet 与容器运行时:CRI、Pod Sync、PLEG、Probe 和资源状态

Kubelet 是每个节点上的节点代理。它从 API Server 获取分配给本节点的 Pod,对这些 Pod 进行本地编排,并通过容器运行时接口(Container Runtime Interface,CRI)请求容器运行时创建、启动、停止和查询容器。

Kubelet 并不直接负责实现容器隔离。真正创建 Linux namespace、cgroup、网络接口和容器进程的是容器运行时及其底层组件。Kubelet 的核心工作可以概括为:

PodSpec期望状态本地运行状态状态汇报与再次调谐\text{PodSpec}_{\text{期望状态}} \longrightarrow \text{本地运行状态} \longrightarrow \text{状态汇报与再次调谐}

这个过程不是一次性脚本,而是一个持续运行的控制循环。CRI、Pod Sync、PLEG、Probe 和资源状态分别处在这个循环的不同位置。


一、先建立组件关系

典型的数据流如下:

flowchart LR
    API[API Server] -->|PodSpec、NodeSpec| K[Kubelet]
    K --> P[Pod Sync / Pod Workers]
    P -->|CRI gRPC| R[Container Runtime]
    R --> S[Sandbox]
    R --> C[Containers]

    R -->|容器状态、事件、统计| PLEG[PLEG]
    PLEG --> P

    P --> Probe[Probe Manager]
    Probe -->|HTTP/TCP/Exec/gRPC| C
    Probe --> P

    K -->|PodStatus、NodeStatus| API
    K --> Evict[Eviction Manager]
    Evict -->|资源压力、驱逐决定| P
    Stats[CRI / cAdvisor / cgroup / 文件系统] --> Evict

关键路径是:

  1. API Server 中存在一个绑定到节点的 Pod。
  2. Kubelet 收到 Pod 变化,或者定期重新处理该 Pod。
  3. Pod Sync 根据 PodSpec 和当前状态计算需要执行的动作。
  4. Kubelet 通过 CRI 调用容器运行时。
  5. PLEG 观察运行时状态变化,帮助 Kubelet 发现容器退出、创建或删除。
  6. Probe Manager 对运行中的容器执行健康检查。
  7. Kubelet 将 PodStatus 和 NodeStatus 汇报回 API Server。
  8. 资源监控和驱逐逻辑可能改变 Pod 的生命周期。

这几个组件并非严格串行。Probe、PLEG、状态汇报和资源监控都有自己的事件或周期,因此需要把 Kubelet 理解成多个相互协调的异步控制循环,而不是单个函数顺序执行。


二、CRI:Kubelet 与容器运行时之间的稳定边界

2.1 CRI 解决什么问题

CRI 是 Kubelet 与容器运行时之间的 gRPC 接口。它把 Kubernetes 节点管理逻辑与具体运行时解耦:

  • Kubelet 不需要知道运行时如何实现镜像存储;
  • Kubelet 不需要直接调用 runc 或其他 OCI runtime;
  • 运行时可以使用不同的容器管理实现;
  • Kubelet 通过统一接口查询 Pod、Sandbox、Container 和资源统计。

当前 Kubernetes 使用 CRI v1 作为正式接口。节点上的运行时通常通过 Unix Domain Socket 提供服务,例如:

unix:///run/containerd/containerd.sock
unix:///var/run/crio/crio.sock

具体 socket 路径取决于发行版和运行时,不应在脚本中硬编码为某一个路径。

CRI 有两类主要服务:

  • RuntimeService:管理 Pod Sandbox 和容器;
  • ImageService:拉取、删除、查询镜像。

CRI 并不是 Kubernetes API。它使用自己的对象和状态模型;例如,CRI 中常见的对象是:

  • PodSandbox
  • Container
  • PodSandboxStatus
  • ContainerStatus
  • Image

2.2 为什么 Pod 需要 Sandbox

Kubernetes 的 Pod 是调度和管理单位。一个 Pod 中的多个容器通常共享:

  • 网络命名空间;
  • Pod IP;
  • IPC 命名空间;
  • 可选的 PID 命名空间;
  • 通过 emptyDir 等机制提供的共享卷。

因此,运行时通常先为 Pod 创建一个 Sandbox,再把 Pod 内的普通容器加入这个 Sandbox。很多 Linux 容器运行时会创建一个保持网络命名空间存活的基础容器,常被称为 pause 或 infra container,但这个名称和实现不是 Kubernetes API 的规范保证。

典型顺序如下:

RunPodSandbox
    ↓
CreateContainer(init-1)
StartContainer(init-1)
    ↓
CreateContainer(init-2)
StartContainer(init-2)
    ↓
CreateContainer(app)
StartContainer(app)
    ↓
配置 Probe、同步状态

如果 Sandbox 失效,Kubelet 通常不能只重新启动普通容器,因为普通容器依赖原有的 namespace。常见处理是:

停止或移除旧 Sandbox
    ↓
创建新 Sandbox
    ↓
重新创建 Pod 内容器

这也是为什么“容器进程还在”不一定意味着 Pod 网络仍然正常:Sandbox 或其网络配置可能已经异常。

2.3 Pod 生命周期中的 CRI 调用

一个简化的 Pod 创建流程可以表示为:

sequenceDiagram
    participant A as API Server
    participant K as Kubelet
    participant R as Runtime
    participant N as CNI
    participant P as Pod进程

    A->>K: PodSpec
    K->>R: RunPodSandbox(config)
    R->>N: 配置 Pod 网络
    N-->>R: Pod IP / 网络结果
    R-->>K: sandboxID

    K->>R: PullImage(必要时)
    K->>R: CreateContainer(init)
    K->>R: StartContainer(init)
    R-->>K: init 完成

    K->>R: CreateContainer(app)
    K->>R: StartContainer(app)
    R-->>P: 启动进程

    K->>R: ContainerStatus / PodSandboxStatus
    K->>A: PodStatus

这里的 PullImage 不一定每次都真正下载镜像。运行时会根据镜像引用、镜像缓存和 imagePullPolicy 决定是否需要拉取。

CRI 调用失败时,错误必须按阶段区分:

阶段 典型失败 可能表现
RunPodSandbox CNI、网络、cgroup、运行时故障 Pod 长期 Pending 或 Sandbox 创建失败
PullImage 镜像不存在、认证失败、网络失败 ErrImagePullImagePullBackOff
CreateContainer volume、权限、namespace 或参数错误 容器创建失败
StartContainer 入口程序、挂载、运行时启动失败 CrashLoopBackOff 或快速退出
StopContainer 进程不响应、运行时卡死 Pod 删除延迟、节点清理不完整

CrashLoopBackOff 不是 CRI 返回的独立容器状态,而是 Kubelet 针对反复启动失败实施退避后,在 Pod 事件和容器状态中呈现的结果。

2.4 使用 crictl 观察 CRI

crictl 直接访问 CRI,适合绕过 Kubernetes API 检查节点本地状态。前提是已经安装它,并且配置了正确的运行时 endpoint。

sudo crictl info
sudo crictl pods
sudo crictl ps -a
sudo crictl images

可能看到:

CONTAINER           IMAGE                  CREATED        STATE
a1b2c3d4            nginx:1.27             2 minutes ago  Running
e5f6g7h8            busybox:1.36           2 minutes ago  Exited

查询某个容器:

sudo crictl inspect a1b2c3d4

查询 Sandbox:

sudo crictl pods --name my-pod
sudo crictl inspectp <sandbox-id>

这些输出反映的是 CRI 运行时所见的状态,不等价于 API Server 中的 PodStatus。例如,运行时可能已经报告容器退出,但 Kubelet 尚未完成状态同步,因此短时间内 crictlkubectl get pod 可能不一致。

生产环境中直接执行 crictl rmcrictl stopp 或手工删除运行时目录有较高风险:Kubelet 可能同时在进行 Pod Sync,造成状态竞争、残留挂载或错误事件。诊断时优先使用只读命令;确需强制清理,应先确认 Kubelet 和运行时的并发行为,并准备节点级恢复方案。


三、Pod Sync:Kubelet 如何把 PodSpec 变成本地动作

3.1 Pod Sync 不是一次同步,而是持续调谐

Kubelet 维护每个本地 Pod 的期望状态和实际状态。可以形式化为:

A(P)=Plan(D(P),O(P),E(P))A(P) = \operatorname{Plan}(D(P), O(P), E(P))

其中:

  • PP 是一个 Pod;
  • D(P)D(P) 是 API Server 提供的期望状态,例如镜像、命令、挂载、Probe 和资源限制;
  • O(P)O(P) 是运行时和 Kubelet 当前观察到的状态;
  • E(P)E(P) 是事件、Probe 结果、删除请求和节点资源压力等外部输入;
  • A(P)A(P) 是需要执行的动作集合,例如创建、启动、停止、重建或更新状态。

理想情况下,调谐会使:

O(P)D(P)O(P) \rightarrow D(P)

但这个关系不是简单的“字段完全相等”。容器一旦创建,某些字段不能原地修改,例如:

  • 容器镜像;
  • 启动命令;
  • 大部分安全上下文;
  • 端口和挂载等容器创建参数。

所以当 PodSpec 发生不可变字段变化时,Kubelet 的实际动作不是更新已有容器,而是停止旧容器并创建新容器。Deployment 的滚动更新正是利用了新的 Pod,而不是原地修改现有容器。

3.2 Kubelet 如何知道需要重新 Sync

常见触发源包括:

  • Pod Informer 收到新增、更新或删除事件;
  • PLEG 报告容器或 Sandbox 状态变化;
  • Probe 状态发生变化;
  • 定时同步;
  • 节点资源压力触发驱逐;
  • Kubelet 启动后加载本地状态;
  • Pod 删除或终止流程继续推进。

事件通常先进入队列,再由 Pod worker 处理。实现细节会随 Kubelet 版本变化,但有两个稳定的工程事实:

  1. 事件处理是异步的;
  2. 同一个 Pod 的操作需要避免并发地互相破坏。

因此,从 API Server 修改 PodSpec,到容器实际改变,再到 PodStatus 更新,通常存在延迟。不能把 API 写入成功理解成节点动作已经完成。

3.3 Pod Sync 的重要状态分层

一个 Pod 至少有以下几层状态:

  1. API 期望状态:PodSpec;
  2. Kubelet 内部状态:同步阶段、容器重启策略、终止进度;
  3. CRI 状态:Sandbox 和容器是否存在、是否运行、退出码和原因;
  4. Probe 状态:就绪、存活、启动探针的最近结果;
  5. API 可见状态:PodStatus、ContainerStatus 和 Conditions。

这些状态可能短暂不一致。例如:

容器进程已退出
    ↓
运行时已更新 ContainerStatus
    ↓
PLEG 检测到退出事件
    ↓
Pod worker 执行重启策略
    ↓
Kubelet 更新 PodStatus

如果在第二步和第五步之间执行查询,crictlkubectl 可能显示不同结果。这不一定表示数据损坏,而是异步状态传播的正常表现。

3.4 Pod Sync 与 Pod 状态的几个边界

restartPolicy 只控制容器重启策略

Pod 的 restartPolicy 目前包括:

  • Always
  • OnFailure
  • Never

它作用于 Pod 内普通容器的退出处理,不等价于“整个 Pod 永远恢复”。

例如,restartPolicy: Never 的容器退出后,Kubelet 不会在同一个 Pod 内重新启动它;但控制器可能根据 Job、Deployment 等更高层对象创建新的 Pod。

Pod phase 不是完整状态机

Pod phase 是较粗粒度的摘要状态:

  • Pending
  • Running
  • Succeeded
  • Failed
  • Unknown

它不应该被当作容器级状态的替代品。一个 Pod 处于 Running,并不保证所有应用容器都 Ready;一个 Pod 处于 Pending,也可能是镜像未拉取、Sandbox 未创建或调度之外的多种原因。

删除 Pod 是终止流程

删除 Pod 后,API 对象通常先进入 terminating 状态。Kubelet 会:

  1. 执行容器终止逻辑;
  2. 处理 preStop
  3. 向容器发送终止信号;
  4. 等待终止宽限期;
  5. 必要时发送强制终止;
  6. 清理容器、Sandbox、挂载和本地状态。

preStop 不是 Probe,也不是 CRI 的独立健康检查。它是容器终止流程的一部分,执行失败不会自动阻止删除流程。


四、PLEG:Kubelet 如何发现运行时发生了什么

4.1 PLEG 的职责

PLEG 是 Pod Lifecycle Event Generator,即 Pod 生命周期事件生成器。它的作用是观察容器运行时状态,并生成供 Kubelet 处理的生命周期事件,例如:

  • 容器创建;
  • 容器启动;
  • 容器退出;
  • 容器删除;
  • Sandbox 状态变化。

PLEG 的关键价值在于:Kubelet 不能只依赖 API Server 事件,因为容器运行时的状态变化并不会先写回 API Server。容器可能因为进程自身崩溃、节点内存不足、运行时异常或外部信号退出,PLEG 帮助 Kubelet 发现这些本地变化。

4.2 传统 relist 模型

传统 PLEG 的基本思路是周期性查询运行时:

读取运行时中所有 PodSandbox 和 Container
    ↓
与上一次缓存比较
    ↓
发现新增、消失或状态变化
    ↓
生成 PodLifecycleEvent
    ↓
唤醒对应 Pod 的同步逻辑

设第 tt 次查询得到运行时状态集合 RtR_t,上次缓存为 Rt1R_{t-1},则:

Δt=RtRt1\Delta_t = R_t - R_{t-1}

这里的差异不仅是对象增删,也包括对象状态字段变化。PLEG 将差异转换成生命周期事件。

这种方式的优点是简单、对运行时事件可靠性要求较低;缺点是需要周期性列举对象。当节点上 Pod 和容器数量很多、CRI 响应慢或运行时被拖垮时,relist 可能变慢。

4.3 PLEG 不是重启器,也不是 Probe

常见误解包括:

  • PLEG 发现容器退出,所以 PLEG 负责重启容器;
  • PLEG 检查 HTTP,所以 PLEG 等于 liveness probe;
  • PLEG 卡住,所以所有容器都已经停止。

更准确的因果关系是:

运行时状态变化
    ↓
PLEG 发现并生成事件
    ↓
Pod Sync 重新计算期望动作
    ↓
Kubelet 根据 restartPolicy 和 Pod 状态决定是否重启

PLEG 只负责观察和通知。真正执行重启、删除旧 Sandbox、创建容器的是 Pod Sync 和 CRI 调用。

Probe 检查的是应用定义的健康条件;PLEG 检查的是容器和 Sandbox 的生命周期状态。一个进程可能仍然运行但 HTTP 服务已经失效,这时 Probe 可以失败,而 PLEG 仍会认为容器处于 Running。

4.4 PLEG 延迟的故障表现

当 PLEG 或 CRI 查询长期变慢,常见表现包括:

  • 容器已经退出,但 kubectl 中状态长时间不更新;
  • Pod 删除卡住;
  • Kubelet 日志出现 PLEG relist 或 runtime 操作耗时过长;
  • 节点被标记为不健康,最终可能影响 Ready
  • 大量 Pod 同时发生状态延迟。

诊断时应同时检查:

kubectl describe node <node-name>
kubectl get events --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp
sudo journalctl -u kubelet --since "30 min ago"
sudo journalctl -u containerd --since "30 min ago"   # 使用 containerd 时
sudo crictl ps -a
sudo crictl pods

如果 crictl 查询本身就卡住或极慢,问题重点通常在运行时、磁盘、文件系统、cgroup 或节点负载,而不是 API Server。

不同 Kubernetes 版本可能提供不同的 PLEG 实现或实验性事件机制。不要把某个版本的内部指标名、默认 relist 周期或特性门控当成跨版本 API 保证;诊断时应以当前版本的 Kubelet 日志、指标和源码配置为准。


五、Probe:Kubelet 如何判断应用是否可用

Probe 是 Kubelet 对容器执行的健康检查。它不是由 kubelet 进程直接读取应用内部状态,而是通过指定的探测方式得到一个结果。

常用探测方式包括:

  • HTTP GET;
  • TCP Socket;
  • Exec;
  • gRPC;
  • tcpSockethttpGetexec 是长期常用的 API 方式;
  • gRPC Probe 需要注意 Kubernetes 版本和运行时环境的支持情况。

Probe 结果通常可以理解为:

Success       检查成功
Failure       检查明确失败
Unknown       无法得到可靠结果

具体状态处理和重试由 Kubelet 按探针参数执行,不能简单把一次网络超时直接等同于容器立刻重启。

5.1 三种 Probe 的职责不同

Startup Probe:启动阶段保护

startupProbe 用于判断应用是否已经完成启动。只要启动探针尚未成功:

  • liveness probe 不会开始决定容器是否不健康;
  • readiness probe 也不会把容器判定为可接收流量。

这适合启动时间不稳定的应用,例如需要加载大型模型、执行数据库迁移或恢复本地缓存的服务。

Liveness Probe:是否应重启

liveness probe 失败达到阈值后,Kubelet 会认为容器不健康,并根据 Pod 的重启策略处理容器。它的典型作用是发现:

  • 进程仍在,但死锁;
  • 内部工作线程永久停止;
  • 服务进入无法自行恢复的状态。

liveness 失败并不表示 Pod 会被删除或重新调度。通常是当前节点上的容器被重启;如果节点本身失效,才可能由更高层控制器在其他节点创建替代 Pod。

Readiness Probe:是否接收流量

readiness probe 失败会使容器不 Ready。对于由 Service 选择的 Pod,EndpointSlice 控制器通常会据此移除或标记该端点,使流量不再发送到该容器。

readiness 失败一般不会重启容器。一个应用可以:

进程仍正常运行
    ↓
readiness 失败
    ↓
停止接收 Service 流量

这正是排空、过载保护和依赖不可用时的常见行为。

5.2 一个可运行的 Probe 示例

下面的 Pod 使用一个 HTTP 服务作为示例:

apiVersion: v1
kind: Pod
metadata:
  name: probe-demo
  labels:
    app: probe-demo
spec:
  containers:
  - name: app
    image: registry.k8s.io/e2e-test-images/agnhost:2.53
    args:
    - netexec
    - --http-port=8080
    ports:
    - name: http
      containerPort: 8080
    startupProbe:
      httpGet:
        path: /healthz
        port: http
      periodSeconds: 5
      failureThreshold: 12
    readinessProbe:
      httpGet:
        path: /healthz
        port: http
      periodSeconds: 5
      timeoutSeconds: 2
      failureThreshold: 3
    livenessProbe:
      httpGet:
        path: /healthz
        port: http
      periodSeconds: 10
      timeoutSeconds: 2
      failureThreshold: 3

创建并观察:

kubectl apply -f probe-demo.yaml
kubectl get pod probe-demo -w
kubectl describe pod probe-demo

describe 中可能看到:

Readiness probe failed: HTTP probe failed with statuscode: 500

如果 readiness 失败,Pod 可能仍显示 Running,但 Ready 条件为 False。如果 liveness 连续失败达到 failureThreshold,则可能出现容器重启和事件:

Container probe-demo failed liveness probe, will be restarted

这个示例成立的前提是镜像实际提供 netexec/healthz 端点;镜像版本或测试镜像行为发生变化时,应先检查镜像说明。生产环境不要直接复制测试镜像和端点,应让应用提供语义稳定的健康检查接口。

5.3 Probe 时间参数的计算

对一个启动探针,允许的最长探测窗口可粗略估算为:

TstartupI+F×PT_{\text{startup}} \approx I + F \times P

其中:

  • II 是首次探测前的 initialDelaySeconds
  • FF 是允许失败次数 failureThreshold
  • PPperiodSeconds

但这是近似值,不应当当作精确调度保证。探测执行时间、调度延迟、超时和边界时刻都会影响实际结果。

例如:

startupProbe:
  periodSeconds: 5
  failureThreshold: 12

直觉上提供约 60 秒的失败容忍窗口。若应用实际需要 90 秒,Kubelet 可能在应用完成启动前将其判定为启动失败,并进一步触发容器重启。

一个常见反例是把相同的快速 liveness probe 用于慢启动应用:

应用启动需要 2 分钟
liveness 每 5 秒检查一次
failureThreshold = 3

应用可能在 15 秒左右就被重启,永远到不了可服务状态。这是启动探针存在的主要原因之一。

5.4 Exec Probe 的边界

Exec Probe 在容器的执行环境中运行命令。它不是在宿主机 shell 中运行,因此:

  • 命令必须存在于容器镜像中;
  • 路径和环境变量以容器环境为准;
  • 探测命令本身会消耗容器的 CPU、PID 和文件系统资源;
  • 对高频 Probe 使用复杂脚本可能制造额外负载。

Probe 成功只表示探测命令返回成功,不代表所有业务依赖都健康。反过来,Probe 也可能因为探测接口依赖了一个非关键下游服务而过于敏感,导致应用频繁从 Service 中摘除或反复重启。

5.5 Probe 与容器重启的准确因果

以 liveness 失败为例:

探测失败
    ↓
失败计数达到 failureThreshold
    ↓
Kubelet 将容器视为不健康
    ↓
按 Pod 的重启策略停止并重启容器
    ↓
容器 restartCount 增加

如果容器原本已经退出,Kubelet 可能根据退出码和 restartPolicy 重启它;这与 liveness 失败触发的重启是两条不同路径。诊断时应查看:

kubectl get pod probe-demo -o jsonpath='{.status.containerStatuses[0].lastState}'
kubectl get pod probe-demo -o jsonpath='{.status.containerStatuses[0].restartCount}'
kubectl describe pod probe-demo

六、资源状态:Capacity、Allocatable、Conditions 与驱逐

Kubelet 不只负责容器生命周期,还要把节点资源和健康状态报告给 Kubernetes,并在资源不足时执行本地驱逐逻辑。

6.1 Capacity 与 Allocatable

Node 的资源状态至少要区分:

  • Capacity:节点理论上或检测到的总资源;
  • Allocatable:可以分配给普通 Pod 的资源上限。

对某资源 rr,可以粗略表示为:

Allocatabler=CapacityrSystemReservedrKubeReservedrEvictionReservedr\text{Allocatable}_r = \text{Capacity}_r - \text{SystemReserved}_r - \text{KubeReserved}_r - \text{EvictionReserved}_r

实际实现还可能受到 kubelet 配置、操作系统、设备插件、拓扑管理和特定资源模型影响,因此不能把公式视为所有资源的精确计算规范。

查看节点资源:

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

典型输出可能类似:

Capacity:
  cpu:                8
  memory:             32Gi
  pods:               110
Allocatable:
  cpu:                7800m
  memory:             30Gi
  pods:               110

7800m 表示 7.8 个 CPU。Allocatable 小于 Capacity 的原因是节点需要为操作系统、Kubelet、容器运行时和资源驱逐保留空间。

调度器主要依据 Node 的可分配资源和 Pod 的资源请求做调度。调度成功不保证应用一定不会发生运行时资源争用,因为:

  • 资源请求是调度和计量依据,不是进程绝对上限;
  • CPU request 通常影响配额和权重,CPU limit 影响 cgroup 上限;
  • memory limit 超出后可能发生 OOM;
  • 本地磁盘、PID、网络和设备资源也可能成为瓶颈。

6.2 Pod 的 requests、limits 与 QoS

对一个容器:

  • requests 表示调度和资源保证计算中使用的请求量;
  • limits 表示运行时资源上限或控制参数;
  • 未设置 limit 不代表资源无限,也不代表一定获得节点全部剩余资源。

Pod QoS 类别通常为:

  • Guaranteed
  • Burstable
  • BestEffort

例如,所有容器的 CPU 和内存 request、limit 都设置且相等时,Pod 才可能属于 Guaranteed;完全没有 CPU 和内存请求/限制时,才可能属于 BestEffort;其余多数情况属于 Burstable。

QoS 主要影响资源压力下的处理优先级和 cgroup 管理,不是“Guaranteed 永不被杀”的保证。节点出现严重内核 OOM、硬件故障或管理员强制操作时,任何 Pod 都可能受到影响。

6.3 Node Conditions

常见 Node Condition 包括:

  • Ready:节点是否被认为可正常运行 Pod;
  • MemoryPressure:内存压力;
  • DiskPressure:磁盘或 inode 压力;
  • PIDPressure:进程 ID 资源压力;
  • NetworkUnavailable:网络不可用状态,部分场景由网络插件或控制器参与设置。

查看条件:

kubectl get node <node-name> \
  -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" reason="}{.reason}{" message="}{.message}{"\n"}{end}'

条件为 True 时,不能只看条件名称,还要结合 reasonmessage。例如 DiskPressure=True 可能对应:

  • 容器镜像层占满;
  • /var/lib/kubelet 所在文件系统空间不足;
  • inode 耗尽;
  • ephemeral-storage 使用过高;
  • 日志文件增长过快。

6.4 资源统计与驱逐

Kubelet 的资源判断会使用来自 cgroup、文件系统、运行时和系统监控的数据。不同版本和运行时组合下,统计来源可能不同;不要假设所有指标都由同一个组件提供。

当资源压力达到驱逐阈值时,简化过程是:

采集资源统计
    ↓
计算内存、磁盘、inode、PID 等压力
    ↓
设置 Node Condition
    ↓
选择需要驱逐的 Pod
    ↓
终止 Pod 并更新状态

驱逐不是 Probe 失败。它是节点级资源保护机制。

MemoryPressure

内存压力可能导致:

  • Kubelet 报告 MemoryPressure=True
  • 调度行为受到影响;
  • Kubelet 驱逐 Pod;
  • 容器被 Linux OOM Killer 杀死。

容器被 OOM Killer 杀死时,Pod 中可能看到:

Reason:     OOMKilled
Exit Code:  137

但退出码 137 只表示常见的 SIGKILL 结果,不能单凭它证明一定是某一种 OOM 场景。还应检查内核日志、cgroup 事件和节点监控。

DiskPressure

磁盘压力涉及两种不同维度:

  • 字节空间不足;
  • inode 数量不足。

即使 df -h 仍有空间,df -i 显示 inode 已耗尽,也可能触发 DiskPressure。

常用检查:

df -h
df -i
sudo du -xhd1 /var/lib/kubelet 2>/dev/null
sudo du -xhd1 /var/lib/containerd 2>/dev/null

不要直接删除 /var/lib/kubelet 或运行时目录来“释放磁盘”。其中可能包含正在运行 Pod 的挂载、检查点和元数据。应先识别镜像、容器日志、Pod ephemeral-storage 和孤儿挂载的来源,再通过运行时、日志系统或 Pod 生命周期进行清理。

PIDPressure

每个节点可创建的进程数受 PID 上限影响。PIDPressure 可能来自:

  • 容器内进程泄漏;
  • 高频短命进程;
  • 过多 Pod 或 sidecar;
  • 宿主机服务异常创建进程。

检查节点 PID:

ps -eLf | wc -l
cat /proc/sys/kernel/pid_max
kubectl describe node <node-name>

容器的 pids cgroup 限制和节点整体 PID 资源是两个层次。某个容器的 PID limit 触发,不一定意味着 Node 已经 PIDPressure=True;反过来也成立。


七、把 Pod Sync、PLEG、Probe 和资源压力串成故障路径

7.1 应用进程崩溃

应用进程退出
    ↓
运行时记录容器 Exited
    ↓
PLEG 发现退出事件
    ↓
Pod Sync 根据 restartPolicy 判断
    ↓
重新 StartContainer 或 CreateContainer
    ↓
更新 restartCount 和 lastState

如果容器镜像、命令和 Sandbox 仍可复用,Kubelet 可能重新启动容器;如果 Sandbox 或容器配置不可恢复,则可能重建更多对象。

7.2 应用仍在运行但失去服务能力

进程仍存在
    ↓
readinessProbe 失败
    ↓
Pod Ready=False
    ↓
EndpointSlice 不再把端点作为可用后端

此时 PLEG 可能没有任何异常事件,因为从运行时角度看容器仍然 Running。若 livenessProbe 后续也失败,才会进一步触发容器重启。

7.3 运行时不可用

CRI 调用超时或失败
    ↓
Pod Sync 无法获得可靠状态
    ↓
PLEG 查询延迟或失败
    ↓
Pod 状态更新滞后
    ↓
Node Ready 可能受影响

这种情况下,kubectl 中的 Pod 状态不一定能实时反映节点真实情况,因为状态汇报本身也可能依赖 Kubelet 和运行时恢复。

7.4 节点磁盘耗尽

容器日志或镜像持续增长
    ↓
文件系统空间或 inode 下降
    ↓
DiskPressure=True
    ↓
新 Pod 调度、镜像拉取或容器创建失败
    ↓
Kubelet 根据驱逐策略终止部分 Pod

磁盘压力既可能造成“新容器创建失败”,也可能导致“已有容器被驱逐”。两者不能只通过 Pod phase 区分,应查看 Node 事件、Kubelet 日志和文件系统状态。


八、从 API、Kubelet、CRI 三层进行排障

排障时要先确认问题发生在哪一层。

8.1 第一层:API 视角

kubectl get pod <pod> -o wide
kubectl describe pod <pod>
kubectl get pod <pod> -o json
kubectl get events --sort-by=.lastTimestamp

重点看:

  • Pod 是否已经绑定到目标节点;
  • Pod phase 和 conditions;
  • containerStatuses[*].state
  • lastStatereasonexitCode
  • restartCount
  • 事件中的 Failed、BackOff、Unhealthy、Killing;
  • Pod IP 和容器状态是否一致。

例如:

kubectl get pod <pod> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{" state="}{.state}{" last="}{.lastState}{" restarts="}{.restartCount}{"\n"}{end}'

这能快速判断是容器尚未创建、正在运行、反复退出,还是刚刚被 Probe 或资源压力影响。

8.2 第二层:节点与 Kubelet 视角

kubectl describe node <node-name>
sudo systemctl status kubelet
sudo journalctl -u kubelet --since "15 min ago"

重点看:

  • Node Conditions;
  • 磁盘、内存和 PID 压力;
  • PLEG 或 runtime 操作超时;
  • Probe 失败;
  • 驱逐事件;
  • CNI、volume、mount 和 cgroup 错误;
  • Kubelet 是否无法连接 API Server。

如果 Node Ready=False,不要直接推断是容器运行时故障。还需要检查:

  • Kubelet 是否运行;
  • Kubelet 证书和 API 连接;
  • 容器运行时;
  • CNI;
  • 节点时间;
  • 磁盘和内存;
  • 是否存在网络隔离。

8.3 第三层:CRI 与系统视角

sudo crictl info
sudo crictl pods
sudo crictl ps -a
sudo crictl stats
sudo journalctl -u containerd --since "15 min ago"
sudo dmesg -T | egrep -i 'oom|killed process|cgroup|overlay|filesystem'

如果 Kubernetes API 显示容器不存在,但 crictl ps -a 显示它存在,可能是状态传播延迟;如果两者长期不一致,则应重点检查:

  • Kubelet 是否卡住;
  • PLEG 是否持续 relist;
  • runtime endpoint 是否错误;
  • 运行时数据库是否异常;
  • 容器是否由非 Kubernetes 工具创建;
  • 节点上是否存在残留 Sandbox。

crictl stats 是否能提供完整 CPU、内存和磁盘指标,取决于运行时、CRI 版本和节点配置。缺少某些字段不一定表示资源为零。


九、常见误解与反例

误解一:Pod 是一个容器

Pod 是一组共享部分 namespace、网络和生命周期语义的容器。运行时通常先创建 Sandbox,再创建 init container 和应用容器。把 Pod 当成一个容器,会错误理解网络、重启、日志和资源统计。

误解二:容器 Running 就表示应用可用

Running 通常只表示容器主进程仍在运行。应用可能已经无法处理请求,此时应由 readiness probe 反映流量接收能力。

误解三:readiness 失败会重启容器

readiness 的主要作用是控制可用端点,不负责重启。重启通常由容器退出、liveness 失败或其他 Kubelet 生命周期逻辑触发。

误解四:PLEG 发现问题后立即修复

PLEG 只报告运行时变化。修复动作由 Pod Sync 根据 PodSpec、restartPolicy、Sandbox 状态和其他条件决定。

误解五:Node Ready 就代表所有 Pod 正常

Node Ready 是节点级条件,不是每个 Pod 的业务健康汇总。节点可以 Ready,同时某个 Pod 的 readiness 为 False、容器反复重启或服务完全不可用。

误解六:资源 limit 是节点级资源保证

limit 主要通过 cgroup 等机制约束容器;它不保证节点永远有对应资源,也不保证应用一定获得稳定性能。节点级资源不足时,Kubelet、内核 OOM 和运行时都可能介入。


十、规范保证、实现细节和生产取舍

需要区分三类事实。

Kubernetes API 和生命周期语义

以下属于应优先依赖的 API 语义:

  • Pod 是调度和管理单位;
  • Probe 的 startup、readiness、liveness 职责不同;
  • PodStatus、NodeStatus 和 Conditions 通过 API 暴露;
  • restartPolicy 定义容器退出后的基本处理策略;
  • CRI 是 Kubelet 与运行时的接口边界;
  • requests、limits 和 QoS 参与调度与资源管理。

常见实现

以下是常见但不应视为所有版本、所有运行时都完全相同的实现:

  • 使用 pause/infra 容器承载 Pod Sandbox;
  • PLEG 通过周期性 relist 发现运行时变化;
  • Kubelet 使用 cAdvisor、CRI 和 cgroup 获取不同资源统计;
  • Pod worker 按 Pod 串行化部分同步动作;
  • Probe 由 Kubelet 执行,而不是由 Service 或 API Server 执行。

生产环境取舍

生产系统中最危险的做法通常不是 Probe 参数偏小,而是把不同层次的状态混为一谈:

  • 用 Pod phase 代替应用健康状态;
  • 用容器 Running 代替流量可用;
  • 用 API Server 状态代替运行时实时状态;
  • df -h 代替完整磁盘和 inode 检查;
  • 用手工删除运行时目录代替生命周期清理;
  • 用 liveness probe 检查所有下游依赖;
  • 用资源 limit 数值代替节点容量和实际工作集分析。

更可靠的排障顺序是:

确认 Pod 是否已调度
    ↓
确认 Pod 是否有 Sandbox
    ↓
确认容器是否创建、启动或退出
    ↓
区分进程失败、Probe 失败和资源驱逐
    ↓
比较 kubectl、crictl、Kubelet 日志和 Node Conditions
    ↓
最后判断是 API、Kubelet、CRI、CNI、存储还是内核层问题

Kubelet 的核心不是“启动几个容器”,而是持续地把声明式 PodSpec 转换为节点上的实际进程、namespace、cgroup、网络和挂载,并把这些异步变化重新汇总为 API 可见状态。CRI 提供执行边界,Pod Sync 负责调谐,PLEG 负责发现生命周期变化,Probe 负责应用层健康判断,资源状态负责节点级保护。只有把这五者放在同一条状态传播链路上,才能正确解释 Pod 为什么没有启动、为什么反复重启、为什么不接收流量,以及为什么节点会进入压力状态。


系列导航与关联阅读

官方资料

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