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 |
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 一旦上传即不可修改,避免版本混乱。
python:3.11-slim),该基础层只需在每台 Node 上下载一次。这是镜像拉取性能优化的第一杠杆——提高基础层复用率。
2.3 镜像拉取完整流程
当 kubelet 通过 CRI (Container Runtime Interface) 调用 containerd 或 Docker 拉取镜像时,背后经历以下 7 个关键步骤:
- 解析镜像引用: 将
nginx:1.25解析为registry-1.docker.io/library/nginx:1.25,获取 Registry 地址与 Tag。 - 获取 Manifest: 向 Registry 请求
/v2/<name>/manifests/<tag>,返回镜像的 Manifest JSON。 - 解析 Manifest: 客户端解析 Manifest,提取 Config Digest 和所有 Layer Digest 列表。
- 检查本地缓存: 遍历 Layer Digest,查询本地
/var/lib/containerd/io.containerd.content.v1.content或 Docker 的 overlay2 目录是否已存在该 Blob。 - 并发拉取缺失层: 对于未缓存的层,客户端并发发起 HTTP GET 请求,从 Registry 下载压缩后的 Blob(通常为
.tar.gz)。 - 解压与解包: 下载完成后,将压缩的 Blob 解压为 tar 包,再解包为文件系统变更集,写入本地存储。
- 挂载与生效: containerd 将新拉取的层与已有层通过 overlay2 挂载,生成完整的 rootfs,并启动容器。
2.4 并发拉取与解压的 I/O 模型
在步骤 6 中,containerd 默认使用 goroutine 池 并发拉取多个缺失层。每个层拉取完成后立即进入解压阶段,形成 下载-解压流水线。这一阶段的性能模型可简化为:
总拉取时间 ≈ max( 网络传输时间 , 解压时间 ) × 层数 / 并发度
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 |