一个 kubectl apply -f pod.yaml 之后,终端里出现了 pod/my-app created。这行输出只意味着 API Server 接受了资源定义并写入 etcd——Pod 还没有运行在任何节点上,甚至还没有被任何节点知晓。从这个瞬间到容器真正响应流量,Pod 经历了一条由多个组件接力完成的状态转换链。

这条链不是黑盒。Kubernetes 把每个阶段都编码进 status.phasestatus.conditionsstatus.containerStatuses 三个字段里,并通过 Events 记录关键节点的时间戳。理解这条链,就能在 Pod 卡住时精确定位是调度失败、镜像拉取失败、探针失败,还是 OOM Kill。

本文的核心问题是:一个 Pod 从 kubectl applyRunning 经历了哪些状态转换,每个状态背后是哪个组件在操作?

状态转换全景图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
kubectl apply


etcd (Pod phase=Pending, no nodeName)
│ API Server 接受写入,Scheduler watch 到新 Pod

Scheduler → binds Pod to Node (更新 Pod.spec.nodeName)
│ kubelet watch 到自己节点上有新 Pod

kubelet
├── 拉取镜像(CRI ImageService)
├── 运行 init containers(顺序,前一个成功才运行下一个)
└── 启动 containers(并行)

├── livenessProbe ──► 失败 → 重启容器
├── readinessProbe ──► 失败 → 从 Endpoints 摘除
└── startupProbe ──► 失败 → 阻塞 liveness/readiness 探针


Pod phase=Running, condition Ready=true

│ (kubectl delete / 滚动更新触发删除)

preStop hook 执行 → SIGTERM → gracePeriodSeconds → SIGKILL
Pod phase=Terminating → Succeeded / Failed(取决于 restartPolicy)

phase 字段与 conditions 字段的区别

Pod 的 status.phase 是一个高层摘要,只有五个取值:

  • Pending:Pod 已被 API Server 接受,但还没有所有容器都处于运行状态。包含等待调度和等待镜像拉取两种情况。
  • Running:至少一个容器正在运行,或者正在启动/重启过程中。
  • Succeeded:所有容器都以状态码 0 退出,且 restartPolicy 不为 Always
  • Failed:至少一个容器以非 0 状态退出或被系统终止,且 restartPolicy 不为 Always
  • Unknown:通常意味着 API Server 无法与 kubelet 通信,节点可能宕机。

phase=Running 不代表应用已经可以处理请求。这是最常见的误解之一。一个刚刚拉起容器但 readinessProbe 还没通过的 Pod,phase 就是 Running,但它不会出现在任何 Service 的 Endpoints 里。

status.conditions 提供更细粒度的信息,四个关键 condition:

Condition 设置者 含义
PodScheduled Scheduler Pod 已绑定到节点
Initialized kubelet 所有 init containers 成功完成
ContainersReady kubelet 所有容器通过了 readinessProbe
Ready kubelet Pod 可以接受流量(ContainersReady + readinessGates)

kubectl get pod my-app -o jsonpath='{.status.conditions}' 可以看到每个 condition 的 statusreasonlastTransitionTime,这是诊断 Pod 卡在哪个阶段最直接的方式。

Pending 阶段:调度器的工作

Pod 进入 etcd 时,spec.nodeName 为空,status.phasePending,condition PodScheduled=False。Scheduler 通过 Informer 机制 watch 到这个新 Pod,将其放入优先级队列。

调度过程(Filter → Score → Bind)在05 调度器深入中详细展开。这里关注调度完成后发生了什么:Scheduler 向 API Server 发送一个 Binding 对象(或者直接 PATCH pod.spec.nodeName),API Server 更新 etcd,condition PodScheduled=True

此时 Pod 仍然是 Pending。kubelet 还没有开始任何操作。

kubelet 接管:从镜像拉取到容器启动

kubelet 通过 Informer 监听 API Server,发现有 spec.nodeName 等于本节点名称的 Pod 进入,触发 syncPod 主循环。

镜像拉取

kubelet 通过 CRI(Container Runtime Interface)的 ImageService.PullImage gRPC 接口向容器运行时(如 containerd)发送拉取请求。拉取期间 Pod 的 status.containerStatuses[*].statewaiting,reason 为 ContainerCreating。如果镜像不存在于任何可达仓库,reason 会变成 ErrImagePull,随后变成 ImagePullBackOff(kubelet 引入指数退避)。

