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

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,再决定哪些空间可以安全释放。

参考资料

相关推荐
广州灵眸科技有限公司1 小时前
灵眸科技EAI3572-Core-L核心板即将发布!八核+4TOPS NPU,面向工业与边缘AI
linux·运维·服务器·数据库·yolo
流浪0011 小时前
Linux系统篇28——通信(五):信号量专篇:从电影院订票到内核一张大表,把信号量一次讲透
linux·运维·服务器
张洛闻Eren1 小时前
云原生k8s【第七课】:集群监控与可观测性
linux·运维·docker·云原生·容器·kubernetes·k8s
青禾8379 小时前
Linux 操作系统入门指南:从目录结构到常用命令
linux·运维·服务器
zhengqweasd10 小时前
流量没跑满、带宽空闲,业务依然卡顿
运维·服务器·网络
Y38153266212 小时前
Docker Compose 服务依赖与健康检查:healthcheck 让容器按正确顺序启动
运维·docker·容器
稳联技术老娜12 小时前
伺服内嵌EtherNet IP方案:嵌入式小板SPI通讯固件Demo程序适配改造
运维·服务器·网络
en.en..12 小时前
Linux系统编程:fcntl 与 ioctl
linux·运维·服务器
2601_9652354813 小时前
快速了解docker
docker·容器·dubbo