2. 镜像拉取基础原理:Docker/containerd 镜像分层结构、Manifest、Blob 存储与拉取流程

在分析 Deployment 镜像拉取性能瓶颈之前,必须深入理解镜像的底层存储与传输机制。Docker 与 containerd 均遵循 OCI (Open Container Initiative) 镜像规范,其核心设计围绕分层 (Layer)、清单 (Manifest) 与 内容寻址 Blob 展开。本节将拆解这三个核心概念,并完整呈现一次镜像拉取的端到端流程。

2.1 OCI 镜像分层结构

OCI 镜像由一组只读的层 (Layer) 堆叠而成,每一层代表 Dockerfile 中的一条指令(如 RUN、COPY、ADD)。层与层之间通过 Union File System (UnionFS) 技术(如 overlay2、aufs)合并为容器的根文件系统。

关键设计原则: 层是共享且可缓存的。同一宿主机上多个容器或镜像可以共用相同的底层,大幅减少磁盘占用与拉取时间。
层级概念 说明 存储标识
Base Layer (基础层) 镜像最底层,通常为操作系统基础文件(如 ubuntu:22.04 的 rootfs) 内容哈希 (sha256)
Intermediate Layer (中间层) Dockerfile 中每条指令产生的增量文件变更 内容哈希 (sha256)
Top Layer (顶层/可写层) 容器运行时创建的可写层,容器销毁时丢弃 临时 UUID
OCI 镜像分层堆叠结构 Layer 0: Base (ubuntu:22.04) Layer 1: apt update & install Layer 2: COPY app.jar Layer 3: RUN useradd Container Writable Layer 只读层,内容寻址 临时,容器删除即消失 UnionFS 合并视图 overlay2 将各层 合并为单一 rootfs 上层覆盖下层同名文件

2.2 Manifest 与 Blob 存储

镜像仓库 (Registry) 中存储的并非一个完整的“镜像文件”,而是由 Manifest (清单) 和一组 Blob (二进制大对象) 组成。Manifest 是镜像的元数据索引,Blob 是实际的数据内容。

2.2.1 Manifest (清单)

Manifest 是一个 JSON 文档,描述了镜像的配置信息以及组成该镜像的所有层的引用。Docker 目前主要使用 Manifest V2 Schema 2 格式。

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
  "config": {
    "mediaType": "application/vnd.docker.container.image.v1+json",
    "size": 7023,
    "digest": "sha256:fc35a...a8c4"
  },
  "layers": [
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 32654,
      "digest": "sha256:2edce...c3a9"
    },
    {
      "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
      "size": 16712,
      "digest": "sha256:9b1b4...e7f2"
    }
  ]
}
Manifest 字段 作用 性能影响
config 镜像配置(环境变量、入口点、历史等) 小文件,拉取快,但解析耗时
layers[].digest 每一层的内容哈希 (SHA256),用于内容寻址与完整性校验 决定层是否已缓存,直接影响拉取量
layers[].size 每一层压缩后的大小 决定网络传输耗时与解压耗时

2.2.2 Blob 存储与内容寻址

每个 Layer 和 Config 在 Registry 中都以 Blob 形式存储,其唯一标识符是 Digest (摘要),即 sha256:<64位十六进制哈希>。这种设计带来以下优势:

  • 去重: 相同内容的层在不同镜像间共享同一个 Blob,Registry 只需存储一份。
  • 完整性: 拉取后客户端重新计算哈希,与 Manifest 中的 Digest 比对,防止数据损坏。
  • 不可变性: Blob 一旦上传即不可修改,避免版本混乱。
容量规划启示: 在 Kubernetes 集群中,如果多个 Deployment 使用相同的基础镜像(如 python:3.11-slim),该基础层只需在每台 Node 上下载一次。这是镜像拉取性能优化的第一杠杆——提高基础层复用率。

2.3 镜像拉取完整流程

当 kubelet 通过 CRI (Container Runtime Interface) 调用 containerd 或 Docker 拉取镜像时,背后经历以下 7 个关键步骤:

  1. 解析镜像引用: 将 nginx:1.25 解析为 registry-1.docker.io/library/nginx:1.25,获取 Registry 地址与 Tag。
  2. 获取 Manifest: 向 Registry 请求 /v2/<name>/manifests/<tag>,返回镜像的 Manifest JSON。
  3. 解析 Manifest: 客户端解析 Manifest,提取 Config Digest 和所有 Layer Digest 列表。
  4. 检查本地缓存: 遍历 Layer Digest,查询本地 /var/lib/containerd/io.containerd.content.v1.content 或 Docker 的 overlay2 目录是否已存在该 Blob。
  5. 并发拉取缺失层: 对于未缓存的层,客户端并发发起 HTTP GET 请求,从 Registry 下载压缩后的 Blob(通常为 .tar.gz)。
  6. 解压与解包: 下载完成后,将压缩的 Blob 解压为 tar 包,再解包为文件系统变更集,写入本地存储。
  7. 挂载与生效: containerd 将新拉取的层与已有层通过 overlay2 挂载,生成完整的 rootfs,并启动容器。
镜像拉取端到端流程 (7 步) 1. kubelet 发起拉取 2. 解析镜像引用 3. 获取 Manifest 4. 解析 Manifest 5. 检查本地缓存 已缓存 → 跳过 6. 并发拉取缺失层 7. 解压 & 挂载生效 性能瓶颈关键点 • 网络带宽与延迟 • Registry 并发限制 • 磁盘 I/O (解压) • 层缓存命中率

2.4 并发拉取与解压的 I/O 模型

在步骤 6 中,containerd 默认使用 goroutine 池 并发拉取多个缺失层。每个层拉取完成后立即进入解压阶段,形成 下载-解压流水线。这一阶段的性能模型可简化为:

总拉取时间 ≈ max( 网络传输时间 , 解压时间 ) × 层数 / 并发度
常见陷阱: 当网络带宽充足时,瓶颈会转移到磁盘 I/O。解压大量小文件(如 apt install 产生的 /usr/share/doc 目录)会导致高 inode 压力与随机写延迟。在 HDD 或 I/O 受限的节点上,解压时间可能超过网络传输时间。

2.5 关键性能指标 (KPI) 速查

指标 定义 影响阶段 典型阈值
层缓存命中率 本地已缓存层数 / 总层数 步骤 5 > 80% 为优
Manifest 拉取延迟 从请求到收到 Manifest 的耗时 步骤 3 < 200ms
层下载吞吐量 每秒下载的压缩层字节数 步骤 6 取决于网络带宽
解压速率 每秒解压的未压缩字节数 步骤 7 > 100 MB/s (SSD)
镜像拉取总耗时 从 kubelet 发起至容器就绪 全流程 大型镜像 < 60s