imagePullPolicy 控制行为:

  • Always:每次都拉取(无论本地是否有)
  • IfNotPresent:本地有则跳过拉取
  • Never:从不拉取,镜像必须预先存在于节点

Init Containers

Init containers 按数组顺序串行执行。第 N 个 init container 必须以状态码 0 退出,第 N+1 个才会启动。如果某个 init container 失败,kubelet 根据 restartPolicy 决定是否重试:

  • restartPolicy: AlwaysOnFailure:kubelet 重启失败的 init container,有指数退避(最长 5 分钟),在 status.initContainerStatuses 里能看到 restartCount
  • restartPolicy: Never:init container 失败后 Pod 进入 Failed 状态。

condition Initialized=True 在所有 init containers 成功完成后由 kubelet 设置。

Sandbox 与主容器启动

kubelet 先通过 CRI RuntimeService.RunPodSandbox 创建 pause 容器(也叫 infra container)。pause 容器建立 Pod 的网络命名空间,CNI 插件在这一步被调用,为 Pod 分配 IP。

之后所有主容器并行启动(RuntimeService.CreateContainer + StartContainer),共享 pause 容器的网络和 IPC 命名空间。

三类探针的精确语义

探针通过三种机制执行检查:exec(在容器内执行命令)、httpGet(对容器 IP 发 HTTP GET)、tcpSocket(建立 TCP 连接)。三类探针控制不同的行为:

startupProbe

startupProbe 在容器启动后开始探测,直到连续成功 successThreshold 次。在此期间,livenessProbereadinessProbe 都被挂起,不会执行。

作用:给启动慢的应用(如 JVM 应用)提供足够的启动时间,而不必将 livenessProbe.initialDelaySeconds 设置得很大。

失败结果:超过 failureThreshold * periodSeconds 后,kubelet 杀死容器并根据 restartPolicy 决定是否重启。

livenessProbe

startupProbe 成功(或未配置)后,livenessProbe 开始定期探测。失败达到 failureThreshold 次时,kubelet 向容器发送 SIGKILL(不经过 gracePeriod),然后根据 restartPolicy 重启容器。

livenessProbe 回答的问题是:容器是否还活着、能否继续服务?如果进程死锁但没有退出,操作系统不会自动重启它,livenessProbe 可以检测到这种状态。

readinessProbe

readinessProbe 失败时,kubelet 将该 Pod 从 Service 对应的 Endpoints 中摘除——Pod 不再接收通过 Service 路由的流量。探针恢复后,Pod 重新加入 Endpoints。整个过程中,Pod 保持 Running 状态,容器不会被重启。

readinessProbe 回答的问题是:容器是否准备好接受流量?这与容器是否活着是两个不同的问题。数据库连接池初始化期间,容器可以是活的但没有就绪。

condition ContainersReadyReady 的变化由 readinessProbe 的结果驱动。

Pod 终止:优雅关闭路径

kubectl delete pod my-app 触发以下序列,在 terminationGracePeriodSeconds(默认 30 秒)内完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kubectl delete pod


API Server 设置 Pod.metadata.deletionTimestamp,phase 变 Terminating

▼ (并行执行)
kubelet kube-proxy / Endpoints controller
├── 执行 preStop hook └── 将 Pod 从 Endpoints 摘除
│ (exec 或 httpGet,同步等待) (流量停止路由到该 Pod)
└── 向容器发送 SIGTERM

▼ (等待 terminationGracePeriodSeconds)
如果容器仍在运行 → 发送 SIGKILL


kubelet 通过 CRI 删除容器,向 API Server 报告
API Server 从 etcd 删除 Pod 对象

preStop hook 的执行时间计入 terminationGracePeriodSeconds。如果 preStop 本身需要较长时间,需要相应调大 terminationGracePeriodSeconds

一个常见的误解是认为删除 Pod 后容器会立即消失。实际上,Endpoints controller 和 kube-proxy 的更新存在延迟,如果没有 preStop hook 加一个短暂的 sleep,正在处理中的请求可能在容器收到 SIGTERM 时就被中断。标准做法是:

1
2
3
4
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 5"]

这给 kube-proxy 更新 iptables 规则留出时间,在容器开始处理 SIGTERM 之前,新的连接就不会再路由过来。

可观测实验:用 kubectl 观察状态转换

准备 kind 集群

1
kind create cluster --name lifecycle-demo

观察 Pod 启动过程

