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

Kubernetes Pod 完整模型:共享 Namespace、容器、Sandbox 和生命周期

Pod 是 Kubernetes 调度、管理和观测的最小工作负载单元。它通常包含一个或多个容器,这些容器共享一部分运行环境和生命周期,但并不因此变成“一个容器”。

理解 Pod 需要同时回答四个问题:

  1. Pod 在 Kubernetes API 中是什么对象,和容器是什么关系?
  2. Pod 内哪些 Namespace 被共享,哪些资源仍然隔离?
  3. Sandbox、基础设施容器和业务容器如何协同工作?
  4. Pod 从创建、启动、运行到重启、终止或被驱逐时,状态如何变化?

下面以 Kubernetes 当前稳定 API 为基础说明。具体容器运行时、Linux 内核能力、云厂商网络插件和发行版可能改变实现细节,但不会改变 Pod 作为调度与管理边界的基本模型。


一、Pod 的抽象:一组共享上下文的容器

Pod 的 API 对象主要由两部分组成:

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  containers:
    - name: nginx
      image: nginx:1.27
status:
  phase: Running
  podIP: 10.244.1.20

其中:

  • metadata 描述 Pod 的身份、标签、注解和命名空间。
  • spec 是用户期望的状态,例如容器、镜像、资源、卷和重启策略。
  • status 是 Kubernetes 观察到的状态,例如调度结果、Pod IP、容器状态和条件。
  • spec.containers 至少包含一个应用容器。
  • initContainers、原生 sidecar、ephemeral container 是不同生命周期语义的容器类型。

Pod 不是一个进程,也不是一个镜像。它是一个由 kubelet 管理的运行单元:

控制器或用户
    │
    │ 创建/更新 Pod 对象
    ▼
API Server
    │
    │ 调度结果:节点名
    ▼
kubelet
    │
    ├── 创建 Pod Sandbox
    ├── 创建共享网络等运行环境
    ├── 启动 Init 容器
    ├── 启动普通容器和 Sidecar
    └── 汇报 Pod Status

Pod 内的容器具有以下共同点:

  • 通常共享网络 Namespace,因此可以通过 localhost 互相访问。
  • 通常共享 IPC Namespace,因此可以使用同一套 System V IPC 或 POSIX message queue。
  • 通常共享 UTS Namespace,因此具有相同的 hostname。
  • 可以通过 Pod 卷共享文件。
  • 由同一个 kubelet 负责启动、重启、探测和终止。
  • 作为一个整体被调度到同一个节点,不能把 Pod 内的容器分别调度到不同节点。

但它们仍然可能拥有不同的:

  • 根文件系统和容器镜像;
  • Mount Namespace;
  • PID Namespace;
  • 用户身份和 Linux capabilities;
  • CPU、内存、设备及安全策略;
  • 容器级状态与退出码。

因此,“Pod 是容器组”是一个起点,但“共享环境”不等于“所有隔离边界都消失”。


二、Pod 共享哪些 Namespace

2.1 Network Namespace:Pod 网络的核心

默认情况下,一个 Pod 内的容器共享同一个 Network Namespace。这个 Namespace 包含:

  • 网络接口;
  • IP 地址;
  • 路由表;
  • 端口空间;
  • ARP 或邻居表;
  • 网络相关的内核状态。

因此同一个 Pod 中两个容器监听同一个端口会冲突:

容器 A:监听 0.0.0.0:8080
容器 B:监听 0.0.0.0:8080
结果:后启动或后绑定的容器通常因 Address already in use 失败

它们之间访问服务通常使用:

http://127.0.0.1:8080

而不是通过 Pod IP。Pod IP 仍然可以用于从 Pod 外部访问,但容器间使用回环地址更直接。

一个简单的验证 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: shared-network
spec:
  containers:
    - name: web
      image: nginx:1.27
      ports:
        - containerPort: 80
    - name: client
      image: curlimages/curl:8.10.1
      command: ["sh", "-c"]
      args:
        - |
          sleep 3600

创建并查看:

kubectl apply -f shared-network.yaml
kubectl get pod shared-network -o wide

进入 client 容器访问 Nginx:

kubectl exec -it shared-network -c client -- \
  curl -sS http://127.0.0.1

预期会返回 Nginx 的 HTML 页面。这里的因果链是:

  1. kubelet 为 Pod 创建一个共享网络环境;
  2. 两个容器加入该环境;
  3. Nginx 在该网络 Namespace 中监听 80
  4. client 容器的 127.0.0.1 指向同一个 Network Namespace;
  5. 请求到达 Nginx,而不是 client 自己的独立网络栈。

需要注意,hostNetwork: true 会让 Pod 使用节点的网络 Namespace。此时 Pod 不再拥有通常意义上的独立 Pod 网络,端口会直接与节点端口竞争,也可能影响 DNS 行为和网络隔离:

spec:
  hostNetwork: true

这通常只适用于确实需要节点网络的系统组件。它不是提高网络性能的通用开关。

2.2 IPC Namespace:共享进程间通信对象

Pod 内容器默认共享 IPC Namespace,因此可以共享某些 Linux 进程间通信对象,例如:

  • System V shared memory;
  • System V semaphore;
  • POSIX message queue。

这使得一个容器创建的共享内存对象可能被另一个容器访问。但 IPC 共享不代表文件系统共享;要传递普通文件,仍然需要共享卷。

hostIPC: true 会让 Pod 使用节点的 IPC Namespace,隔离边界显著扩大,必须谨慎使用。

2.3 UTS Namespace:hostname 通常一致

UTS Namespace 管理 hostname 和 domain name。Pod 内的容器通常看到相同的 hostname:

kubectl exec shared-network -c client -- hostname
kubectl exec shared-network -c web -- hostname

两条命令通常输出相同值。这个 hostname 通常与 Pod 名称、Pod hostname 设置及运行时实现有关,不应把它当作稳定的服务发现标识。

Pod 的 DNS 名称由 Kubernetes DNS、Service 和相关 DNS 配置共同决定。容器间通信使用 localhost,跨 Pod 通信则应使用 Service DNS,而不是依赖 Pod hostname。

2.4 PID Namespace:默认不共享,可显式开启

默认情况下,Pod 内的容器通常拥有各自的 PID Namespace。也就是说:

  • 容器 A 中的进程通常不会出现在容器 B 的 ps 输出中;
  • 容器 A 中的 PID 1 不等于容器 B 中的 PID 1;
  • 一个容器不能仅凭同 Pod 身份向另一个容器的进程发送信号。

可以通过 shareProcessNamespace: true 显式让 Pod 内容器共享 PID Namespace:

apiVersion: v1
kind: Pod
metadata:
  name: shared-pid
spec:
  shareProcessNamespace: true
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
    - name: inspector
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]

此时 inspector 可以观察 app 容器中的进程。共享 PID Namespace 的代价是进程可见性扩大,调试能力和信息暴露面同时增加。

hostPID: true 则会让 Pod 使用节点 PID Namespace。这样 Pod 可能看到节点上的进程,属于更强的宿主机权限边界,不应与普通的 Pod 级进程共享混为一谈。

2.5 Mount Namespace 和文件系统:通常不共享

容器通常拥有自己的根文件系统和 Mount Namespace。下面的两个容器并不会因为属于同一 Pod 就自动看到对方镜像中的文件:

容器 A 的 /app/config.yaml
容器 B 通常看不到

如果需要共享文件,必须显式声明卷:

apiVersion: v1
kind: Pod
metadata:
  name: shared-volume
spec:
  volumes:
    - name: shared-data
      emptyDir: {}
  containers:
    - name: writer
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          echo "written by writer" > /data/message
          sleep 3600
      volumeMounts:
        - name: shared-data
          mountPath: /data
    - name: reader
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          while true; do cat /data/message 2>/dev/null || true; sleep 5; done
      volumeMounts:
        - name: shared-data
          mountPath: /data

这里共享的是 emptyDir 卷挂载点,不是两个容器的根文件系统。emptyDir 的生命周期与 Pod 绑定:容器重启时数据通常保留,但 Pod 被删除并重新创建后,原来的 emptyDir 不会保留。

2.6 用户 Namespace 与安全边界

容器是否使用独立的 Linux user namespace,取决于 Kubernetes 配置、版本、运行时和 Pod 安全设置。不能简单地把“同一个 Pod”理解为“同一个 Linux 用户空间”。

即使容器共享网络,也可以拥有不同的:

securityContext:
  runAsUser: 1000
  allowPrivilegeEscalation: false

因此以下概念必须分开:

  • Pod 级共享 Namespace;
  • 容器级用户和能力;
  • 节点级 Namespace;
  • Kubernetes RBAC 身份;
  • Linux 文件权限。

Kubernetes ServiceAccount 也不是 Linux 用户。ServiceAccount 控制 API Server 访问身份,不能替代 runAsUser 或文件系统权限。


三、Pod Sandbox:容器之前存在的运行环境

3.1 Sandbox 的含义

Pod Sandbox 是容器运行时为 Pod 建立的共享运行环境。它不是 Kubernetes Pod API 中一个供用户直接编写的普通容器,而是 CRI(Container Runtime Interface)层的重要概念。

典型创建流程如下:

kubelet
  │
  │ RunPodSandbox
  ▼
容器运行时
  │
  ├── 创建 Pod 的网络 Namespace
  ├── 调用 CNI 配置网卡、IP、路由
  ├── 创建或准备其他共享 Namespace
  └── 返回 Sandbox ID
       │
       ├── CreateContainer / StartContainer:Init 容器
       └── CreateContainer / StartContainer:应用容器

