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

文章目录
- [Docker 29 与 containerd image store:镜像为什么会保存两种形态](#Docker 29 与 containerd image store:镜像为什么会保存两种形态)
-
- [1. 先确认当前后端](#1. 先确认当前后端)
- [2. 为什么同一张镜像要保存两种形态](#2. 为什么同一张镜像要保存两种形态)
- [3. content store 是"镜像内容"](#3. content store 是“镜像内容”)
- [4. snapshotter 是"可运行文件系统"](#4. snapshotter 是“可运行文件系统”)
- [5. 所以"接近两倍"不等于重复导入](#5. 所以“接近两倍”不等于重复导入)
- [6. 为什么普通镜像看起来没问题](#6. 为什么普通镜像看起来没问题)
- [7. containerd image store 为什么成为默认](#7. containerd image store 为什么成为默认)
- [8. `/var/lib/containerd` 为什么开始重要](#8.
/var/lib/containerd为什么开始重要) - [9. Docker data-root 也出现了新的边界](#9. Docker data-root 也出现了新的边界)
- [10. 切换 storage backend 不是清理操作](#10. 切换 storage backend 不是清理操作)
- [11. 一张图收敛这个模型](#11. 一张图收敛这个模型)
把一个大型开发镜像迁移到新的 Ubuntu 主机后,有时会看到一个很反直觉的结果:
text
镜像内容大约 27 GiB
Docker 相关磁盘却接近 50 GiB
如果新机器使用 Docker Engine 29+ 的全新安装,这个现象很可能与 containerd image store 有关。
Docker 官方当前文档明确说明:Docker Engine 29.0 及以后版本的全新安装,默认使用 containerd image store;从较早版本升级的系统则可能继续使用传统 graph driver。
1. 先确认当前后端
Docker 官方提供的检查命令是:
bash
docker info -f '{{ .DriverStatus }}'
如果输出类似:
text
[[driver-type io.containerd.snapshotter.v1]]
说明当前正在使用 containerd image store。
Docker Engine 默认使用 containerd 的 overlayfs snapshotter 管理解包后的镜像文件系统。
这意味着两台同样是 Ubuntu 24.04 的机器,因为 Docker 安装历史不同,也可能表现不一样:
text
旧安装升级:可能继续 overlay2
新安装 Docker 29+:默认 containerd image store
2. 为什么同一张镜像要保存两种形态
最直观的理解是:
text
镜像既要"运输",又要"运行"
这两种需求的数据形态不同。
可以类比为:
text
运输形态 运行形态
────────────────── ──────────────────
类似压缩包 类似解压后的目录
适合 pull / push 适合直接挂载
manifest / blob filesystem snapshot
按 digest 管理 snapshotter 管理
Docker 官方对 containerd image store 的磁盘说明非常明确:它会保留压缩形式的 image layers,同时还会把它们解包到磁盘,因此相同镜像通常比传统 storage driver 占更多空间。
3. content store 是"镜像内容"
从 OCI/Registry 角度,一张镜像并不是一个普通 Linux 目录,而是一组内容对象:
text
Image
├── manifest / index
├── config
├── layer blob A
├── layer blob B
└── ...
它们通常使用 digest 标识:
text
sha256:...
这类内容适合:
text
docker pull
docker push
registry 分发
多平台镜像
内容校验
provenance / SBOM
containerd 的 content store 管理的就是这类可分发内容。
保留这些内容的一个直接好处是:push 时不需要每次重新扫描完整根文件系统,再重新制造镜像层。
4. snapshotter 是"可运行文件系统"
但容器真正运行时需要看到:
text
/
├── bin
├── etc
├── lib
├── opt
└── usr
因此 layer 还需要 unpack。
#mermaid-svg-rVl0HZVG2wFh9ZoR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rVl0HZVG2wFh9ZoR .error-icon{fill:#552222;}#mermaid-svg-rVl0HZVG2wFh9ZoR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rVl0HZVG2wFh9ZoR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .marker.cross{stroke:#333333;}#mermaid-svg-rVl0HZVG2wFh9ZoR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rVl0HZVG2wFh9ZoR p{margin:0;}#mermaid-svg-rVl0HZVG2wFh9ZoR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster-label text{fill:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster-label span{color:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster-label span p{background-color:transparent;}#mermaid-svg-rVl0HZVG2wFh9ZoR .label text,#mermaid-svg-rVl0HZVG2wFh9ZoR span{fill:#333;color:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .node rect,#mermaid-svg-rVl0HZVG2wFh9ZoR .node circle,#mermaid-svg-rVl0HZVG2wFh9ZoR .node ellipse,#mermaid-svg-rVl0HZVG2wFh9ZoR .node polygon,#mermaid-svg-rVl0HZVG2wFh9ZoR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .rough-node .label text,#mermaid-svg-rVl0HZVG2wFh9ZoR .node .label text,#mermaid-svg-rVl0HZVG2wFh9ZoR .image-shape .label,#mermaid-svg-rVl0HZVG2wFh9ZoR .icon-shape .label{text-anchor:middle;}#mermaid-svg-rVl0HZVG2wFh9ZoR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .rough-node .label,#mermaid-svg-rVl0HZVG2wFh9ZoR .node .label,#mermaid-svg-rVl0HZVG2wFh9ZoR .image-shape .label,#mermaid-svg-rVl0HZVG2wFh9ZoR .icon-shape .label{text-align:center;}#mermaid-svg-rVl0HZVG2wFh9ZoR .node.clickable{cursor:pointer;}#mermaid-svg-rVl0HZVG2wFh9ZoR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .arrowheadPath{fill:#333333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rVl0HZVG2wFh9ZoR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rVl0HZVG2wFh9ZoR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rVl0HZVG2wFh9ZoR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster text{fill:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR .cluster span{color:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rVl0HZVG2wFh9ZoR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rVl0HZVG2wFh9ZoR rect.text{fill:none;stroke-width:0;}#mermaid-svg-rVl0HZVG2wFh9ZoR .icon-shape,#mermaid-svg-rVl0HZVG2wFh9ZoR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rVl0HZVG2wFh9ZoR .icon-shape p,#mermaid-svg-rVl0HZVG2wFh9ZoR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rVl0HZVG2wFh9ZoR .icon-shape .label rect,#mermaid-svg-rVl0HZVG2wFh9ZoR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rVl0HZVG2wFh9ZoR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rVl0HZVG2wFh9ZoR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rVl0HZVG2wFh9ZoR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} unpack
overlayfs
Content Store
manifest + blobs
Filesystem Snapshots
Container RootFS
Writable Layer
Container Process
snapshotter 管理的是已经准备好用于容器挂载的文件系统状态。
这样执行:
bash
docker run cross-dev:20.04
时,就不需要每次重新解压二十多 GiB 内容。
5. 所以"接近两倍"不等于重复导入
假设镜像内容约:
text
27 GiB
而宿主机相关数据约:
text
50+ GiB
可以先用下面这个模型理解:
text
Docker Image
│
┌─────────┴─────────┐
▼ ▼
image content unpacked snapshot
用于分发和身份 用于运行
但不要机械得出:
text
27 × 2 = 必然 54 GiB
真实数字还会受到:
text
压缩率
层共享
镜像格式
metadata
容器 writable layer
构建缓存
统计口径
影响。
正确说法应该是:
containerd image store 同时保存分发形态和运行形态,因此大型镜像会明显放大磁盘占用,但不是固定的 2 倍公式。
6. 为什么普通镜像看起来没问题
对于 500 MB 或 1 GiB 镜像,多占几百 MB 往往不明显。
但交叉编译开发镜像可能包含:
text
完整 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 官方列出的能力包括:
text
本地保存多平台镜像
attestation / provenance / SBOM
Wasm workload
可插拔 snapshotter
传统 classic image store 对这些现代 OCI 场景支持有限。
所以 containerd image store 的设计目标不只是:
text
"本机能不能运行这一个容器"
而是同时覆盖镜像分发、身份、现代 manifest/index 和运行时文件系统。
8. /var/lib/containerd 为什么开始重要
传统 Docker 环境中,很多人习惯只看:
bash
sudo du -sh /var/lib/docker
但使用 containerd image store 时,Docker 当前文档说明 image contents 和 container snapshots 默认位于:
text
/var/lib/containerd
而其他 Docker daemon 数据仍可能位于:
text
/var/lib/docker
因此新环境应该同时看:
bash
sudo du -sh /var/lib/docker /var/lib/containerd
这也是为什么把旧经验直接套到 Docker 29 新安装上,会得到错误判断。
9. Docker data-root 也出现了新的边界
过去常见做法是在 /etc/docker/daemon.json 中配置:
json
{
"data-root": "/mnt/docker-data"
}
把 Docker 数据迁到大磁盘。
但 Docker 当前官方文档特别提醒:使用 containerd image store 时,Docker data-root 不会自动改变 containerd image/snapshot 的存储位置。
于是可能出现:
text
/var/lib/docker -> 已迁到大盘
/var/lib/containerd -> 仍在根分区
然后根分区还是不断变满。
对于大型开发镜像,这是一个必须提前了解的变化。
10. 切换 storage backend 不是清理操作
有人看到 containerd image store 更占空间,会想到切回 overlay2。
需要注意 Docker 官方说明:切换 storage backend 会让另一套 backend 中已经存在的镜像和容器暂时不可见,但数据仍然留在磁盘。
也就是说:
text
镜像"看不见"
不等于
镜像数据"被删除"
如果只是为了腾磁盘而来回切换后端,反而可能让系统中同时保留两套数据,更难排查。
因此后端切换应该被当作存储架构选择,而不是 prune 的替代品。
11. 一张图收敛这个模型
#mermaid-svg-xfN1IWkH7tLbAvs9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-xfN1IWkH7tLbAvs9 .error-icon{fill:#552222;}#mermaid-svg-xfN1IWkH7tLbAvs9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-xfN1IWkH7tLbAvs9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .marker.cross{stroke:#333333;}#mermaid-svg-xfN1IWkH7tLbAvs9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-xfN1IWkH7tLbAvs9 p{margin:0;}#mermaid-svg-xfN1IWkH7tLbAvs9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster-label text{fill:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster-label span{color:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster-label span p{background-color:transparent;}#mermaid-svg-xfN1IWkH7tLbAvs9 .label text,#mermaid-svg-xfN1IWkH7tLbAvs9 span{fill:#333;color:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .node rect,#mermaid-svg-xfN1IWkH7tLbAvs9 .node circle,#mermaid-svg-xfN1IWkH7tLbAvs9 .node ellipse,#mermaid-svg-xfN1IWkH7tLbAvs9 .node polygon,#mermaid-svg-xfN1IWkH7tLbAvs9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .rough-node .label text,#mermaid-svg-xfN1IWkH7tLbAvs9 .node .label text,#mermaid-svg-xfN1IWkH7tLbAvs9 .image-shape .label,#mermaid-svg-xfN1IWkH7tLbAvs9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-xfN1IWkH7tLbAvs9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .rough-node .label,#mermaid-svg-xfN1IWkH7tLbAvs9 .node .label,#mermaid-svg-xfN1IWkH7tLbAvs9 .image-shape .label,#mermaid-svg-xfN1IWkH7tLbAvs9 .icon-shape .label{text-align:center;}#mermaid-svg-xfN1IWkH7tLbAvs9 .node.clickable{cursor:pointer;}#mermaid-svg-xfN1IWkH7tLbAvs9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .arrowheadPath{fill:#333333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xfN1IWkH7tLbAvs9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-xfN1IWkH7tLbAvs9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xfN1IWkH7tLbAvs9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster text{fill:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 .cluster span{color:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-xfN1IWkH7tLbAvs9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-xfN1IWkH7tLbAvs9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-xfN1IWkH7tLbAvs9 .icon-shape,#mermaid-svg-xfN1IWkH7tLbAvs9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-xfN1IWkH7tLbAvs9 .icon-shape p,#mermaid-svg-xfN1IWkH7tLbAvs9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-xfN1IWkH7tLbAvs9 .icon-shape .label rect,#mermaid-svg-xfN1IWkH7tLbAvs9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-xfN1IWkH7tLbAvs9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-xfN1IWkH7tLbAvs9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-xfN1IWkH7tLbAvs9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} unpack
Registry / Image Archive
containerd Content Store
containerd Snapshotter
Container RootFS
/var/lib/containerd
真正需要记住的是:
text
content store:镜像作为可分发内容的形态
snapshotter:镜像作为可运行文件系统的形态
containerd image store 会让这两类数据同时存在。
所以一个 20+ GiB 的开发镜像迁移到 Docker Engine 29+ 新主机后,看到 /var/lib/containerd 迅速增长并不一定是异常。
下一步应该先区分 image content、snapshot 和 BuildKit cache,再决定哪些空间可以安全释放。
参考资料
- Docker Docs:containerd image store with Docker Engine --- https://docs.docker.com/engine/storage/containerd/
- Docker Docs:Storage drivers --- https://docs.docker.com/engine/storage/drivers/
- Docker Engine 29 release notes --- https://docs.docker.com/engine/release-notes/29/