创建一个带 init container 和 startupProbe 的 Pod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: lifecycle-demo
spec:
initContainers:
- name: init-db
image: busybox:1.35
command: ["sh", "-c", "echo 'init running'; sleep 3; echo 'init done'"]
containers:
- name: app
image: nginx:1.25
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 2
periodSeconds: 2
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 10
periodSeconds: 2
EOF

在另一个终端窗口运行:

1
kubectl get pod lifecycle-demo -w

输出序列大致如下:

1
2
3
4
5
6
7
8
NAME             READY   STATUS     RESTARTS   AGE
lifecycle-demo 0/1 Pending 0 0s
lifecycle-demo 0/1 Pending 0 0s
lifecycle-demo 0/1 Init:0/1 0 0s
lifecycle-demo 0/1 Init:0/1 0 4s
lifecycle-demo 0/1 PodInitializing 0 7s
lifecycle-demo 0/1 Running 0 8s
lifecycle-demo 1/1 Running 0 12s

注意 READY 列的变化:0/1 持续到 readinessProbe 通过,才变为 1/1。这是从 Endpoints 角度看到的就绪状态。

观察 conditions

1
kubectl get pod lifecycle-demo -o json | jq '.status.conditions[] | {type, status, reason}'

可以看到四个 condition 的状态,以及 PodScheduledInitialized 先于 ContainersReady 变为 True

观察 Events 的时间序列

1
kubectl describe pod lifecycle-demo

Events 部分会显示:

1
2
3
4
5
6
7
8
9
10
11
12
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 30s default-scheduler Successfully assigned default/lifecycle-demo to kind-control-plane
Normal Pulling 29s kubelet Pulling image "busybox:1.35"
Normal Pulled 25s kubelet Successfully pulled image "busybox:1.35"
Normal Created 25s kubelet Created container init-db
Normal Started 25s kubelet Started container init-db
Normal Pulling 22s kubelet Pulling image "nginx:1.25"
Normal Pulled 18s kubelet Successfully pulled image "nginx:1.25"
Normal Created 18s kubelet Created container app
Normal Started 18s kubelet Started container app

ScheduledPulling(init container 的镜像)之间的间隔,是 kubelet 发现 Pod 并开始处理的时间。PullingPulled 是镜像下载时间。Started(init container)到下一个 Pulling(主容器镜像)是 init container 的执行时间。

触发 livenessProbe 失败

1
kubectl exec lifecycle-demo -- rm /usr/share/nginx/html/index.html

几秒后观察:

1
kubectl get pod lifecycle-demo -w

当 livenessProbe 连续失败达到 failureThreshold 次,会看到 RESTARTS 列的计数增加,Pod 进入 Running 状态后 RESTARTS 从 0 变 1。

观察优雅终止

1
kubectl delete pod lifecycle-demo

-w 观察到 Pod 进入 Terminating 状态,等待约 30 秒(如果 nginx 没有其他进程在运行,实际上 nginx 会很快响应 SIGTERM 退出,不会等满 30 秒)。

将实验结果映射到 K8s 对象

kubectl get pod -w 输出的 STATUS 列不是直接来自 status.phase,而是 kubectl 基于多个字段计算的显示字符串:

  • Init:N/M:正在运行第 N 个(共 M 个)init container,来自 status.initContainerStatuses
  • PodInitializing:init containers 全部完成,主容器镜像正在拉取
  • ContainerCreating:镜像拉取完成,容器正在创建
  • Running:phase 为 Running
  • TerminatingdeletionTimestamp 已设置但 Pod 对象还在 etcd 中

READY 列(如 1/1)来自 status.containerStatusesready=true 的数量比总数量,由 readinessProbe 结果驱动。

Events 由 kubelet 通过 API Server 写入,存储在 etcd 中,默认保留 1 小时。每个 Event 对应 source.component(如 kubeletdefault-scheduler),可以追溯是哪个组件触发了哪个操作。

模式提炼

Pod 状态机是"分层状态 + 多探针 + 优雅终止"三个机制的组合。

分层状态:phase 是粗粒度的外部可见状态,conditions 是细粒度的阶段检查点,containerStatuses 是每个容器的微观状态。三层叠加给出完整的 Pod 状态视图,而不是用单一字段表示所有情况。

多探针分离职责:startupProbe 解决启动时间不确定的问题,livenessProbe 解决死锁检测问题,readinessProbe 解决流量路由问题。三个探针回答三个不同的问题,混用或省略任一个都会带来不同的故障模式。

