Docker 29 与 containerd image store:镜像为什么会保存两种形态

摘要:从 content store 与 snapshotter 两个角色解释 Docker Engine 29 的 containerd image store,以及大型镜像为什么比传统 overlay2 更占磁盘。

在这里插入图片描述

@[toc]

把一个大型开发镜像迁移到新的 Ubuntu 主机后,有时会看到一个很反直觉的结果:

1
2
镜像内容大约 27 GiB
Docker 相关磁盘却接近 50 GiB

如果新机器使用 Docker Engine 29+ 的全新安装,这个现象很可能与 containerd image store 有关。

Docker 官方当前文档明确说明:Docker Engine 29.0 及以后版本的全新安装,默认使用 containerd image store;从较早版本升级的系统则可能继续使用传统 graph driver。

1. 先确认当前后端

Docker 官方提供的检查命令是:

1
docker info -f '{{ .DriverStatus }}'

如果输出类似:

1
[[driver-type io.containerd.snapshotter.v1]]

说明当前正在使用 containerd image store。

Docker Engine 默认使用 containerd 的 overlayfs snapshotter 管理解包后的镜像文件系统。

这意味着两台同样是 Ubuntu 24.04 的机器,因为 Docker 安装历史不同,也可能表现不一样:

1
2
旧安装升级:可能继续 overlay2
新安装 Docker 29+:默认 containerd image store

2. 为什么同一张镜像要保存两种形态

最直观的理解是:

1
镜像既要“运输”,又要“运行”

这两种需求的数据形态不同。

可以类比为:

1
2
3
4
5
6
运输形态                  运行形态
────────────────── ──────────────────
类似压缩包 类似解压后的目录
适合 pull / push 适合直接挂载
manifest / blob filesystem snapshot
按 digest 管理 snapshotter 管理

Docker 官方对 containerd image store 的磁盘说明非常明确:它会保留压缩形式的 image layers,同时还会把它们解包到磁盘,因此相同镜像通常比传统 storage driver 占更多空间。

3. content store 是“镜像内容”

从 OCI/Registry 角度,一张镜像并不是一个普通 Linux 目录,而是一组内容对象:

1
2
3
4
5
6
Image
├── manifest / index
├── config
├── layer blob A
├── layer blob B
└── ...

它们通常使用 digest 标识:

1
sha256:...

这类内容适合:

1
2
3
4
5
6
docker pull
docker push
registry 分发
多平台镜像
内容校验
provenance / SBOM

containerd 的 content store 管理的就是这类可分发内容。

保留这些内容的一个直接好处是:push 时不需要每次重新扫描完整根文件系统,再重新制造镜像层。

4. snapshotter 是“可运行文件系统”

但容器真正运行时需要看到:

1
2
3
4
5
6
/
├── bin
├── etc
├── lib
├── opt
└── usr

因此 layer 还需要 unpack。

1
2
3
4
5
flowchart LR
Content["Content Store\nmanifest + blobs"] -->|"unpack"| Snapshot["Filesystem Snapshots"]
Snapshot -->|"overlayfs"| RootFS["Container RootFS"]
RootFS --> Writable["Writable Layer"]
Writable --> Process["Container Process"]

snapshotter 管理的是已经准备好用于容器挂载的文件系统状态。

这样执行:

1
docker run cross-dev:20.04

时,就不需要每次重新解压二十多 GiB 内容。

5. 所以“接近两倍”不等于重复导入

假设镜像内容约:

1
27 GiB

而宿主机相关数据约:

1
50+ GiB

可以先用下面这个模型理解:

1
2
3
4
5
6
           Docker Image

┌─────────┴─────────┐
▼ ▼
image content unpacked snapshot
用于分发和身份 用于运行

但不要机械得出:

1
27 × 2 = 必然 54 GiB

真实数字还会受到:

1
2
3
4
5
6
7
压缩率
层共享
镜像格式
metadata
容器 writable layer
构建缓存
统计口径

