4. 镜像拉取原理:kubelet 与 CRI、containerd/CRI-O 的交互流程
理解镜像拉取的完整链路,是进行容量规划与拉取加速优化的前提。当用户通过 kubectl run 或 Deployment 创建 Pod 时,kubelet 并不会直接调用容器运行时去拉取镜像,而是通过一套标准化的接口协议——CRI(Container Runtime Interface),与底层的 containerd 或 CRI-O 进行交互。整个流程涉及多个组件的有序协作,任何一个环节的瓶颈都会直接影响 Pod 启动速度。
4.1 核心组件与角色定义
在深入流程之前,先明确参与镜像拉取的四个关键角色:
| 组件 | 角色 | 职责 |
|---|---|---|
| kubelet | 节点代理 | 监听 API Server,管理 Pod 生命周期,调用 CRI 创建/删除容器 |
| CRI shim(containerd/CRI-O) | CRI 实现层 | 实现 CRI 接口(gRPC),将 kubelet 请求翻译为运行时操作 |
| containerd / CRI-O 守护进程 | 容器运行时核心 | 管理镜像存储层、容器生命周期、快照(snapshotter) |
| 镜像仓库(Registry) | 镜像存储 | 提供镜像层(layer)的拉取与认证服务 |
关键认知: kubelet 并不关心底层是 containerd 还是 CRI-O,它只通过 CRI 的
ImageService 和 RuntimeService 两个 gRPC 服务进行通信。这种解耦设计使得 Kubernetes 可以灵活替换容器运行时。
4.2 镜像拉取完整交互流程
从 kubelet 发起 Pod 创建请求到镜像层全部落盘,共经历以下 7 个核心步骤:
- kubelet 触发 SyncPod:kubelet 的 Pod 管理工作循环检测到新 Pod 需要调度到本节点,调用
SyncPod方法。 - kubelet 调用 CRI ImageService.PullImage:kubelet 根据 Pod Spec 中的
image字段,构造PullImageRequest,通过 gRPC 发送给 CRI shim。 - CRI shim 解析请求:containerd 或 CRI-O 的 CRI 插件接收请求,解析镜像名称、标签、认证信息(imagePullSecrets)。
- CRI shim 调用底层运行时 PullImage:将解析后的请求转发给 containerd 核心或 CRI-O 的镜像管理模块。
- 镜像层下载与解压:运行时根据 manifest 获取镜像层列表,并发或顺序下载每一层(layer),并解压到 snapshotter 管理的快照目录。
- 返回镜像引用:下载完成后,运行时返回镜像的 digest 和元数据给 CRI shim。
- CRI shim 返回 PullImageResponse:kubelet 收到响应,确认镜像已就绪,继续执行后续的容器创建流程。
4.3 CRI 接口详解:ImageService 与 RuntimeService
CRI 定义了两种核心 gRPC 服务,镜像拉取属于 ImageService 的范畴:
| 服务 | 关键方法 | 用途 |
|---|---|---|
| ImageService | PullImage、ListImages、ImageStatus、RemoveImage |
镜像拉取、查询、删除 |
| RuntimeService | RunPodSandbox、CreateContainer、StartContainer |
Pod 沙箱与容器生命周期管理 |
kubelet 在 SyncPod 中会先调用 PullImage,确保镜像存在后,再调用 CreateContainer 和 StartContainer。如果镜像已经存在于节点本地缓存,kubelet 会跳过拉取步骤,直接使用已有镜像。
4.4 containerd 与 CRI-O 的内部实现差异
虽然两者都实现了 CRI 接口,但在镜像拉取的内部实现上存在关键差异,这些差异会影响拉取性能和资源消耗:
| 维度 | containerd | CRI-O |
|---|---|---|
| 镜像存储后端 | 自研 content store + snapshotter | 直接使用 containers/storage(与 Podman 共享) |
| 镜像拉取并发度 | 默认并发拉取所有层(可配置) | 串行拉取层,但支持并行拉取不同镜像 |
| 解压方式 | 使用 containerd 自带的解压器 | 调用系统 tar/gzip 命令 |
| 镜像缓存策略 | 基于 content-based 寻址,自动去重 | 基于引用计数,支持镜像分层缓存 |
| 对 CRI 的响应 | 通过 CRI plugin 嵌入 containerd 进程 | 独立进程 crio 实现 CRI 接口 |
容量规划启示: containerd 的并发拉取策略在带宽充足时能显著缩短拉取时间,但会瞬时消耗大量网络和磁盘 I/O。CRI-O 的串行拉取更温和,适合 I/O 受限的节点。在规划节点资源预留时,需要根据所选运行时调整网络带宽和磁盘 IOPS 的预留量。
4.5 镜像拉取的关键性能瓶颈点
从交互流程中可以识别出以下四个主要瓶颈,它们直接影响 Pod 启动速度和节点资源消耗:
- 网络带宽与延迟:镜像层从 Registry 下载的速度受限于节点出口带宽和 Registry 的响应能力。大镜像(>1GB)在带宽不足时可能成为 Pod 启动的瓶颈。
- 磁盘 I/O 与解压性能:镜像层下载后需要解压并写入 snapshotter 目录。使用 HDD 或 IOPS 受限的磁盘时,解压和写入会成为瓶颈。
- 镜像层去重与缓存命中率:如果节点上多个 Pod 使用相同的基础镜像层,containerd/CRI-O 的 content-based 寻址可以避免重复下载。缓存命中率低时,每次拉取都需要完整下载。
- Registry 认证与限流:频繁的拉取请求可能触发 Registry 的速率限制(rate limit),导致拉取失败或排队等待。
实战注意: 在容量规划时,不要只关注 CPU 和内存。镜像拉取阶段的网络带宽和磁盘 IOPS 同样需要预留。建议为每个节点预留至少 100Mbps 的带宽用于镜像拉取,并优先使用 SSD 作为容器镜像存储盘。
4.6 镜像拉取流程中的关键数据结构
kubelet 发送给 CRI shim 的 PullImageRequest 包含以下关键字段,理解这些字段有助于排查拉取问题:
// PullImageRequest 是 kubelet 通过 CRI 发送给运行时的请求
message PullImageRequest {
// 镜像的完整引用,如 docker.io/library/nginx:1.25
ImageSpec image = 1;
// 认证信息,来自 imagePullSecrets
AuthConfig auth = 2;
// Pod 沙箱配置,用于确定拉取镜像时的平台架构
PodSandboxConfig sandbox_config = 3;
}
// ImageSpec 定义镜像的标识
message ImageSpec {
// 镜像引用字符串
string image = 1;
// 可选的注解,用于传递额外元数据
map<string, string> annotations = 2;
}
// AuthConfig 包含 Registry 认证凭据
message AuthConfig {
string username = 1;
string password = 2;
string auth = 3;
string server_address = 4;
string identity_ = 5;
string registry_ = 6;
}
当 kubelet 调用 PullImage 后,CRI shim 会返回 PullImageResponse,其中包含镜像的唯一标识(digest),用于后续的容器创建引用:
message PullImageResponse {
// 镜像的引用,包含 digest,如 docker.io/library/nginx@sha256:abc123...
string image_ref = 1;
}
4.7 镜像拉取流程的优化切入点
基于上述原理,可以从以下四个方向进行镜像拉取加速:
| 优化方向 | 具体措施 | 影响环节 |
|---|---|---|
| 镜像层缓存 | 部署节点级镜像缓存(如 Harbor 代理缓存、Kraken) | 减少远程 Registry 拉取次数 |
| 并发拉取调优 | 调整 containerd 的 concurrent_downloads 参数 |
提升网络利用率 |
| 镜像分层优化 | 使用多阶段构建,减少镜像层数,合并 RUN 指令 | 减少需要下载的层数量 |
| 预拉取与预热 | 在节点初始化或低负载时预拉取常用镜像 | 避免 Pod 启动时的拉取等待 |
容量规划关联: 镜像拉取加速的最终目的是减少 Pod 启动时间,从而降低节点资源预留的峰值需求。通过缓存和预拉取,可以将镜像拉取的资源消耗从“突发高峰”转变为“平稳持续”,使容量规划更加精确。