优雅终止:preStop + SIGTERM + gracePeriod + SIGKILL 的四层机制,给应用足够的时间完成正在处理的请求、刷新缓存、关闭连接。滚动更新期间,配合 Endpoints controller 的摘除时序,可以实现请求零中断的发布。

工程迁移表

K8s 机制 类比概念 差异点
startupProbe Spring Boot Actuator /healthinitialDelaySeconds K8s 的 startupProbe 期间 liveness 被完全挂起,Spring 的 delay 只是等待,liveness 检查仍然生效
livenessProbe JVM shutdown hook 检测 / watchdog 进程 livenessProbe 由外部 kubelet 执行,不依赖进程自身;JVM shutdown hook 是进程内机制
readinessProbe 数据库连接池的 testOnBorrow / 负载均衡器健康检查 readinessProbe 失败只影响 Endpoints,不影响 Pod 存活;数据库连接池的健康检查失败通常会丢弃连接
preStop + SIGTERM JVM shutdown hook / systemd ExecStop preStop 是容器生命周期钩子,在 SIGTERM 之前执行;JVM shutdown hook 在 JVM 收到 SIGTERM 后才触发
terminationGracePeriodSeconds 数据库连接池 drain 超时 / 线程池 awaitTermination 超时后 SIGKILL 强制杀死,不给进程任何处理机会
initContainers 数据库 migration 脚本(在应用启动前运行)/ Spring ApplicationContextInitializer init container 完全独立于主容器,可以用不同镜像,主容器失败不会重跑 init container
Pod phase Spring Bean BeanDefinitionParserDelegate 状态 phase 是 kubelet 上报到 API Server 的摘要,不是状态机的内部状态;真实状态在容器运行时里

常见误解

误解一:phase=Running 说明应用已经可以接受请求

phase=Running 只意味着至少一个主容器正在运行(进程存在)。readinessProbe 可能还没有通过,Pod 还不在 Service 的 Endpoints 里。在滚动更新场景中,如果不配置 readinessProbe,新 Pod 一旦 Running 就会收到流量,但应用可能还在初始化。

验证方式:kubectl get endpoints <service-name>,看 Pod 的 IP 是否出现在 ENDPOINTS 列。

误解二:发送 SIGTERM 后容器会立即被杀死

发送 SIGTERM 后,kubelet 等待 terminationGracePeriodSeconds(默认 30 秒)。如果进程在这个时间内自然退出,就不会收到 SIGKILL。SIGTERM 是"请求终止"信号,进程可以捕获它并执行清理工作。

terminationGracePeriodSeconds 期间,Pod 的 phaseRunning(直到容器全部退出),status.deletionTimestamp 已设置,这是 Terminating 状态的实际来源。

误解三:Init Containers 可以并行执行

Init containers 严格串行。数组中的 init container 按顺序执行,每个必须成功退出才运行下一个。这与主容器并行启动形成对比。

串行的设计是有意的:init containers 通常用于依赖检查(等待数据库就绪)、数据准备(从外部存储同步配置)等场景,这些场景需要确定的执行顺序。

如果需要并行初始化,只能通过主容器的 postStart hook 或者应用自身的启动逻辑实现,而不是 init containers。

练习

  1. 创建一个带三个 init containers 的 Pod,其中第二个 init container 故意失败(exit 1),观察 kubectl describe pod 的 Events 和 status.initContainerStatuses,确认 restartCount 在不同 restartPolicy 下的行为。

  2. 创建一个 Pod,配置 readinessProbe 检查一个不存在的路径(如 /healthz),并创建一个 Service 指向它,通过 kubectl get endpoints 确认 Pod 不在 Endpoints 里;然后 exec 进容器创建该路径,观察 Endpoints 的变化。

  3. 创建一个 Pod,将 terminationGracePeriodSeconds 设为 60,在容器的 preStop hook 里执行 sleep 10,然后 delete 这个 Pod 并用 -w 观察 Terminating 阶段的持续时间;修改 sleep 为 70(超过 grace period),观察 Pod 是否被强制杀死。

  4. 模拟 OOM Kill:在容器里运行一个分配大量内存的进程,观察 kubectl describe pod 中的 LastState,确认 reason: OOMKilled,并观察 restartCount 的变化和 phase 是否保持 Running。

系列导航

参考资料