Kubernetes 容器运行时:kubelet、CRI、containerd 与 Pod Sandbox
Pod 已经分配到节点,却一直没有进入 Ready。此时 scheduler 已完成自己的工作,接下来要区分的是:运行环境没有建立、镜像没有准备好、容器进程没有启动,还是进程已启动但健康检查未通过。
这些阶段都可能表现为“容器还不能服务”,但负责组件、失败位置和恢复动作不同。Pod Sandbox 是把它们分开的一个重要观察点:一个 Sandbox 已经 Ready,并不意味着业务容器已经启动成功。
kubelet 通过 CRI 使用运行时
容器运行时接口(Container Runtime Interface,CRI)是 kubelet 与运行时之间的 gRPC 接口,包含 RuntimeService 与 ImageService。kubelet 不直接执行 runc,也不直接把 Docker CLI 命令拼成容器操作。本文环境使用 containerd,它的 CRI 实现接收请求,再安排底层容器任务与 OCI runtime 的执行。
kind 的节点在宿主机上是 Docker 容器,节点内部的 Kubernetes 工作负载则由该节点内的 containerd 管理。这是两层运行环境:在宿主机执行 docker ps 主要看到 kind 节点;要观察 Pod Sandbox 和业务容器,应进入目标节点使用 crictl。把两层容器 ID 混用,会查错日志和进程。
本文对应 Kubernetes v1.35.1(8fea90b45245ef5c8ba54e7ae044d3e777c22500),实际运行时为 containerd v2.2.1。没有修改 Kubernetes 二进制,也没有启用共享进程命名空间、host network 或容器级重启规则。
Sandbox 是 Pod 的运行环境边界
Sandbox 表示运行时为 Pod 建立的一组运行环境。Linux 普通 Pod 中,网络命名空间通常由 Sandbox 维持,多个业务容器加入同一个网络命名空间。因此它们共享 Pod IP、端口空间和 loopback,一个容器可以通过 127.0.0.1 访问另一个容器的监听端口,也可能发生端口占用冲突。
这不代表“Pod 内所有 namespace 全部共享”。NamespacesForPod 分别计算 Network、IPC 与 PID 配置。默认 PID 命名空间是容器级的;只有配置 shareProcessNamespace 等相应选项时,行为才改变。共享网络也不会自动共享容器的根文件系统,文件共享仍需要合适的卷挂载。
pause 是传统 Linux 运行时中用于维持 Sandbox 命名空间的基础容器进程。它不负责转发所有业务流量,也不代理每一次 localhost 请求。具体 Sandbox 实现受 RuntimeClass 和运行时影响,虚拟机隔离型运行时不能直接套用“一个 pause 进程加几个普通容器”的全部细节。
从 SyncPod 到业务进程启动
SyncPod 先通过 computePodActions 比较期望 Pod 与已观察到的运行时状态。它计算哪些容器需要停止、哪些需要启动,以及是否需要重新创建 Sandbox,不是每次同步都无条件重建全部容器。
需要 Sandbox 时,createPodSandbox 生成带有 namespace/name、UID、attempt、DNS、日志目录和安全配置的 PodSandboxConfig,最终调用 RunPodSandbox。这些标识让运行时状态能够重新关联到原来的 API 对象。
对于本文的 containerd 普通 Linux 网络路径,运行时负责创建网络命名空间并调用 CNI 建立 Pod 网络。containerd v2.2.1 的 RunPodSandbox 与 setupPodNetwork 展示了这条调用链:先保存 netns 路径,再调用网络插件并检查接口与 IP 结果。kubelet 与 containerd 之间的 CRI 返回成功,才会继续后面的启动路径;不能把图画成 kubelet 直接调用 CNI 后再通知运行时。Sandbox 创建中的网络错误通常出现在 FailedCreatePodSandBox 事件中。
这段代码还在调用 setupPodNetwork 之前登记失败清理,因为网络插件可能已经创建了一部分资源才返回错误。重试并不意味着前一轮没有副作用;containerd 会尝试拆除网络和相关运行环境,清理本身也可能失败。本文没有注入这种故障,这里的恢复关系依据源码,不能当作已验证的完整回滚保证。
Sandbox 可用后,kubelet 按初始化与容器生命周期规则推进。startContainer 的关键顺序是:
镜像准备完成后,kubelet 生成容器配置并调用 CRI CreateContainer。StartContainer 与 PostStart 的分支在后文展开。
| |
这些是这段函数内的执行顺序,不代表整个节点只允许一个容器镜像操作。其他 Pod 可以同时启动,镜像拉取并发还受 kubelet 配置约束。
CreateContainer 返回的是运行时容器 ID,不等于应用进程已经启动。StartContainer 成功后才记录 started 事件;即便如此,应用也可能立刻退出,或者尚未满足 readiness。PostStart 在启动之后调用,其失败还可能导致 kubelet 杀死已经启动的容器。
这里的先后关系是 kubelet 发起 CRI 调用与 Hook 调用的顺序。容器 ENTRYPOINT 已可在运行时中执行,PostStart 与它并发,没有“Hook 执行完,主进程才开始”的保证。依赖预先生成文件或检查前置条件的应用,应使用合适的 init container 或自身启动逻辑,不能把 PostStart 当作 ENTRYPOINT 之前的同步屏障。Hook 还可能重复投递,处理应当幂等;并发与投递语义见官方生命周期 Hook 文档。下面的源码可以核对 CRI 调用顺序及 Hook 失败后的终止分支。
sequenceDiagram
participant A as API Server
participant K as kubelet
participant R as containerd CRI
participant N as CNI
participant C as 业务进程
K->>A: 建立本节点 Pod 的 Watch
A-->>K: 返回已绑定 Pod 的变化
rect rgb(225, 239, 255)
K->>R: RunPodSandbox(UID 与网络配置)
R->>N: 普通 Linux 网络路径的 ADD
N-->>R: 网络配置结果
R-->>K: Sandbox ID(成功路径)
end
rect rgb(238, 246, 222)
K->>R: 检查或拉取业务镜像
K->>R: CreateContainer(Sandbox ID)
R-->>K: Container ID
K->>R: StartContainer
R->>C: 经底层运行时启动进程
R-->>K: 启动结果
end
K->>A: 异步更新容器状态和 Pod 状态
图展示首次成功启动的主路径。卷准备、安全上下文处理、init container 和探针未展开;没有画出 API 状态更新到实际进程之间不存在的同步完成屏障。API Server 也不直接连接运行时启动进程。
一次容器重启通常不重建 Sandbox
在 Sandbox 仍可用时,某个业务容器退出,kubelet 可以只为该容器调用新的 CreateContainer / StartContainer。原来的 Pod UID、Sandbox ID 和其他业务容器 ID 都可以保持不变,退出容器则获得新的 ID。
与此相对,computePodActions 判定 Sandbox 需要重建时,SyncPod 先停止相关运行环境,再根据 Pod 的状态和重启规则安排重新创建。源码中 CreateSandbox 与 ContainersToStart 是不同的动作。已有 Sandbox 失效、节点或运行时恢复后的具体处理,不能从一次应用容器退出推导出来。
容器不断失败时还会触发启动退避。CrashLoopBackOff 表达的是 kubelet 当前暂缓重启的状态,不是一个独立的底层错误原因;还应继续读最近一次终止状态、上次日志以及事件。
用三种证据区分运行环境与应用
实验在 zj 的专用 kind 集群 article-resources 中运行,使用预载的 python:3.12-alpine。配置、脚本、证据和复现说明见 k8sdev/article-resources-runtime。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。
局部重启的检查同时比较 Pod UID、peer 容器和 Sandbox,避免把整个 Pod 重建误判为单容器重启。
第一组 Pod 有两个普通容器:server 监听 8080,peer 保持运行。脚本分别读取两个容器的 /proc/self/ns/net,并在 peer 内请求 http://127.0.0.1:8080。观察到网络命名空间标识相同,HTTP 返回 200。两项证据分别对应“共享同一个 netns”和“应用确实可通过 localhost 通信”,单看一个 Pod IP 不足以替代后一个检查。
第二步向 server 的 Python 主进程发送它能够处理的 SIGINT,等待 kubelet 重新启动 server。校验同时比较 Pod UID、两个容器 ID、server 重启次数以及通过 CRI 列出的 Sandbox ID:server 获得新容器 ID,peer 容器 ID、Pod UID 和 Sandbox ID 保持不变。这里验证的是应用容器局部重启,没有主动破坏 Sandbox 或网络插件。
第三组 Pod 使用同一个已存在镜像,命令指定为不存在的 /does-not-exist。实际运行时错误包含:
| |
该 Pod 的 Sandbox 已经处于 SANDBOX_READY,业务容器却没有成功运行。在本文版本的实现中,失败发生于 CRI StartContainer 内创建底层任务的阶段,不能仅凭错误文字中的 “create” 就归类为 Kubernetes 的 CreateContainerError。多次失败后,还可能观察到 CrashLoopBackOff。
根据失败阶段收敛排障范围
| 观察到的状态或事件 | 接下来需要核对的事实 |
|---|---|
Pending 且尚无 spec.nodeName | 调度条件、资源请求、绑定是否完成 |
FailedCreatePodSandBox | RuntimeClass、Sandbox 镜像、运行时与 CNI 错误 |
ErrImagePull / ImagePullBackOff | 镜像地址、凭据、仓库连通性、拉取策略 |
CreateContainerConfigError | Secret、ConfigMap、安全上下文与容器配置生成错误 |
RunContainerError | 可执行文件、挂载、权限和底层运行时启动错误 |
CrashLoopBackOff | 最近终止原因、退出码、上次容器日志与启动探针 |
| Running 但不 Ready | readiness 状态、应用监听与依赖;不能只查启动链 |
kubectl get pods 的 STATUS 是便于展示的摘要,ContainerCreating 也不是 Pod phase。需要把 API 条件、事件、运行时状态和具体进程关联起来,而不是把一个展示字符串当作完整的状态机。