在很多 Linux 容器运行时中,Sandbox 由一个很小的“基础设施容器”维持,例如常被称为 pause 容器。它的主要作用不是执行业务,而是持有或维持 Pod 的共享 Namespace,使其他容器可以加入这个环境。

但要区分三层概念:

  1. Pod:Kubernetes API 对象和调度单元;
  2. Pod Sandbox:CRI 运行时管理的 Pod 级运行环境;
  3. pause 或类似基础设施容器:某些运行时用来维持 Sandbox 的实现机制。

Kubernetes API 并不保证每个发行版一定使用名为 pause 的容器,也不保证其镜像、进程或内部实现完全相同。不要依赖 pause 容器的名称、ID 或 PID 编写业务逻辑。

3.2 Sandbox 与业务容器的关系

应用容器加入 Sandbox,而不是各自独立创建一个 Pod 网络。于是:

Pod Sandbox
  ├── 共享 Network Namespace
  ├── 共享 IPC Namespace
  ├── 可能共享 PID Namespace
  ├── app 容器
  ├── sidecar 容器
  └── 其他容器

当某个普通容器退出时,kubelet 通常只重启该容器,而不必重新创建整个 Sandbox。例如应用进程崩溃:

容器进程退出
    │
    ▼
容器状态变为 Terminated
    │
    ▼
kubelet 根据 RestartPolicy 决定是否重启
    │
    └── 重启同一 Pod 中的该容器

但如果 Sandbox 丢失或损坏,影响范围会扩大:

Sandbox 不可用
    │
    ▼
Pod 级网络和共享运行环境失效
    │
    ▼
运行时重建 Sandbox
    │
    ▼
重新创建或启动 Pod 中的容器
    │
    ▼
Pod IP 可能发生变化

因此,“容器重启”和“Pod Sandbox 重建”不是同一类故障。前者通常只影响一个容器;后者可能影响整个 Pod,并导致网络连接中断。

3.3 Sandbox 创建失败的诊断

当 Sandbox 创建失败时,业务容器可能根本没有启动。常见原因包括:

  • CNI 插件无法分配 IP;
  • 节点网络配置错误;
  • IP 地址池耗尽;
  • 容器运行时异常;
  • 节点磁盘、进程或内核资源不足;
  • Pod 使用了节点不支持的 Namespace 或安全能力。

诊断顺序可以是:

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

重点观察 Events 中的:

FailedCreatePodSandbox
Failed to create pod sandbox
networkPlugin ...

如果没有任何业务容器日志,不代表应用没有问题;可能是 Sandbox 尚未创建成功,应用进程尚未获得启动机会。


四、Pod 的创建和启动时序

一个普通 Pod 的典型启动过程如下:

sequenceDiagram
    participant U as 用户/控制器
    participant A as API Server
    participant S as Scheduler
    participant K as kubelet
    participant R as CRI Runtime
    participant C as CNI
    participant I as Init 容器
    participant W as 普通容器

    U->>A: 创建 Pod Spec
    A->>S: 发现未调度 Pod
    S->>A: 写入 nodeName
    A->>K: kubelet watch 到 Pod
    K->>R: RunPodSandbox
    R->>C: 配置网络
    C-->>R: 返回接口/IP
    R-->>K: Sandbox Ready
    K->>R: 创建并启动 Init 容器
    R->>I: 执行初始化任务
    I-->>K: 成功退出
    K->>R: 创建并启动普通容器
    R->>W: 启动业务进程
    K->>A: 更新 Pod Status

实际系统会并发拉取镜像、创建容器和执行探针,但生命周期约束仍然成立:

  1. Pod 必须先被调度到节点;
  2. Sandbox 必须可用;
  3. Init 容器必须按顺序成功完成;
  4. 普通容器才会正常启动;
  5. kubelet 持续执行探针、收集状态并处理重启。

镜像拉取可能发生在不同阶段,具体并发细节由 kubelet 和运行时实现决定。不要把“镜像已下载”当作“容器已经开始执行”。


五、Init 容器、普通容器和 Sidecar

5.1 Init 容器:启动前置条件

Init 容器在应用容器之前运行,并且必须按声明顺序逐个成功完成:

apiVersion: v1
kind: Pod
metadata:
  name: init-example
spec:
  initContainers:
    - name: prepare
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          echo "prepare data"
          touch /work/ready
      volumeMounts:
        - name: work
          mountPath: /work
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          test -f /work/ready
          echo "application started"
          sleep 3600
      volumeMounts:
        - name: work
          mountPath: /work
  volumes:
    - name: work
      emptyDir: {}

启动逻辑是:

prepare 未启动       → app 不启动
prepare 运行中       → app 不启动
prepare 退出码为 0   → 允许 app 启动
prepare 退出码非 0   → 根据 Pod 重启策略重新执行 prepare