影响。

正确说法应该是:

containerd image store 同时保存分发形态和运行形态,因此大型镜像会明显放大磁盘占用,但不是固定的 2 倍公式。

6. 为什么普通镜像看起来没问题

对于 500 MB 或 1 GiB 镜像,多占几百 MB 往往不明显。

但交叉编译开发镜像可能包含:

1
2
3
4
5
6
完整 Ubuntu 用户空间
20+ GiB Yocto SDK
目标 sysroot
交叉 GCC / Binutils
头文件和库
CMake / Python / Thrift

当单镜像已经达到二三十 GiB 时,存储模型中的额外一份形态会直接放大成几十 GiB。

所以问题并不是 containerd 对普通镜像“突然失控”,而是大型 SDK 把差异放大了。

7. containerd image store 为什么成为默认

Docker Engine 29 选择 containerd image store,并不仅仅是为了替换 overlay2 目录结构。

Docker 官方列出的能力包括:

1
2
3
4
本地保存多平台镜像
attestation / provenance / SBOM
Wasm workload
可插拔 snapshotter

传统 classic image store 对这些现代 OCI 场景支持有限。

所以 containerd image store 的设计目标不只是:

1
“本机能不能运行这一个容器”

而是同时覆盖镜像分发、身份、现代 manifest/index 和运行时文件系统。

8. /var/lib/containerd 为什么开始重要

传统 Docker 环境中,很多人习惯只看:

1
sudo du -sh /var/lib/docker

但使用 containerd image store 时,Docker 当前文档说明 image contents 和 container snapshots 默认位于:

1
/var/lib/containerd

而其他 Docker daemon 数据仍可能位于:

1
/var/lib/docker

因此新环境应该同时看:

1
sudo du -sh /var/lib/docker /var/lib/containerd

这也是为什么把旧经验直接套到 Docker 29 新安装上,会得到错误判断。

9. Docker data-root 也出现了新的边界

过去常见做法是在 /etc/docker/daemon.json 中配置:

1
2
3
{
"data-root": "/mnt/docker-data"
}

把 Docker 数据迁到大磁盘。

但 Docker 当前官方文档特别提醒:使用 containerd image store 时,Docker data-root 不会自动改变 containerd image/snapshot 的存储位置。

于是可能出现:

1
2
/var/lib/docker      -> 已迁到大盘
/var/lib/containerd -> 仍在根分区

然后根分区还是不断变满。

对于大型开发镜像,这是一个必须提前了解的变化。

10. 切换 storage backend 不是清理操作

有人看到 containerd image store 更占空间,会想到切回 overlay2。

需要注意 Docker 官方说明:切换 storage backend 会让另一套 backend 中已经存在的镜像和容器暂时不可见,但数据仍然留在磁盘。

也就是说:

1
2
3
镜像“看不见”
不等于
镜像数据“被删除”

如果只是为了腾磁盘而来回切换后端,反而可能让系统中同时保留两套数据,更难排查。

因此后端切换应该被当作存储架构选择,而不是 prune 的替代品。

11. 一张图收敛这个模型

1
2
3
4
5
6
flowchart TB
Registry["Registry / Image Archive"] --> Content["containerd Content Store"]
Content -->|"unpack"| Snapshot["containerd Snapshotter"]
Snapshot --> Container["Container RootFS"]
Content --> Disk["/var/lib/containerd"]
Snapshot --> Disk

真正需要记住的是:

1
2
content store:镜像作为可分发内容的形态
snapshotter:镜像作为可运行文件系统的形态

containerd image store 会让这两类数据同时存在。

所以一个 20+ GiB 的开发镜像迁移到 Docker Engine 29+ 新主机后,看到 /var/lib/containerd 迅速增长并不一定是异常。

下一步应该先区分 image content、snapshot 和 BuildKit cache,再决定哪些空间可以安全释放。

参考资料