3. 镜像拉取原理

容器镜像的拉取速度直接决定了 Pod 冷启动的延迟。要理解预热为何能加速、削峰为何有效,必须先深入镜像的底层存储模型与拉取协议。本节从分层结构、Manifest 清单、Blob 存储和完整拉取流程四个维度拆解镜像拉取原理。

3.1 镜像分层结构:只读层的堆叠与共享

Docker 与 containerd 均遵循 OCI(Open Container Initiative)镜像规范。镜像由一组只读层(Layer)堆叠而成,每一层代表 Dockerfile 中的一条指令(如 RUN、COPY、ADD)。

  • 基础层(Base Layer):通常来自 FROM 指令,如 ubuntu:22.04,是整个镜像的根文件系统。
  • 增量层(Diff Layer):后续每条指令生成一个增量层,仅保存与上一层的差异文件。
  • 容器层(Container Layer):运行容器时,在只读层之上叠加一个可写层(upperdir),所有写操作写入该层,遵循写时复制(CoW)策略。
关键指标: 同一宿主机上,多个容器共享相同的基础层和中间层,磁盘占用仅一份,拉取时也只需拉取本地缺失的层。
预热原理: 预热工具(如 kubelet image pull 或 spegel)提前将各层 Blob 下载到节点本地,使 Pod 调度时无需等待远程 Registry 响应,实现秒级启动。

3.2 Manifest 清单:镜像的元数据索引

Manifest 是镜像的“目录文件”,描述了镜像包含哪些层、各层的摘要(Digest)、大小、媒体类型以及配置信息。OCI 规范定义了两种 Manifest 格式:

Manifest 类型 适用场景 核心字段
Image Manifest (V2) 单架构镜像 config、layers[](每个 layer 包含 digest、size、mediaType)
Image Index (OCI Index) 多架构镜像(如 amd64 + arm64) manifests[](每个 manifest 指向一个平台特定的 Image Manifest)

拉取时,客户端首先通过 tag 或 digest 获取 Manifest,解析出所有层的 Digest 列表,然后按顺序拉取每一层。

3.3 Blob 存储:内容寻址与去重

每一层在 Registry 中存储为一个 Blob(二进制大对象),其唯一标识符是 Digest(sha256:xxxx)。Blob 存储的核心特性:

  • 内容寻址:Digest 由内容哈希计算得出,内容不变则 Digest 不变,天然支持去重。
  • 不可变性:Blob 一旦上传即不可修改,只能通过新 Digest 发布新版本。
  • 压缩存储:Registry 通常存储 gzip 压缩后的 Blob,拉取时按需解压。
避坑提示: 如果镜像层数过多(超过 50 层),Manifest 体积增大,拉取时解析时间线性增长。建议合并 RUN 指令,减少层数。

3.4 镜像拉取流程:从 Registry 到本地存储

以下 SVG 图展示了 containerd 从 Registry 拉取镜像的完整流程,包含认证、Manifest 获取、Blob 并行下载、解压与挂载。

镜像拉取流程:Registry → containerd → 本地存储 镜像拉取流程(containerd 视角) 1. ctr / crictl pull 2. Registry 认证(Bearer ) 3. GET /v2/<name>/manifests/<tag> 4. 解析 Manifest → 层 Digest 列表 5. 并行 GET /v2/<name>/blobs/<digest> 6. 解压 → 挂载到 overlayfs 7. 镜像可用(本地缓存) 关键加速点 • 并行拉取 Blob(步骤 5) • 本地缓存命中跳过下载 • 预热提前完成步骤 3-6

3.5 拉取流程关键步骤详解

  1. 认证握手:客户端向 Registry 发起 GET /v2/,Registry 返回 401 Unauthorized 并携带 Bearer realm,客户端使用凭证获取 。
  2. Manifest 获取:客户端发送 GET /v2/<name>/manifests/<tag>,Registry 返回 JSON 格式的 Manifest,包含 layers[] 数组。
  3. 层依赖解析:客户端按 layers 顺序遍历,检查本地 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs 是否已存在相同 Digest 的 Blob。
  4. 并行下载:对缺失的层,containerd 使用 goroutine 池 并发发起 GET /v2/<name>/blobs/<digest> 请求,默认并发数为 3(可通过 max_concurrent_downloads 调整)。
  5. 解压与校验:下载完成后,计算 Blob 的 SHA256 摘要与 Manifest 中的 Digest 比对,一致后解压为 tar 格式,写入 snapshotter 的挂载目录。
  6. overlayfs 挂载:containerd 将所有层按顺序挂载为 overlayfs 的 lowerdir,最终呈现为完整的根文件系统。

3.6 拉取性能对比:分层 vs 扁平镜像

维度 分层镜像(推荐) 扁平镜像(单层)
拉取时间 可并行下载,仅拉取增量层 必须串行下载整个大 Blob
磁盘占用 共享基础层,多容器节省空间 每个容器独立存储完整文件系统
缓存命中率 基础层变更少,缓存命中率高 任何修改都导致整个镜像缓存失效
预热效率 只需预热缺失的增量层 必须预热整个镜像
实践建议: 在 Backstage 插件或 CI/CD 流水线中,优先使用 docker pull --platform 指定架构,避免拉取多架构索引中不必要的 Manifest。预热时使用 containerd 的 ctr image pull 命令,可跳过 Docker Engine 的额外开销。

3.7 冷启动削峰的关键:Blob 本地缓存

当大量 Pod 同时调度到同一节点时,若镜像层未缓存,Registry 会承受巨大的并发拉取压力,导致带宽瓶颈和拉取超时。预热策略的核心就是提前将 Blob 缓存到节点本地,使拉取流程中的步骤 3-6 几乎零耗时。

以下 SVG 图对比了无预热与有预热场景下的拉取时间线:

冷启动削峰:无预热 vs 有预热拉取时间线 冷启动削峰对比:无预热 vs 有预热 无预热: 认证 Manifest 并行拉取 Blob(网络延迟 + 带宽瓶颈) 解压 挂载 ~15s 有预热: 认证 Manifest 本地命中 解压 挂载 ~2s 预热效果:拉取时间从 ~15s 降至 ~2s,削峰比约 7.5x 预热工具提前将 Blob 缓存到节点,Pod 启动时跳过网络下载,仅执行本地解压与挂载

3.8 小结

镜像拉取的核心瓶颈在于 Blob 网络下载。理解分层结构、Manifest 索引和并行拉取机制后,可以精准设计预热策略:

  • 预热时优先拉取 基础层 和 公共层,最大化共享缓存。
  • 使用 Digest 引用 而非 Tag,避免因 Tag 变更导致缓存失效。
  • 在 Backstage 插件中集成预热调度,在集群低负载时段提前拉取热门镜像。