Kubernetes Pod 生命周期源码分析:创建、容器重启、重建与资源回收
最后更新:
容器退出后又启动了,Pod 的名字没有变,emptyDir 里的文件也还在。删除 Pod 后,Deployment 补出了一个新副本,文件却不见了。这两次恢复经过了哪些代码,又分别保留了哪些资源?
只画一条“API Server → scheduler → kubelet”的线还回答不了这些问题。Pod 是 API 对象,节点上还有 sandbox、进程、命名空间、cgroup、挂载目录和容器文件系统;它们由不同组件创建,也不会在同一时刻消失。理解生命周期,需要把这些对象的身份和回收条件一起跟踪。
本文使用 2026 年 10 月 22 日查询到的最新稳定 release Kubernetes v1.37.1,固定源码提交 f78e722310e50bcaca9276be22276d9e91d91308。在 API Server、scheduler、controller-manager 和 kubelet 中添加日志后,编译节点镜像,在 zj 上的双节点 kind 集群运行。环境为 kind v0.33.0、containerd 2.3.4、runc 1.4.3、Linux 6.12,使用 kindnet 和 local-path 存储。
插桩补丁、实验代码和使用说明见 k8sdev/pod-lifecycle-source。
先把一个 Pod 的完整生命期连起来
下面的总图以普通、由 API 管理的 Linux Pod 为对象。跨组件交互用时序图,组件内部的判断和重试用流程图。时序图的实线箭头表示请求或调用,虚线箭头表示返回或标明了 watch 的异步通知;watch 不代表接收方已经完成处理。par 中的分支独立推进,不保证彼此的完成顺序。图中的“终止中”“节点清理完成”是处理阶段,不是 Pod phase 枚举。kubectl 显示的 Terminating、CrashLoopBackOff 也不是额外的 phase。
sequenceDiagram
participant W as 用户 / 工作负载控制器
participant A as API Server
participant S as scheduler
participant K as kubelet
participant R as CRI 运行时
W->>A: 创建 Pod
A->>A: 准入、校验、持久化,分配 UID
A-->>W: 返回创建结果
A-->>S: watch:待调度 Pod
S->>A: Binding 请求
A->>A: 持久化 spec.nodeName
A-->>K: watch:分配到本节点的 Pod
K->>K: 节点准入、cgroup、目录与卷准备
K->>R: RunPodSandbox / CreateContainer / StartContainer
R-->>K: sandbox 与容器状态
K->>A: 更新 Pod status
opt 同一 UID 内的故障恢复
K->>R: 按策略重启容器或重建 sandbox
K->>A: 更新状态,保持 UID
end
alt 收到 API 删除请求
W->>A: DELETE Pod
A-->>K: watch:删除标记
else 工作自然完成或节点要求终止
K->>K: 转入终止处理
end
par 节点停止与清理
K->>R: 停止容器与 sandbox
K->>K: 卸载卷、释放资源;后台 GC 独立推进
K->>A: 更新终态;满足条件时完成删除请求
and 控制面协调
A-->>W: watch:删除状态或终态
opt 所属控制器仍需要副本
W->>A: 创建替代 Pod,获得新 UID
end
end
Note over W,R: API 对象删除、旧节点清理、新副本启动没有统一完成时刻
自然完成的裸 Pod 可以长期保留终态 API 对象;进入节点终止处理并不自动等于收到 API DELETE。反过来,强制删除 API 对象也不证明节点上的进程已经停止。后文把创建、运行、替换、删除和回收分别展开,避免在一张图中把这些不同步的动作连成假的串行流程。
这里的工作负载控制器不是 scheduler。Deployment、ReplicaSet、Job、StatefulSet 控制器运行在 kube-controller-manager 中,分别负责各自对象的协调。EndpointSlice 控制器负责服务端点,PodGC 负责符合条件的 Pod API 对象清理,也属于 kube-controller-manager。它们通过 API 和 informer 协作,不在一个函数里依次调用;后面的时序图会分别标出控制器及其进程归属。
用 UID 区分 Pod,用容器 ID 区分进程实例
Pod 名字适合查找,UID 才能区分对象的不同生命期。删除后再创建同名 Pod,UID 也会改变。节点上的容器又有各自的 ID;同一个 Pod 中的容器重启,不要求 Pod UID 改变。
本次实验的主对象是一个 Deployment:Pod 中包含普通 init 容器、原生 sidecar 和应用容器。应用挂载普通 emptyDir、内存 emptyDir、PVC、ConfigMap、Secret 和 downwardAPI,并配置三种探针。
下面是实际采集的身份变化,ID 只展示前缀:
| 操作后的状态 | Pod UID | Sandbox ID | 应用容器 ID | Pod IP |
|---|---|---|---|---|
| 首次启动 | c0b2e8f7 | f858431b48ef | 17c28e0cf0c5 | 10.244.1.6 |
| readiness 失败 | c0b2e8f7 | f858431b48ef | 17c28e0cf0c5 | 10.244.1.6 |
| 应用进程崩溃后重启 | c0b2e8f7 | f858431b48ef | 7a893854c92c | 10.244.1.6 |
| liveness 失败后重启 | c0b2e8f7 | f858431b48ef | 65d149471cac | 10.244.1.6 |
| RestartAllContainers | c0b2e8f7 | f858431b48ef | e9214c79a4ea | 10.244.1.6 |
| sandbox 故障后恢复 | c0b2e8f7 | 4757b58f237b | b9073dcd8221 | 10.244.1.7 |
| 删除后由控制器补副本 | 78c15fda | 9a6f11eae5b9 | 75a04f1a11d6 | 10.244.1.8 |
这张表也说明,不能拿 IP 当作 Pod 的身份。本次 sandbox 重建保留了 UID,却重新分配了 IP;具体地址是否变化还取决于网络实现和 IP 分配结果。
API 写入完成,节点执行才刚开始
直接创建 Pod 时,请求先经过 API Server 的认证、鉴权、解码、默认值处理和准入。创建 Deployment 时,最初写入的是 Deployment;Deployment 控制器管理 ReplicaSet,ReplicaSet 再根据期望副本数创建 Pod。controller-manager 中的 RealPodControl.createPods最终仍通过 Kubernetes API 创建 Pod。
Pod 的 REST 存储注册在 pkg/registry/core/pod/storage/storage.go。通用的 Store.create执行创建策略、校验及存储操作,etcd 后端的 store.Create负责实际写入。
我把 LIFECYCLE storage.created 放在存储创建成功之后,并记录 UID、resourceVersion 和 dryRun。这样可以区分“进入了创建函数”和“创建已经成功”,也不会把 server dry-run 当作真实持久化。
本次 Pod 写入记录为:
| |
上面省略了日志前缀和长 ID,未改变阶段与时间。API Server 不会在这次请求里等待 scheduler 选好节点、kubelet 拉起应用后才返回成功。
准入也可能改变最终存下来的 Pod。实验 YAML 只指定了 serviceAccountName: reader,实际对象中多出了 kube-api-access-... projected 卷。它来自 ServiceAccount 准入插件:满足自动挂载条件时,为 Pod 添加投射卷及容器挂载。这份运行中的 PodSpec 才是后续组件执行的依据。
创建阶段有两层异步关系:工作负载控制器通过 API 创建下一级对象,scheduler 再通过 watch 发现待调度的 Pod。下面将认证、默认值、准入与策略校验合并为请求处理阶段;图中的失败分支都不会生成一个已经运行的 Pod。
sequenceDiagram
participant U as 用户
participant A as API Server
box kube-controller-manager
participant D as Deployment 控制器
participant R as ReplicaSet 控制器
end
participant E as etcd
participant S as scheduler
opt 使用 Deployment 管理 Pod
U->>A: 创建 Deployment
A->>E: 保存 Deployment
A-->>D: watch / informer 通知
D->>A: 创建或更新 ReplicaSet
A->>E: 保存 ReplicaSet
A-->>R: watch / informer 通知
R->>R: 检查期望与实际副本数
end
alt ReplicaSet 需要补副本
R->>A: 创建 Pod
else 用户直接创建裸 Pod
U->>A: 创建 Pod
end
A->>A: 认证、鉴权、默认值、准入与策略校验
alt 请求不合法
Note over U,A: 返回错误给本次调用方,不保存 Pod
else 校验通过且非 dry-run
A->>E: Store.create → store.Create
E-->>A: 写入成功,返回 resourceVersion
Note over U,A: 向调用方返回 Pod,不等待节点启动
A-->>S: watch:未绑定且 schedulerName 匹配的 Pod
S->>S: 加入调度队列
end
调度绑定只确定节点,不创建容器
scheduler 从调度队列取出 Pod,执行过滤、打分、资源预留及绑定。主路径在 schedule_one.go,默认绑定插件在 DefaultBinder.Bind。
调度器的 assume 操作先在自身缓存中记账,防止下一次调度继续使用已经承诺出去的容量。随后,绑定请求经 Pod 的 Binding 子资源进入 API Server;BindingREST.setPodNodeAndMetadata通过条件更新设置 spec.nodeName。本地 assume 和 API 中绑定成功是两个动作,失败时还需要释放前面的预留。
绑定请求日志为:
| |
这条插桩位于绑定入口,因此名字明确写成 requested。是否完成绑定,还要看 API 对象的 nodeName 和节点随后收到的 Pod,不能仅凭这一行宣布“调度完成”。
PVC 也可能参与调度。使用延迟绑定的 StorageClass 时,VolumeBinding 插件会把节点选择和卷供应协调起来;Pod 不能先在任意节点启动,再期待本地卷自动出现在那里。详细过程见 PVC 生命周期。
flowchart TB
A["调度队列取出 Pod<br/>读取节点与已分配资源快照"] --> B["PreFilter / Filter<br/>检查资源、亲和性、卷等约束"]
B --> C{"存在可用节点?"}
C -->|"否"| PF["PostFilter 可尝试抢占等处理<br/>不保证找到可行解"]
PF --> D["记录失败<br/>等待相关事件或退避后重新入队"]
D -. "再次尝试" .-> A
C -->|"是"| E["PreScore / Score<br/>选出目标节点"]
E --> F["assume:本地缓存记账<br/>Reserve:插件预留资源"]
F --> G["Permit:通过、等待或拒绝"]
G -->|"可进入绑定周期"| H["异步 binding cycle<br/>等待 Permit,执行 PreBind"]
H --> I["Bind 插件提交 pods/binding<br/>API 条件更新 spec.nodeName"]
I -->|"成功"| J["PostBind;绑定结果经 watch 传播"]
F -->|"预留失败"| U["Unreserve / Forget<br/>释放本次预留与缓存假定"]
G -->|"拒绝"| U
H -->|"等待或 PreBind 失败"| U
I -->|"绑定失败"| U
U --> D
J -. "本节点 Pod 更新" .-> K["kubelet 接收 Pod"]
这些插件阶段以实际调度配置为准,不表示每个阶段都有外部调用。抢占也不是“删掉低优先级 Pod 后立即绑定”:受害者退出和资源释放仍需要时间,候选节点不等于已绑定节点。直接指定 nodeName 的 Pod 会绕过这条 scheduler 路径,仍需接受节点侧准入和资源准备。
kubelet 按 UID 推进状态,不为每个事件开一套创建流程
kubelet 的 API 源在 config/apiserver.go中按本节点的 spec.nodeName 监听 Pod。API 更新、运行时事件、探针结果和周期同步,都可能触发一次检查。
这些通知最终交给 podWorkers。它按 Pod UID 维护 worker;容量为 1 的通知 channel 只表示“有工作”,实际待处理状态放在 pendingUpdate 中。多次通知可以合并,worker 再读取当前状态执行。
这个设计有两个作用。相同 UID 的同步被串行推进,避免同时创建或停止同一批容器;事件密集时,也不必逐条重放已经过时的中间状态。同名新 Pod 使用新 UID,不会继承旧实例的 worker 状态。
worker 又区分三条执行路径:
SyncPod:根据期望规约与当前运行时状态推进启动、运行和重启。SyncTerminatingPod:停止容器和 sandbox。SyncTerminatedPod:等待卷卸载,处理 cgroup、缓存登记及最终状态。
进入终止状态后,普通更新不能把旧 Pod 再带回创建路径。这里的控制对象始终是一个 UID,而不是一个会反复“复活”的名字。
flowchart TB
A["API 配置更新"] -.-> Q["kubelet 同步入口<br/>按 UID 更新 pendingUpdate"]
B["运行时事件 / PLEG<br/>探针结果、周期同步"] -.-> Q
Q --> W["通知该 UID 的 worker<br/>合并重复通知,串行执行"]
W --> S{"worker 当前阶段"}
S -->|"正常同步"| P["SyncPod<br/>比较期望与当前状态"]
P -->|"仍需运行"| R["安排下一轮同步"]
R -.-> Q
P -->|"自然完成或需要终止"| T["SyncTerminatingPod"]
S -->|"已请求终止"| T
T -->|"停止失败"| RT["保留终止状态,退避重试"]
RT -.-> T
T -->|"确认停止成功"| C["SyncTerminatedPod"]
S -->|"容器已停止,待清理"| C
C -->|"卸载或清理失败"| RC["保留清理阶段,后续重试"]
RC -.-> C
C -->|"完成"| F["该 UID 的 worker 完成<br/>其他后台清理仍按各自条件推进"]
节点资源的创建顺序
节点侧准入在 kubelet 接纳 Pod 时检查;通过后,kubelet.SyncPod处理 Pod 状态、Pod cgroup、目录、卷和运行时同步。其中几个关键的依赖关系是:
sequenceDiagram
box kubelet
participant K as Pod worker / SyncPod
participant V as 卷管理器
participant M as kuberuntime
end
participant R as CRI 运行时
participant C as CNI 插件
Note over K,M: 前置条件:节点准入通过,Pod 未要求终止<br/>已到终态或被拒绝的 Pod 不继续这条启动路径
K->>K: 检查网络就绪条件,登记 Secret / ConfigMap<br/>创建或确认 Pod cgroup 与数据目录
Note over K: 网络未就绪或目录、cgroup 准备失败时,本轮返回
K->>V: WaitForAttachAndMount
V->>V: 独立协调所需卷的准备
V-->>K: 返回挂载结果
alt 卷未就绪
K->>K: 返回错误,后续同步重试<br/>特定不可恢复错误进入终止
else 卷就绪
K->>K: 获取镜像拉取凭据,登记探针
K->>M: SyncPod
M->>M: computePodActions
opt 需要创建或更换 sandbox
M->>R: 如有旧实例,先停止旧容器与 sandbox
M->>M: 按需准备 DRA,生成 sandbox 配置
M->>R: RunPodSandbox
R->>C: 普通网络 Pod:调用 CNI 配置网络
C-->>R: 网络配置结果
R-->>M: sandbox ID 或错误
Note over M,R: 停止、DRA 或 sandbox 创建失败,本轮中止并交回重试
M->>R: PodSandboxStatus
R-->>M: sandbox 状态与 IP
M->>K: OnPodSandboxReady
end
M->>M: 使用可用 sandbox<br/>推进 init、sidecar 与应用容器启动
M-->>K: 本轮同步结果
end
这张图只表示节点上的主要依赖。卷管理器、探针和状态管理器还有各自的循环,不是所有工作都发生在一个同步函数里。
PodReadyToStartContainers 表达 sandbox、网络及相关准备条件,不代表镜像已经拉取或应用已经 Ready。v1.37.1 在 sandbox 就绪回调处推进这项状态;没有配置 DRA 资源时,图中的 DRA 准备不代表实际分配了设备。
cgroup 和卷先准备好
PodContainerManager.EnsureExists创建 Pod 层级的 cgroup。容器创建时,资源配置再交给运行时落实到容器 cgroup。实验应用设置 cpu request=50m、cpu limit=200m、memory limit=128Mi,节点上读到:
| |
request 影响调度记账及相应的资源权重;limit 对应运行时约束。它们不是“在 API 里填一个数字就已经分配了一块物理硬件”。CPU 配额的性能影响见 CPU 资源管理。
SyncPod 随后调用 WaitForAttachAndMount。卷准备失败时,这次同步不会继续进入容器运行时的 SyncPod。本次缺失 ConfigMap 的实验正好停在这里:已有 nodeName,出现 FailedMount,但 CRI 中还没有这个 UID 的 sandbox。补齐 ConfigMap 后,同一 UID 继续启动。
ServiceAccount token 则由 projected 卷插件通过 TokenRequest 获取并投射。这个 token 与 Pod 绑定,不应被理解成每创建一个 Pod 就生成一个永久 Secret 对象。
sandbox 提供容器共同使用的运行环境
kuberuntime.SyncPod先通过 computePodActions 比较规约与当前状态,决定哪些容器要保留、停止、创建,以及是否需要新 sandbox。
创建 sandbox 的入口是 createPodSandbox,它生成 CRI 配置并调用 RunPodSandbox。kubelet 没有直接执行 CNI 插件:本实验使用的 containerd RunPodSandbox创建网络命名空间,通过 setupPodNetwork 调用 CNI,再把 sandbox 信息返回给 kubelet。
普通 Pod 内的容器共享网络环境,所以能通过 localhost 通信。PID namespace 默认不因此共享;是否共享进程命名空间由 shareProcessNamespace 等配置决定。容器各自仍有根文件系统和挂载视图,不能把 sandbox 理解成“所有隔离资源都只有一份”。
init、sidecar 和应用容器有不同的启动条件
startContainer负责镜像检查、生成容器配置,再调用 CRI 的 CreateContainer 和 StartContainer。
普通 init 容器要成功结束,才推进后续初始化。原生 sidecar 是带 restartPolicy: Always 的 init 容器:满足启动条件后允许继续启动后续容器,同时自身保持运行;配置 startupProbe 时,该探针也参与它的启动判定。
flowchart TB
A["sandbox 已可用<br/>computePodActions 决定本轮启动集合"] --> E["先处理尚未创建的 ephemeral containers<br/>存在时才执行,可用于排查 init 故障"]
E --> I{"init 序列推进到哪里?"}
I -->|"普通 init 待执行"| N["启动该 init,等待退出"]
N --> Q{"退出结果"}
Q -->|"成功"| NEXT["下一轮推进后续 init"]
Q -->|"失败且允许重启"| RETRY["退避后重启该 init"]
RETRY --> N
Q -->|"失败且不允许重启"| FAIL["Pod 失败,进入终止路径"]
I -->|"原生 sidecar 待启动"| S["启动 restartPolicy=Always 的 init"]
S --> START{"已满足 started 条件?"}
START -->|"未满足"| WAIT["等待启动结果或 startupProbe<br/>失败时按 sidecar 规则恢复"]
WAIT -.-> START
START -->|"满足"| NEXT
NEXT -.-> I
I -->|"初始化完成"| APP["启动本轮需要启动的应用容器<br/>应用容器之间不保证业务就绪顺序"]
APP --> RUN["持续运行,分别计算容器与 Pod 状态"]
每次“启动容器”还会经过下面的调用链。ephemeral container 只在没有对应运行时状态时创建,退出后不按普通应用容器的策略自动重启。sidecar 的 started 也不同于 ready:它可以已经允许后续初始化推进,却还没有达到服务就绪条件。
sequenceDiagram
box kubelet
participant M as kuberuntime
participant I as 镜像管理器
participant L as 内部生命周期钩子
end
participant R as CRI 运行时
participant P as 容器进程
M->>M: 检查重启退避,未到时间则本轮跳过
M->>I: EnsureImageExists
I->>R: 按 pullPolicy 检查镜像或 PullImage
R-->>I: 镜像结果
I-->>M: 镜像引用或错误
M->>M: 生成挂载、环境变量、资源与安全配置
M->>R: CreateContainer
R-->>M: 容器 ID
M->>L: PreStartContainer
L-->>M: 节点资源管理动作结果
M->>R: StartContainer
R->>P: 启动进程
R-->>M: 启动调用结果
opt 配置了 postStart
M->>M: 通过生命周期 runner 执行 hook
Note over M,P: 应用进程可同时执行,postStart 不在 ENTRYPOINT 之前
opt postStart 失败
M->>R: 停止该容器
end
end
Note over M,R: 每一步失败都会返回相应错误,后续同步按策略与退避处理<br/>图中的后续成功步骤以此前调用成功为前提
postStart 在 StartContainer 返回后调用,但进程此时已经开始执行,因此它不是应用 ENTRYPOINT 之前的初始化屏障。内部 PreStartContainer 则是 kubelet 的资源管理钩子,不能与 PodSpec 中的生命周期 hook 混为一谈。
本次同一 UID 的日志顺序如下:
| |
其中穿插了多次 kubelet.sync。这不表示每次同步都重建了一遍 Pod:每轮都会比较当前状态,只执行仍然需要的动作。
Running、Ready 和流量资格分别表达什么
Pod phase 的 Running 表示容器已经创建,且至少有一个仍在运行或处于启动、重启过程中。它不等于应用请求一定成功。Ready condition 还取决于容器就绪状态与 readiness gates。
实验删除应用的 readiness 标记文件后,Pod UID、sandbox 和容器 ID 全部不变,EndpointSlice 的 ready 变成 false。恢复文件后,Pod 恢复就绪,没有触发重新调度或创建容器。
EndpointSlice 的状态转换可以直接看 podToEndpoint。在本实验默认 Service 配置下,删除中的 Pod 出现过:
| |
这三个值并不矛盾:Pod 正在终止,不再作为普通就绪端点,但应用当时仍然处于可服务状态。Service、EndpointSlice 控制器和节点数据面各自更新,API 中的端点变化也不是所有网络转发规则同时完成切换的证明。
四种容易混为一谈的重启
单个容器退出
进程崩溃、OOM 或 liveness 失败后,kubelet 根据重启策略和当前状态决定是否创建新容器。调度器不负责这个过程,Pod 也没有重新经历 API 创建和绑定。
本次杀死应用进程后,容器 ID 改变,restartCount 从 0 增加到 1,sandbox、普通 init 和 sidecar 的容器 ID 都没有改变。两个 emptyDir 中的标记仍然存在。
故障注入从节点的祖先 PID namespace 向应用进程发 SIGKILL。容器中的 PID 1 有特殊信号语义,不能把容器内 kill -9 1 的命令返回值当成进程已经死亡的证据。
OOM 场景使用另一个 restartPolicy: OnFailure 的 Pod:容器内存上限为 32Mi,首次启动申请 128Mi,随后记录 reason=OOMKilled、exitCode=137 并重启。单凭退出码 137 不能认定 OOM,前面的人工 SIGKILL 也会产生这类退出码,需要同时看终止原因及内存证据。
探针触发的重启
readiness 失败改变就绪状态;startup 或 liveness 连续失败达到阈值时,kubelet 会安排停止目标容器,再按重启策略处理。停止路径还可能执行 preStop,并使用剩余终止宽限时间。
本次 liveness 失败后,应用换了容器 ID,sandbox 与卷保留。这里测试的是容器重启,不是删除 Pod。探针的阈值和结果缓存实现见 探针的作用与实现。
RestartAllContainers 原地重启
v1.37.1 中,ContainerRestartRules 和 RestartAllContainersOnContainerExits 的 feature gate 默认启用。实验应用配置了如下容器级规则:
| |
应用退出 42 后,普通 init、sidecar 和应用容器都产生了新的容器 ID;UID、sandbox 和两个 emptyDir 中的标记保持不变。
实现位于 computePodActions 与运行时 SyncPod 的容器重置分支:停止、移除需要重置的容器,再推进初始化和应用启动。它可以重跑初始化过程,却没有删除 API 中的 Pod。把它和 kubectl rollout restart 混为一谈,会误判数据和身份是否保留;后者通过修改控制器模板推动滚动替换。
sandbox 重建
sandbox 失效时,kubelet 也可能在同一个 Pod UID 下重建 sandbox 和容器。实验主动停止 sandbox 后,UID 不变,sandbox ID 改变,Pod IP 从 10.244.1.6 变成 10.244.1.7,两个 emptyDir 的标记仍保留。
因此“UID 没变”不代表“网络环境完全没变”,而“容器都重启了”也不代表“emptyDir 已清空”。需要分别检查 Pod、sandbox 和容器三个层次。
把运行期的分支合起来看,恢复范围由故障层次与策略共同决定:
flowchart TB
A["运行时或探针状态变化"] --> T{"该 UID 已要求终止?"}
T -->|"是"| END["进入终止路径,不再恢复旧 Pod"]
T -->|"否"| TYPE{"变化类型"}
TYPE -->|"readiness 失败或恢复"| READY["更新容器 Ready 与 Pod Ready<br/>不因此重启容器"]
READY -. "状态传播" .-> EP["EndpointSlice 更新<br/>数据面异步更新端点使用资格"]
TYPE -->|"startup / liveness 达到失败阈值"| STOP["安排停止目标容器"]
TYPE -->|"进程退出 / OOM"| POLICY["检查容器重启规则<br/>未匹配时使用有效 restartPolicy"]
STOP --> POLICY
POLICY -->|"匹配 RestartAllContainers"| ALL["停止并移除需重置的容器<br/>保留 UID、sandbox 和 Pod 卷"]
ALL --> INIT["重新推进 init、sidecar、应用启动"]
POLICY -->|"允许单容器重启"| ONE["经过退避创建新容器 ID<br/>保留可用 sandbox 与 Pod 卷"]
POLICY -->|"不允许重启"| CHECK{"还有需要运行的工作?"}
CHECK -->|"有"| KEEP["继续其他容器,汇总 Pod 状态"]
CHECK -->|"无"| TERMINAL["按退出结果确定 Succeeded / Failed<br/>进入节点终止处理"]
TYPE -->|"sandbox 不可用或必须更换"| SB["计算 KillPod / CreateSandbox<br/>停止旧容器与 sandbox"]
SB --> AGAIN{"策略与完成状态允许重新启动?"}
AGAIN -->|"允许"| NEWSB["新 sandbox / 网络环境<br/>同一 UID,重新推进容器启动"]
AGAIN -->|"不允许"| TERMINAL
这张图中的策略检查不能简化成“退出就重启”。对普通应用容器,Always 会重启,OnFailure 只在失败时重启,Never 不重启;启用容器级规则时还要先判断规则。原生 sidecar 与 ephemeral container 使用前文各自的规则。startupProbe 尚未成功时,其他探针不会像正常运行阶段那样推进检查;readiness 失败始终不承担杀容器的职责。
computePodActions 也会避免因 sandbox 丢失而重新启动已经完成、且不应重启的工作。图中的 sandbox 故障恢复不是无条件重新执行整个 Pod。
控制器重建的是新 Pod
删除 Deployment 管理的 Pod 后,ReplicaSet 发现可计入的副本不足,重新调用 Pod 创建接口。API Server 为新对象分配新 UID,scheduler 再选节点,kubelet 再创建该 UID 的资源。旧对象不是被移动到另一台节点后继续运行。
本次旧 Pod 的删除请求与新副本创建记录分别为:
| |
旧 Pod 当时还有 Finalizer,应用退出发生在后面。这说明 API 中可以同时看到旧的终止实例和新副本,不能仅按对象数量判断“控制器多创建了一个”。
新副本中的 emptyDir 标记消失,而 PVC 中的 user-marker 保留。两种存储对应不同生命周期:emptyDir 属于旧 Pod 在该节点上的资源;普通 PVC 独立存在,新 Pod 可以继续引用它。
并非所有 Pod 都有控制器补副本。实验另建了两个裸 Pod,均配置 restartPolicy: Never:退出 0 的进入 Succeeded,退出 7 的进入 Failed,容器重启次数都是 0。Job、Deployment、StatefulSet 对终态和副本的处理各不相同;StatefulSet 即使重新使用同一个 Pod 名字,新对象仍有新 UID。
sequenceDiagram
participant A as API Server
box kube-controller-manager
participant R as ReplicaSet 控制器
end
participant S as scheduler
participant O as 旧 Pod 所在 kubelet
participant N as 新 Pod 所在 kubelet
Note over A,N: 两个 kubelet 泳道表示职责,新旧 Pod 也可能在同一节点
par 旧 UID 的终止
A-->>O: watch:旧 Pod 删除标记或终态
O->>O: 停止旧容器,清理旧 UID 资源
O->>A: 更新旧 Pod 状态
and 控制器补副本
A-->>R: informer:旧 Pod 删除标记或终态
R->>R: 检查可计入副本是否少于期望
alt 仍需补副本
R->>A: 创建新 Pod
A-->>R: 返回新 UID
A-->>S: watch:新 Pod 待调度
S->>A: 绑定新 Pod
A-->>N: watch:新 Pod 分配到本节点
N->>N: 新建 sandbox、容器、emptyDir,挂载引用的 PVC
N->>A: 更新新 Pod 状态与 Ready
else 已满足副本数或缩容为 0
R->>R: 不创建替代 Pod
end
end
Note over A,O: 旧 API 对象可能仍受 Finalizer 阻挡,不妨碍 ReplicaSet 补新 UID
图中故意没有从“旧资源全部删除”连到“创建新 Pod”的箭头:本次 ReplicaSet 实验没有这个等待关系。StatefulSet 的同名替换、Job 的完成与重试策略需要按各自控制器判断,不能直接套用 ReplicaSet 的补副本条件。节点失联时,也必须区分“控制面决定替换”和“旧节点已停止进程”;前者不构成后者的证据。
删除包含多条并发推进的路径
通用存储的 Store.Delete会处理宽限期与 Finalizer。符合优雅删除条件时,API Server 先更新删除时间和宽限期,而不是立即把对象从存储中抹掉。
删除标记写入后,节点终止、服务端点更新和工作负载协调独立推进。图中用不同背景区分三条并行路径,点击图后可以缩小查看全貌,也可以放大查看细节。
sequenceDiagram
participant U as 删除请求方
participant A as API Server
participant K as kubelet
box kube-controller-manager
participant E as EndpointSlice 控制器
participant W as 工作负载控制器
end
participant P as kube-proxy / 数据面
U->>A: DELETE Pod<br/>宽限期与前置条件
A->>A: 保存删除标记与宽限期
A-->>U: 返回请求结果
par 节点终止
A-->>K: watch:Pod 删除标记
K->>K: SyncTerminatingPod<br/>停止容器与 sandbox
K->>A: 更新终态
K->>K: SyncTerminatedPod<br/>卸载卷、清理资源
opt canBeDeleted 满足
K->>A: DELETE,grace=0<br/>限定旧 Pod UID
end
and 服务端点更新
A-->>E: informer:Pod 删除标记<br/>或状态变化
E->>A: 更新 EndpointSlice<br/>默认 ready=false<br/>terminating=true
A-->>P: watch:EndpointSlice 更新
P->>P: 更新端点使用资格<br/>处理转发规则
and 工作负载协调
A-->>W: informer:Pod 删除标记
W->>W: 按本控制器规则<br/>检查期望与实际状态
alt 需要替代 Pod
W->>A: 创建新 Pod
A-->>W: 返回新 UID
else 不需要补副本
W->>W: 不创建 Pod
end
end
Note over A: 最终删除仍受 Finalizer 约束
Note over A,W: ReplicaSet 补副本不等待旧 UID 资源清理完毕
图中的并行分支没有固定的先后保证。尤其是流量更新和进程退出,不是一次跨组件事务。
EndpointSlice 的更新也要先写回 API Server,kube-proxy 再通过 watch 接收,EndpointSlice 控制器不会直接调用 kube-proxy。图中的默认 Service 行为是 ready=false,serving 仍取决于 Pod Ready;publishNotReadyAddresses 会改变 ready 的计算。
正常 kubelet 路径的最终删除条件可核对 status manager 的 canBeDeleted:对象已有删除时间、不是 mirror Pod、API 所见 phase 已终态,并且节点终止清理已完成。API Server 本身并不通过 CRI 检查进程;其他删除路径,尤其强制删除,不能被理解为已经执行了图中的所有节点步骤。
preStop 使用的是终止宽限期中的时间
SyncTerminatingPod调用运行时停止 Pod。killContainer处理 preStop、终止顺序及宽限时间,再调用 CRI StopContainer。运行时负责向进程发送停止信号,并在超时后执行强制终止。
实验设置 20 秒宽限期,preStop 等待约 4 秒。实际传给应用容器 StopContainer 的 gracePeriod 为 16,而不是重新得到完整 20 秒。
原生 sidecar 的停止安排在应用容器退出之后。本次日志中应用容器的停止完成时间是 15:43:07.020,sidecar 是 15:43:07.038,之后才请求停止 sandbox。这个顺序对日志转发、连接代理等 sidecar 尤其重要。
flowchart TB
A["SyncTerminatingPod<br/>记录终止状态,停止 liveness / startup 探针"] --> B["killPodWithSyncResult<br/>安排各运行容器的停止工作"]
B --> C["killContainer 确定有效宽限期<br/>处理 Pod、探针或调用方覆盖值"]
C --> D{"配置 preStop 且仍有时间?"}
D -->|"是"| H["执行 preStop<br/>从剩余宽限期扣除耗时"]
D -->|"否"| O["若存在终止顺序约束<br/>等待轮到该容器,并扣除等待时间"]
H --> O
O --> S["CRI StopContainer<br/>运行时发送有效停止信号<br/>超时则强制终止进程"]
S --> J["等待各容器停止调用结束<br/>应用先停,原生 sidecar 按反向顺序退出"]
J --> SB["对该 Pod 的 sandbox 调用 StopPodSandbox<br/>运行时停止 sandbox 并处理网络拆除"]
SB --> E{"本轮停止操作成功?"}
E -->|"否"| RETRY["返回错误,后续终止同步重试"]
RETRY -.-> B
E -->|"是"| VERIFY["移除探针,重新读取运行时状态<br/>确认没有仍在运行的容器"]
VERIFY -->|"仍有运行容器或查询失败"| RETRY
VERIFY -->|"确认全部停止"| DRA["按需 UnprepareDynamicResources<br/>失败则重试,不提前完成终止"]
DRA -->|"成功"| FINAL["根据最终退出状态更新 Pod status<br/>进入 SyncTerminatedPod"]
DRA -->|"失败"| RETRY
这里的顺序约束作用于停止信号前的等待,不能推导成“所有 sidecar 的 preStop 都要等应用退出才开始”。源码中 preStop 在 waitForTurn 之前执行。若等待耗尽宽限期,也不能保证每个 sidecar 都获得完整的优雅退出时间;killContainer 还会为最终停止调用保留最小时间窗口。StopPodSandbox 的错误会进入重试,停止成功与运行时对象已被 GC 删除仍是两件事。
Finalizer 保留对象,不负责让应用继续运行
实验给旧 Pod 添加了 lab.example.com/observe Finalizer,再发起删除。之后采集到的旧对象仍存在,并且:
| |
应用已经停止,Pod API 对象仍等待 Finalizer 被处理。这里的删除时间包含宽限期语义,不应直接拿它当“收到 DELETE 请求的精确时刻”;前面的请求日志发生在 15:43:02。
Finalizer 是由相应控制器或外部逻辑完成清理后移除的标记。API Server 不会因为看到一个字符串,就自动知道要清理哪块磁盘或哪个外部服务。强制删除也不能替代对节点进程和外部资源的确认,相关故障见 Pod 删除流程与 Finalizer。
API Server 内部的删除判断适合继续使用流程图。它只判断 API 对象和请求条件,不替代节点上的终止流程:
flowchart TB
A["Store.Delete 接收删除请求"] --> P{"前置条件与删除策略通过?"}
P -->|"否"| E["返回错误,不删除对象"]
P -->|"是"| G{"仍需优雅删除等待?"}
G -->|"是"| W["写入或保留删除时间与宽限期<br/>等待后续更新或删除请求"]
G -->|"否"| F{"仍有 Finalizer?"}
F -->|"是"| H["保留对象与删除标记<br/>等待责任方完成清理并移除 Finalizer"]
F -->|"否"| D["删除存储中的对象<br/>向 watch 传播删除事件"]
W -. "后续删除请求" .-> A
H -. "Finalizer 更新仍需满足宽限期条件" .-> G
进程退出之后还有资源回收
SyncTerminatedPod等待卷卸载、检查卷路径清理、注销 Secret/ConfigMap 的 Pod 登记、销毁 Pod cgroup,并推进最终状态。实验新增的 kubelet.cleaned 日志放在这条路径末尾。
这条日志不代表所有痕迹同时消失。停止过的容器、sandbox 和日志还受到 containerGC的保留与回收条件控制;孤立 Pod 目录通过 cleanupOrphanedPodDirs清理。回收逻辑还会检查卷是否仍然挂载,避免把一个没卸载的存储目录当作普通文件递归删除。
本次在 API 删除之后继续等待并检查,最终确认旧 UID 的容器和 sandbox 不再出现在 CRI 列表中,原网络命名空间路径、Pod cgroup 与 /var/lib/kubelet/pods/<uid> 目录也已消失。
控制面的 PodGC清理的是符合条件的 Pod API 对象;节点的容器 GC 和镜像 GC 各有自己的对象与策略。它们不应被合称为一次“删除 Pod”。
kube-controller-manager 在回收阶段也有职责,但不同控制器处理的是不同条件下的 API 资源:
| 控制器 | 观察与执行的工作 | 与节点清理的关系 |
|---|---|---|
| PodGC | 清理达到保留阈值的终态 Pod,以及符合条件的孤立、终止中 Pod | 操作 Pod API 对象,不直接调用节点 CRI 删除容器 |
| 通用垃圾回收控制器 | 根据 ownerReferences 和删除传播策略处理从属对象 | 删除控制器可能进一步删除其 Pod;不负责卸载 Pod 目录 |
| PV 控制器 | 协调 PVC/PV 绑定和卷回收状态 | 普通 Pod 删除不等于 PVC 删除,也不自动触发后端数据删除 |
| attach/detach 控制器 | 对需要 attach 的卷协调节点附加、分离 | 与 kubelet 的 mount/unmount 分工不同;CSI 场景还涉及外部 attacher 和驱动 |
前两项入口分别是 PodGC与 garbagecollector,存储部分可继续查看 persistentvolume和 attachdetach。这里列的是组件职责;本次 local-path 实验没有走 CSI attach/detach。外部供应器、CSI sidecar 和驱动也不应画进 kube-controller-manager 的进程框内。
sequenceDiagram
box kubelet 内部并行模块
participant W as Pod worker
participant V as 卷管理器
participant G as containerGC
participant D as 孤立目录清理
end
participant R as CRI 运行时
Note over W,D: SyncTerminatingPod 成功后,worker 允许移除运行时资源
par 卷与 Pod 资源清理
V->>V: 独立协调卷卸载与插件清理
W->>V: WaitForUnmount
V-->>W: 返回卸载结果
Note over W,V: 失败则本轮返回重试,仅成功后继续清理
W->>W: 确认卷路径清理完成<br/>注销 Secret / ConfigMap 登记
W->>W: 销毁 Pod cgroup,释放 user namespace<br/>statusManager.TerminatePod
and 运行时 GC
G->>G: 检查保留策略与 Pod 回收条件
G->>R: RemoveContainer,处理相应日志
G->>R: 清理符合条件的 sandbox<br/>StopPodSandbox / RemovePodSandbox
and 孤立目录回收
D->>D: 检查 Pod 已不再使用,卷未挂载
alt 确认可安全清理
D->>D: 删除孤立 Pod 目录
else 仍有挂载或检查失败
D->>D: 跳过并在后续清理中重试
end
end
这里没有把 GC 接在 kubelet.cleaned 后面:允许回收运行时资源的条件在 SyncTerminatingPod 完成后就成立,GC 可以与卷卸载、cgroup 清理交错发生。API 对象是否还保留、日志保留策略以及 sandbox 是否仍被容器引用,还会继续约束具体回收动作。镜像缓存由另一套镜像 GC 策略管理,不在这条 Pod 专属资源的删除链上。
Pod 相关资源由谁创建,又由谁回收
“Pod 拥有的资源”容易混入两种关系:节点资源属于某个 Pod 实例,API 对象则可能只是被引用。Kubernetes 的 ownerReferences 也有明确方向,例如 ReplicaSet 是 Pod 的 owner,不意味着 Pod 拥有 ReplicaSet。
| 资源 | 创建或维护者 | 容器重启与 Pod 删除时的行为 |
|---|---|---|
| Pod API 对象、UID、规约和状态 | API Server、控制器、kubelet | 容器重启保留 UID;删除重建产生新 UID |
| 调度缓存中的容量占用 | scheduler | 同一 Pod 的容器重启不重新调度;对象删除与缓存事件推动释放 |
| Pod sandbox、pause 与运行时元数据 | CRI 运行时 | 普通容器重启可复用;sandbox 故障可在原 UID 下重建;停止后还需 GC |
| 网络命名空间、接口、地址、路由和 IPAM 分配 | 运行时与 CNI | 与网络 sandbox 生命周期相关;Pod 重建不保证复用 IP |
| 容器进程、PID/mount namespace、可写根文件系统 | containerd、runc 与内核 | 容器重建产生新实例;共享哪些 namespace 由配置决定 |
| Pod/容器 cgroup、CPU 和内存约束 | kubelet 与运行时 | 容器有自己的层级;Pod 终止后清理 Pod 层级,本次已验证 |
| Pod 目录、hosts、resolv.conf、容器日志与日志链接 | kubelet 与运行时 | 分别受容器、Pod 及 GC 的清理条件影响;停止进程不等于立即删除日志 |
| 普通 emptyDir、内存 emptyDir | kubelet 卷插件 | 本次单容器重启、原地全容器重启和 sandbox 重建均保留;新 UID 的 Pod 不继承 |
| ConfigMap、Secret、downwardAPI 的节点投射文件 | kubelet 卷插件 | 卸载并清理该 Pod 的投射;原 ConfigMap、Secret API 对象独立存在 |
| ServiceAccount token 投射与本地缓存 | 准入插件、API Server、kubelet | 投射随 Pod 清理,token 有绑定和有效期;ServiceAccount 本身不会随 Pod 删除 |
| 普通 PVC、PV 与后端数据 | 存储控制器、供应器、卷插件/CSI | 删除 Pod 通常只解除使用与挂载;PVC 删除及 PV reclaimPolicy 再决定后续回收 |
| Service、EndpointSlice 与节点转发规则 | Service/EndpointSlice 控制器、kube-proxy 或其他数据面 | Pod 状态变化推动端点更新;Service 独立存在,EndpointSlice 通常归 Service 所有 |
| 镜像内容、共享只读层与节点缓存 | 运行时、kubelet 镜像 GC | 可能被多个 Pod 共用,不因删除一个 Pod 就全部清除 |
| Events 与运行历史 | 各组件、事件存储与日志系统 | 有独立保留策略,不能拿 Event 是否存在判断进程是否仍运行 |
本次实测覆盖表中的普通 Linux Pod 路径。还有几类资源要随 PodSpec 和集群能力单独展开:
- CSI attach 型卷可能产生 VolumeAttachment;节点上的 stage/publish 挂载与控制面的 attach/detach 是不同动作。本次 local-path 卷没有证明这些 CSI 路径,需结合 存储实验与具体驱动分析。
- 通用临时卷会为 Pod 创建带 ownerReference 的 PVC,回收关系与普通独立 PVC 不同;CSI inline ephemeral volume 又是另一条路径。
- 扩展资源和 Device Plugin、DRA ResourceClaim、独占 CPU、NUMA 拓扑、hugepages 会引入设备或节点资源分配。需要相应驱动、策略与硬件,不能由这个普通 Pod 的运行结果推出它们的释放行为。
- RuntimeClass、Pod overhead、user namespace、hostNetwork/hostPID、seccomp、SELinux 与 AppArmor 会改变资源与隔离配置;宿主机命名空间也不能按普通 Pod sandbox 的规则删除。
- ResourceQuota、LimitRange、RBAC、NetworkPolicy、PDB 是独立的限制或策略对象。它们会影响准入、运行或维护操作,但不会因为某个 Pod 消失就一起删除。
实际排障时,应把“谁引用它”“谁拥有它”“谁负责回收它”分开确认,尤其不要依据名字相同或位于同一 namespace 就判断所有权。
沿失败位置继续读源码
创建不是只能成功或失败的一次调用。同一个 UID 可以在条件补齐后继续推进,终止中的对象也可能在某条清理路径上等待。
本次验证了三处可区分的失败位置:
| 观察到的状态 | 本次确认的事实 | 下一段应查的路径 |
|---|---|---|
| PodScheduled=False,没有 nodeName | 没有节点满足标签条件 | scheduler 的过滤结果、事件及资源约束 |
| 有 nodeName,FailedMount,没有 sandbox | 缺失 ConfigMap;补齐后同一 UID 启动 | 卷管理器与具体卷插件 |
| 容器 OOMKilled,随后重启 | 32Mi 上限下申请 128Mi,OnFailure 触发重启 | 容器终止状态、cgroup 内存约束和重启策略 |
ImagePullBackOff、CNI 初始化失败、CSI 挂载超时、节点失联和强制删除也应按同样方式分析:先固定 UID,再判断已创建了哪些资源、哪个组件还在等待。源码里的调用入口说明责任归属,运行日志和资源状态则用来确认现场真正走到了哪里。