Init 容器适合做一次性准备,例如:

  • 生成配置;
  • 检查依赖是否满足;
  • 初始化共享卷;
  • 执行数据库 Schema 准备;
  • 下载启动所需的静态文件。

Init 容器和普通容器都使用 Pod 的共享网络和显式共享卷,因此 Init 容器可以访问同一 Pod 网络,也可以向卷写入普通容器读取的文件。

查询 Init 状态:

kubectl get pod init-example \
  -o jsonpath='{range .status.initContainerStatuses[*]}{.name}{" => "}{.state}{"\n"}{end}'

查看 Init 日志:

kubectl logs init-example -c prepare

常见误解是把“启动一次”理解成“只执行一次”。Init 容器是每次该 Pod 实例启动时都要执行的前置阶段;如果 Pod 被删除后由控制器重新创建,新的 Pod 会再次执行 Init 容器。

5.2 普通容器:Pod 的长期运行部分

spec.containers 中的普通容器是 Pod 的主要运行容器。它们可以并发启动,没有一个普通容器天然先于另一个普通容器完成初始化。

如果两个容器存在强启动依赖,例如:

app 必须等 proxy 完成某项初始化

不能仅通过声明顺序表达。可以使用:

  • Init 容器完成一次性准备;
  • 应用自身重试;
  • readinessProbe 表示“已经可以接收流量”;
  • 原生 sidecar 语义;
  • 明确的进程间协议。

readinessProbe 不会阻止容器启动,而是影响该 Pod 是否进入可服务状态。启动阻塞和流量就绪是两个不同问题。

5.3 原生 Sidecar:由 Init 容器语义表达长期伴随进程

Kubernetes 支持使用带有 restartPolicy: Always 的 Init 容器表达原生 sidecar:

apiVersion: v1
kind: Pod
metadata:
  name: native-sidecar
spec:
  initContainers:
    - name: log-agent
      image: busybox:1.36
      restartPolicy: Always
      command: ["sh", "-c"]
      args:
        - |
          while true; do
            echo "sidecar is running"
            sleep 5
          done
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c"]
      args:
        - |
          echo "app started"
          sleep 20

它与普通 Init 容器的关键区别是:

  • 仍位于 initContainers 中,因此会先于普通容器启动;
  • restartPolicy: Always 表示它是持续运行的 sidecar;
  • 它不会因为没有退出而阻塞普通容器启动;
  • kubelet 会持续管理它;
  • Pod 终止时,sidecar 的终止顺序与普通应用容器不同。

原生 sidecar 能力已经在较新的 Kubernetes 版本中稳定化,但集群必须满足对应版本和特性支持要求。对于版本较旧的集群,直接使用该字段可能被拒绝或表现不同,应先确认集群版本和 API 行为。

传统 sidecar 通常写在 spec.containers 中。这种方式也能工作,但生命周期语义较弱:如果日志代理永不退出,某些 Job 的完成判断、Pod 终止和容器顺序处理会更复杂。原生 sidecar 的价值在于把“先启动、持续运行、最后关闭”的意图交给 kubelet 管理。

5.4 Ephemeral Container:调试边界

Ephemeral Container 用于向已经运行的 Pod 临时注入调试容器。它不是正常的应用容器,也不是 Init 容器:

kubectl debug -it <pod-name> \
  --image=busybox:1.36 \
  --target=<existing-container> \
  -- sh

典型用途:

  • 原应用镜像没有 shell;
  • 需要使用 psnsenter 或网络诊断工具;
  • 需要观察目标容器的进程或 Namespace;
  • 不希望修改原 Pod 的声明式配置。

Ephemeral Container 的边界包括:

  • 不会自动重启;
  • 不能作为应用正常生命周期的一部分;
  • 不能依赖它完成启动前初始化;
  • 添加后不能像普通容器一样任意修改其全部字段;
  • 是否能观察目标容器进程,取决于 PID Namespace 是否共享以及 --target 和运行时支持;
  • 需要相应 RBAC 权限,且可能受到 Pod Security 或节点安全策略限制。

查询:

kubectl get pod <pod-name> -o jsonpath='{.spec.ephemeralContainers[*].name}'

调试容器能看到什么,不由“属于同一个 Pod”单独决定,而由这些条件共同决定:

可见进程
= Pod 是否 shareProcessNamespace
  + 运行时 target 支持
  + 容器权限
  + 节点和内核安全策略

六、容器状态和 Pod Phase 不是一回事

6.1 容器状态

每个容器都有一个主要状态:

  • Waiting:尚未运行,例如拉取镜像、等待 Init 容器、退避重启;
  • Running:容器进程正在运行;
  • Terminated:容器进程已经结束,包含退出码、原因和时间。

查看容器状态:

kubectl get pod <pod-name> -o json \
  | jq '.status.containerStatuses, .status.initContainerStatuses'

示例:

{
  "name": "app",
  "ready": false,
  "restartCount": 3,
  "state": {
    "waiting": {
      "reason": "CrashLoopBackOff",
      "message": "back-off 40s restarting failed container"
    }
  },
  "lastState": {
    "terminated": {
      "exitCode": 1,
      "reason": "Error"
    }
  }
}

CrashLoopBackOff 不是独立的生命周期状态,而是 kubelet 对反复失败容器执行重启退避时显示的等待原因。它表达的是“现在暂缓重启”,不是“应用一定因为某个固定原因崩溃”。

诊断时应同时检查:

kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <container-name>
kubectl logs <pod-name> -c <container-name> --previous

--previous 很重要,因为当前容器可能已经重启,当前日志不一定包含上一个失败实例的输出。

6.2 Pod Phase

Pod 的 status.phase 是较粗粒度的状态,合法值包括:

Phase 含义
Pending Pod 已被 API Server 接受,但尚未完成运行准备,例如等待调度、镜像或 Init 容器
Running Pod 已绑定节点,且至少有一个容器运行或正在启动
Succeeded Pod 中所有容器都成功终止,且不会再重启
Failed 所有容器都已终止,至少一个容器失败,且不会再重启
Unknown API Server 无法获得 Pod 状态,常见于节点通信问题

Phase 不是健康检查结果,也不是容器状态的简单映射。例如:

  • Pod 可以是 Running,但 ReadyFalse
  • Pod 可以是 Running,但其中一个容器处于重启退避;
  • Pod 可以因为 Readiness 失败而不接收流量,但仍然保持运行;
  • Pod 被删除时,API 对象可能仍存在,但其终止过程由 deletionTimestamp 和各容器状态体现。

因此不要使用类似下面的判断代替真正的就绪判断:

phase == Running  ⇒ 可以接收流量

正确关系更接近:

可接收 Service 流量
≈ PodReady 条件为 True
  且 Service/EndpointSlice 已纳入该端点

两者还存在控制器和网络传播延迟。

6.3 Pod Conditions

常见 Pod Conditions 包括:

  • PodScheduled:是否已经成功调度;
  • Initialized:Init 容器是否完成;
  • ContainersReady:所有应用容器是否就绪;
  • Ready:Pod 是否满足对外服务的就绪条件;
  • PodReadyToStartContainers:较新版本中用于表示 Pod Sandbox 和网络等启动前环境是否准备好。

查看条件:

kubectl get pod <pod-name> -o json \
  | jq '.status.conditions'

Conditions 具有 typestatusreasonmessagelastTransitionTime 等字段。排障时应结合这些字段判断“阻塞发生在哪一阶段”,而不是只看 phase。


七、RestartPolicy:重启谁、何时重启

Pod 的 restartPolicy 有三个合法值:

spec:
  restartPolicy: Always
  • Always:容器结束后总是尝试重启;
  • OnFailure:只有退出码非零时重启;
  • Never:不重启。

默认值是 Always

该策略主要由 kubelet在同一 Pod 内管理容器时使用。它不表示:

容器失败 ⇒ Kubernetes 一定创建一个新 Pod

更准确的模型是:

同一个 Pod 对象
  ├── 容器进程退出
  ├── kubelet 根据 RestartPolicy 判断
  ├── 可能重启同一容器
  └── restartCount 增加

只有控制器,例如 Deployment、Job 或 StatefulSet,才会根据各自语义创建或替换 Pod。

7.1 RestartPolicy 与 Job

对于 Job,常见配置是:

apiVersion: batch/v1
kind: Job
metadata:
  name: one-shot
spec:
  backoffLimit: 3
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: task
          image: busybox:1.36
          command: ["sh", "-c", "echo task; exit 1"]

这里有两层失败处理:

  1. Pod 内的 restartPolicy: Never:容器失败后,当前 Pod 不在节点内重启该容器;
  2. Job 控制器观察 Pod 失败:根据 backoffLimit 决定是否创建新的 Pod。

如果使用 OnFailure,Job 可能让同一个 Pod 内的容器先重试;如果最终 Pod 失败,Job 控制器再根据策略处理。Pod 重启和 Pod 替换是两个不同层次的行为。

7.2 RestartPolicy 与 Init 容器

Init 容器失败时,kubelet 会根据 Pod 的重启语义重新执行它,直到成功或 Pod 进入失败路径。由于普通容器尚未启动,应用容器的失败日志不会解释 Init 阶段的问题。

查看 Init 容器是否反复失败:

kubectl describe pod <pod-name>
kubectl logs <pod-name> -c <init-container-name>

一个 Init 容器不断失败时,Pod 常见地停留在 Pending,同时 Initialized 条件为 False


八、探针、就绪和重启之间的因果关系

探针不是容器生命周期本身,但它会影响 Pod 的可用性和重启行为。

8.1 Readiness Probe

Readiness 失败:

容器仍运行
  ├── Pod Ready 变为 False
  └── Service 通常停止把新流量发给该 Pod

它不会直接重启容器。适用于“进程还活着,但暂时不能服务”的情况,例如:

  • 正在加载配置;
  • 依赖服务不可用;
  • 连接池尚未建立;
  • 过载保护触发。

8.2 Liveness Probe

Liveness 失败达到阈值后,kubelet 会杀死容器,随后根据 restartPolicy 处理:

Liveness 连续失败
    │
    ▼
kubelet 终止容器
    │
    ▼
容器状态 Terminated
    │
    ▼
根据 RestartPolicy 重新启动或结束

如果应用启动很慢,却只配置了过早的 liveness 探针,可能出现:

应用尚未完成启动
  → liveness 失败
  → kubelet 杀死应用
  → 应用再次启动
  → 再次被杀死
  → CrashLoopBackOff

startupProbe 可以把“启动期”和“运行期”分开:startup 成功前,liveness 和 readiness 通常不会按正常运行阶段生效。


九、Pod 的终止流程

删除 Pod 时,Kubernetes 通常执行优雅终止,而不是立即杀死主进程:

sequenceDiagram
    participant U as 用户/控制器
    participant A as API Server
    participant K as kubelet
    participant C as 容器
    participant E as EndpointSlice/Service

    U->>A: DELETE Pod
    A-->>K: Pod 带 deletionTimestamp
    A->>E: 端点开始移除/标记不可用
    K->>C: 执行 preStop(若配置)
    K->>C: 发送 SIGTERM
    Note over K,C: 等待 terminationGracePeriodSeconds
    K->>C: 超时后发送 SIGKILL
    K->>A: 更新容器和 Pod 状态
    A-->>U: Pod 对象最终消失

默认终止宽限期通常为 30 秒,但应以 Pod 的 terminationGracePeriodSeconds 为准:

spec:
  terminationGracePeriodSeconds: 60

容器终止通常包括:

  1. kubelet 发现 Pod 被删除或需要终止;
  2. 执行 preStop 生命周期钩子(如果配置);
  3. 向容器主进程发送 SIGTERM
  4. 等待宽限期;
  5. 超时后发送 SIGKILL
  6. 清理容器和 Sandbox;
  7. 更新 Pod 状态并最终删除对象。

应用必须真正处理 SIGTERM。如果容器中的 PID 1 是一个不会转发信号的 shell,应用可能收不到优雅退出通知。可以使用正确的进程启动方式,例如:

ENTRYPOINT ["/usr/local/bin/server"]

而不是让 shell 长期充当不转发信号的中间进程。

preStop 不是额外的宽限期。它消耗同一个终止宽限窗口;如果 preStop 已经等待很久,应用剩余的优雅退出时间会减少。

9.1 终止时的流量与并发请求

删除 Pod 时,Service 端点更新、负载均衡器传播和客户端连接关闭并不一定瞬时完成。应用的优雅退出逻辑应包括:

  • 停止接受新请求;
  • 等待正在处理的请求结束;
  • 关闭连接和文件;
  • 在宽限期内退出。

只调用 sleep 不能保证正确排空连接,因为它没有表达“当前请求何时完成”。

9.2 强制删除的风险

kubectl delete pod <pod-name> --grace-period=0 --force

强制删除会跳过正常的对象确认和部分优雅终止流程。它适合处理节点失联、对象长期卡住等特殊情况,但风险包括:

  • 进程仍可能在失联节点上运行;
  • 有状态应用可能出现双实例或数据冲突;
  • 未完成的写入可能丢失;
  • Service 端点和实际进程状态在短时间内不一致。

强制删除不是修复应用崩溃的常规手段。


十、Eviction:节点压力下的 Pod 驱逐

Eviction 是 Kubernetes 在资源压力下主动终止 Pod 的机制,常见触发源包括:

  • 节点内存压力;
  • 节点磁盘压力;
  • 临时存储不足;
  • PID 数量压力;
  • 节点失去可用状态。

节点上的 kubelet 会根据压力信号和驱逐策略选择 Pod,并尝试进行终止。被驱逐的 Pod 常见状态可以通过以下命令观察:

kubectl get pod <pod-name> -o json \
  | jq '{phase: .status.phase, reason: .status.reason, message: .status.message}'

可能看到类似:

{
  "phase": "Failed",
  "reason": "Evicted",
  "message": "The node was low on resource: memory."
}

驱逐与容器崩溃不同:

容器崩溃:
  kubelet 可能在原 Pod 内重启容器

Pod 被驱逐:
  当前 Pod 实例被终止
  控制器可能在其他节点创建替代 Pod

控制器是否创建替代 Pod,取决于工作负载类型和控制器状态。例如 Deployment 通常会维持副本数;裸 Pod 没有控制器负责补建。

10.1 资源请求、限制和驱逐选择

Pod 的资源声明影响调度和节点压力下的优先级。以内存为例:

  • requests.memory 主要用于调度;
  • limits.memory 限制容器可使用的内存上限;
  • 节点压力下,QoS、请求与实际使用的关系会影响驱逐选择。

但设置了 requestslimits 并不表示 Pod 不会被驱逐。节点故障、磁盘压力、系统进程消耗和集群级策略都可能导致 Pod 终止。

10.2 PodDisruptionBudget 的边界

PodDisruptionBudget 主要约束自愿中断,例如节点排空操作,不是节点资源压力下的绝对保护。以下情况仍可能突破它的保护能力:

  • 节点硬件或操作系统故障;
  • kubelet 资源驱逐;
  • 强制删除;
  • 进程自身崩溃;
  • 集群控制面或网络故障。

因此 PDB 不能替代副本、持久化、跨节点分布和应用恢复能力。


十一、一个完整的状态推导示例

考虑下面的 Pod:

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  restartPolicy: OnFailure
  initContainers:
    - name: init
      image: busybox:1.36
      command: ["sh", "-c", "echo initialized"]
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "echo running; exit 1"]

状态变化可以逐步推导:

第一步:对象创建

API Server 接受对象,但 Pod 尚未调度:

phase = Pending
PodScheduled = False 或尚未出现

第二步:完成调度和 Sandbox

kubelet 在目标节点创建 Sandbox 并配置网络:

PodScheduled = True
PodReadyToStartContainers = True(如果该条件由当前版本报告)
phase 仍可能是 Pending

此时不能仅凭 PodScheduled=True 认为应用已启动,因为 Init 容器尚未完成。

第三步:执行 Init 容器

Init 容器输出 initialized 并以退出码 0 结束:

Initialized = True

随后 kubelet 才会启动 app

第四步:应用容器失败

app 输出 running 后退出码为 1。由于:

restartPolicy: OnFailure

kubelet 会重启它,而不是立即结束 Pod:

app.state = Waiting
app.restartCount = 1
phase 通常仍为 Running 或处于过渡状态

如果它反复失败,可能看到:

state.waiting.reason = CrashLoopBackOff

第五步:为什么不应只看 phase

此时 Pod 可能仍显示:

Running

但应用根本不能提供服务。必须结合:

kubectl get pod lifecycle-demo
kubectl describe pod lifecycle-demo
kubectl logs lifecycle-demo -c app --previous

才能确认:

  • Init 是否完成;
  • 容器退出码是什么;
  • kubelet 是否正在退避;
  • Pod 是否 Ready;
  • 是否存在探针、镜像或 Sandbox 错误。

十二、控制器如何改变 Pod 的生命周期

Pod 是相对短生命周期的对象。生产应用通常由控制器创建,而不是直接创建裸 Pod:

控制器 Pod 生命周期特点
Deployment 维护无状态 Pod 副本,并执行滚动更新
ReplicaSet 维护指定数量的 Pod
StatefulSet 提供稳定序号、网络身份和有序管理
DaemonSet 通常在符合条件的节点上运行一个 Pod
Job 运行直到任务成功或失败
CronJob 按时间表创建 Job

容器重启通常不会改变 Pod UID:

同一个 Pod
  └── 容器重启,restartCount 增加

控制器重新创建 Pod 则会产生新的 Pod UID:

旧 Pod 被删除
  └── 新 Pod 被创建
      ├── 新 UID
      ├── 可能新的 Pod IP
      └── 新的容器实例

这解释了为什么不能把 Pod IP 当作稳定身份。需要稳定访问地址时,应使用 Service;需要稳定有序身份时,应考虑 StatefulSet。


十三、常用诊断路径

13.1 Pod 长时间 Pending

kubectl describe pod <pod-name>

重点区分:

未调度
  → 查看 Events、nodeSelector、affinity、taint、资源请求

已调度但 Sandbox 失败
  → 查看 FailedCreatePodSandbox、CNI、运行时和节点日志

Init 未完成
  → 查看 initContainerStatuses 和 Init 容器日志

镜像问题
  → 查看 ImagePullBackOff、镜像仓库认证和节点网络

Pending 不是一个单一故障原因,只表示 Pod 尚未进入稳定运行状态。

13.2 Pod Running 但没有流量

依次检查:

kubectl get pod <pod-name> -o wide
kubectl get pod <pod-name> -o json | jq '.status.conditions'
kubectl describe pod <pod-name>
kubectl get endpointslice -l kubernetes.io/service-name=<service-name> -o yaml

常见因果链是:

容器运行
  → readinessProbe 失败
  → Pod Ready=False
  → EndpointSlice 不纳入该地址
  → Service 不发送新流量

也可能是 Service selector、端口名称、targetPort 或网络策略错误,不能只检查容器进程。

13.3 CrashLoopBackOff

kubectl logs <pod-name> -c <container-name> --previous
kubectl describe pod <pod-name>
kubectl get pod <pod-name> -o json \
  | jq '.status.containerStatuses[] | {
      name,
      restartCount,
      state,
      lastState
    }'

按顺序确认:

  1. 上一次进程的退出码;
  2. reasonOOMKilledError 还是探针杀死;
  3. 是否使用了错误的命令或参数;
  4. 是否依赖了尚未就绪的服务;
  5. 是否因权限、挂载或配置错误启动失败;
  6. 是否是 livenessProbe 过早触发。

13.4 Pod 被驱逐

kubectl get pod <pod-name> -o yaml
kubectl describe node <node-name>
kubectl get events --field-selector involvedObject.name=<pod-name>

需要同时查看:

  • 节点 MemoryPressure、DiskPressure、PIDPressure;
  • Pod 的资源 requests 和 limits;
  • 容器实际内存与临时存储使用;
  • 节点上是否存在异常日志、镜像或临时文件增长;
  • 控制器是否会创建替代 Pod。

只删除被驱逐的 Pod 不会解决节点压力;如果根因仍在,替代 Pod 可能继续被驱逐。


十四、容易混淆的边界

14.1 Pod 不是虚拟机

Pod 不提供完整的硬件虚拟化边界。容器通常共享节点内核,某些 Pod 配置甚至可以共享节点的 Network、PID 或 IPC Namespace。安全边界需要由容器运行时、Linux 安全机制、Pod Security、节点隔离和云平台策略共同构成。

14.2 Pod 内共享网络不等于共享端口配置

容器共享端口空间,所以端口必须由应用设计协调。containerPort 主要是声明性元数据,不会自动为进程打开端口,也不会解决两个容器的端口冲突。

14.3 Pod 内多个容器不等于微服务部署单元

多个容器适合强耦合、共同生命周期和低延迟协作的场景,例如:

应用 + 本地代理
应用 + 日志收集器
应用 + 配置或证书刷新器

如果两个组件需要独立扩缩容、独立发布或独立故障域,把它们放在同一个 Pod 会使这些能力变差,因为它们共享调度、网络故障域和终止边界。

14.4 Readiness 不会修复崩溃

Readiness 只描述能否接收流量;它不会让退出的进程重新运行。重启由容器退出、liveness 失败、运行时错误和 RestartPolicy 等路径触发。

14.5 Pod 删除和容器退出不是同一件事

容器可以在 Pod 仍存在时多次退出并重启。Pod 删除则是对整个 Pod 生命周期的操作,通常会终止其中所有容器并清理 Sandbox。

14.6 Sandbox 不是业务容器

Sandbox 的实现可能包含基础设施容器,但它不是应用容器,不应写入业务配置、依赖其名称或试图直接管理它。排查 Sandbox 问题应从 kubelet、CRI、CNI 和节点日志入手,而不是只看应用容器日志。


十五、模型总结:用三个层次理解 Pod

可以把 Pod 的运行状态分成三个层次:

第一层:API 对象
  Pod Spec、Pod Status、Phase、Conditions

第二层:Pod 级运行环境
  Sandbox、网络 Namespace、IPC、UTS、卷挂载关系

第三层:容器级生命周期
  Waiting、Running、Terminated、探针、退出码、重启退避

它们的关系不是一一映射:

一个 Pod
  ├── 一个 API 对象
  ├── 通常一个 Sandbox
  ├── 一个 Pod IP
  ├── 多个容器状态
  └── 一个粗粒度 Phase 与多个 Conditions

因此排查问题时应先问故障属于哪一层:

  • Pending 且没有节点:调度层;
  • FailedCreatePodSandbox:Pod 运行环境层;
  • CrashLoopBackOff:容器生命周期层;
  • RunningReady=False:就绪和流量层;
  • Evicted:节点资源和驱逐层;
  • 删除卡住:终止流程、Finalizer、节点连通性或运行时层。

Pod 的完整模型不是“一个 IP 加几个容器”,而是:

Pod
= 调度单元
+ 共享的部分 Linux 运行环境
+ 一个由 CRI 管理的 Sandbox
+ 多种不同语义的容器
+ kubelet 驱动的容器级生命周期
+ API Server 中的状态观测
+ 控制器对 Pod 实例的持续调谐

掌握这些边界后,才能正确解释 Pod 内的 localhost、Init 顺序、Sidecar 终止、Ephemeral Container 调试、容器重启、Pod Phase、优雅终止和节点驱逐,而不会把某一层的现象误认为另一层的机制。


系列导航与关联阅读

官方资料

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