news 2026/9/13 15:25:37

containerd 快照(Snapshots)设计解析:从 graphdriver 到独立 Snapshotter 的分层文件系统模型

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
containerd 快照(Snapshots)设计解析:从 graphdriver 到独立 Snapshotter 的分层文件系统模型

containerd 快照(Snapshots)设计解析:从 graphdriver 到独立 Snapshotter 的分层文件系统模型

【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd

导读

本文以 containerd 历史设计文档 docs/historical/design/snapshots.md 为主体,系统讲解 containerd 如何用一套独立的、与镜像/容器解耦的Snapshotter接口替代 Docker 时代深度耦合的graphdriver,实现"层"(layer)的分配、快照与挂载。文中会结合仓库中 Snapshotter 接口定义、Mount 对象实现、overlay 快照插件 与 元数据存储层 的源码,带你理解PrepareViewCommitRemove等核心操作的生命周期,以及导入镜像层、运行容器的完整调用链,最终具备独立阅读快照插件源码与二次开发 Snapshotter 的能力。

背景:从 Docker 的 graphdriver 到分层文件系统

Docker 容器自诞生之初就构建在一种名为层(layers)的快照方法论之上。层提供了一种能力:fork 一个文件系统、做出修改、再把变更集保存为新的层。历史上,这套机制以名为graphdriver的组件被深度集成在 Docker daemon 中,使得 Docker daemon 可以在多个操作系统上运行,同时保持提交和分发镜像变更时大体一致的快照语义。

graphdriver与镜像的导入导出深度绑定,包括管理层之间的关系以及容器运行时文件系统,其行为甚至反向决定了镜像格式的传输方式。设计文档指出,这种紧密耦合带来两个问题:

  • 驱动实现的表面积过大,导致各驱动之间行为不一致;
  • 序列化、哈希、解包、打包、挂载等职责全部堆在一个组件里,难以演进。

基于此,containerd 提出一个更灵活的层管理模型:只提供基础快照功能的最小 API,不耦合镜像结构与标识;用更小的实现表面积,换取各驱动实现之间更一致的行为。

Snapshotter 与 graphdriver 的本质区别

维度graphdriver(Docker)Snapshotter(containerd)
对镜像/容器的认知深度集成,管理层关系与容器运行文件系统无任何认知,只面向"准备目录 + 提交目录"
与 tar 变更集格式的关系深度绑定,参与打包/解包不介入 tar 格式,接口直接提供变更集访问
职责范围序列化、哈希、解包、打包、挂载仅提供面向挂载的快照访问与最小元数据
演进成本重构困难可由现有 graphdriver 重构而来,最小化新代码与测试

正如文档所述:"The best aspect is that we can get to this model by refactoring the existing graphdrivers, minimizing the need for new code and sprawling tests."(最好的地方在于,我们可以通过重构现有 graphdriver 得到这个模型,从而最小化新代码和不断膨胀的测试。)

范围(Scope):只做挂载导向的快照

设计文档明确划定了 Snapshotter 的边界:在 Docker 时代,graphdriver承担了序列化(serialization)、哈希(hashing)、解包(unpacking)、打包(packing)、挂载(mounting)等大量功能;而Snapshotter 只提供挂载导向(mount-oriented)的快照访问,仅携带最小元数据

序列化、哈希、解包、打包和挂载都不在该设计中,取而代之的是各 graphdriver 之间的通用实现,而非专用实现。这对性能影响不大,因为接口提供了对变更集的直接访问

这一边界在今天的仓库中依然清晰可见:core/snapshots只管快照语义,而镜像的解包/打包逻辑分布在 core/images/archive、pkg/archive 等模块;快照插件只负责把"目录树"组织成可挂载的文件系统视图。

架构:以父子关系构建的快照图

Snapshotter 提供了一套 API,用于分配、快照和挂载抽象的、基于层的文件系统。其模型通过构建带父子关系的目录集合(即Snapshots)来工作。

一个 Snapshot 代表一个文件系统状态。每个快照都有一个父快照,空父快照用空字符串""表示。可以在父快照与它的快照之间做 diff,从而生成一个经典的"层"。

快照的生命周期

快照的生命周期最能说明其本质,Snapshotter 接口 的注释中给出了权威定义:

  • 活动快照(Active):总是通过PrepareView从一个已提交快照(包括空快照)创建;
  • 已提交快照(Committed):总是通过Commit从一个活动快照创建;
  • 活动快照永远不会变成已提交快照,反之亦然(KindActive/KindView/KindCommitted在 Kind 枚举 中区分);
  • 所有快照都可以被移除。

挂载活动快照后,可以对其做出修改;提交(Commit)动作会创建一个已提交快照,该已提交快照继承活动快照的父快照,随后可作为新活动快照的父快照。活动快照永远不能作为父快照使用

下面的示意图展示了快照之间的关系:

图中可以清楚看到:

  • 活动快照a通过调用Prepare(以已提交快照P₀为父)创建;
  • 修改后a变为a',再调用Commit创建已提交快照P₁
  • a'可继续修改为a'',再次调用Commit创建第二个已提交快照P₂
  • 注意:P₂的父是P₀,而不是P₁——因为Commit只把活动快照"固化"下来,并不会改变其父指针。

操作(Operations):Prepare / View / Commit / Remove

快照的落地依赖于Mount对象以及由用户定义的、用于不透明数据存储的目录。创建新活动快照时,调用方提供一个名为key的标识符;该操作返回一组 mounts,挂载后即可在挂载路径上得到完全准备好的快照——这就是prepare 操作

以下是四个核心操作的语义,与 Snapshotter 接口 的方法一一对应:

操作接口方法语义
preparePrepare(ctx, key, parent, opts...)key为标识创建活动快照(父为已提交快照或空串""),返回一组可挂载的 mounts;挂载后可写入新数据。相同 key 的多次Prepare/View应失败
viewView(ctx, key, parent, opts...)Prepare行为一致,但返回只读视图(mounts 可能带有只读标志),对底层文件系统的修改将被忽略;禁止对 view 的 key 调用Commit,资源回收必须调用Remove
commitCommit(ctx, name, key, opts...)key代表的活动快照固化为name标识的已提交快照,之后name可作为新活动快照的父;提交后key被移除
removeRemove(ctx, key)释放快照关联的所有资源;调用前应先卸载prepare/view返回的 mounts;删除已提交快照时,必须先删除其所有子快照

只读视图与覆盖层的实现细节

View的只读语义在 core/mount/mount.go 中有具体实现:readonlyMounts对 overlay 类型的 mount,会剥离upperdir=workdir=选项并把 upperdir 追加到 lowerdir 末尾,从而把可写 overlay 转换为只读视图;对其他类型则统一追加ro选项。Mount.ReadOnly()(core/mount/mount.go#L96-L125)还会根据 mount 类型推导只读性,例如 erofs 天生只读、overlay 无upperdir=即只读。

接口中的辅助方法与元数据

除四个核心操作外,Snapshotter 接口 还定义了:

  • Stat:按 key 查询快照的Info,用于父解析、存在性检查与类型判别;
  • Update:更新快照的可变属性(如 labels);
  • Usage:返回快照自身(不含父)的磁盘占用与 inode 数(Usage{Inodes, Size});
  • Mounts:在调用View/Prepare之后恢复 mounts;
  • Walk:带过滤器遍历快照图(支持nameparentkindlabels.(label)过滤);
  • Close:释放内部资源。

Info结构(core/snapshots/snapshotter.go#L127-L141)包含KindNameParentLabelsCreatedUpdated等字段。值得注意的是,只有以containerd.io/snapshot/为前缀的标签才会被PrepareViewCommit继承,例如containerd.io/snapshot.ref(远程快照协议中的目标 chainID)、containerd.io/snapshot/diff-idcontainerd.io/snapshot/uidmappingcontainerd.io/snapshot/gidmapping以及containerd.io/snapshot/max-size(块设备/配额上限提示)。

图元数据(Graph metadata):支持对快照图的查询

随着快照被导入容器系统,会形成一个由快照及其父关系构成的"图"。对该图的查询必须是受支持的操作。这正是WalkStatStorage.Remove中"删除前检查子节点"等能力的来源。

在元数据存储层 core/snapshots/storage/bolt.go 中,快照图以 BoltDB bucket 实现:每个快照是一个以 key 命名的 bucket,父子关系通过 parent index 维护。以 Remove 的实现 为例,它先通过 parent prefix 检查"cannot remove snapshot with child"(存在子快照时返回ErrFailedPrecondition),再删除父链接与自身 bucket——这正是文档中"删除已提交快照前必须先删除其子"的底层证据。而 CommitActive 会把活动快照 bucket 重命名为已提交快照 name,并处理重名冲突(返回ErrAlreadyExists)。

快照如何工作:以导入镜像层为例

为了把术语落到实处,设计文档从"导入层"的视角演示了 Snapshotter 的用法。下面我们用 Go API 复现整个过程,并对照接口签名补充现代实现(ctx参数与 GC 保护标签)。

导入一个层(Importing a Layer)

导入层时,只需让 Snapshotter 提供一组 mounts,使目标位置能够捕获变更集。首先取得层 tar 文件的路径,并创建临时解包目录:

layerPath, tmpDir := getLayerPath(), mkTmpDir() // just a path to layer tar file.

然后使用 SnapshotterPrepare一个新的快照事务,使用key并从空父""下降。为了防止解包期间快照被垃圾回收,可以附加containerd.io/gc.root标签(对应 snapshots.WithLabels 选项):

noGcOpt := snapshots.WithLabels(map[string]string{ "containerd.io/gc.root": time.Now().UTC().Format(time.RFC3339), }) mounts, err := snapshotter.Prepare(ctx, key, "", noGcOpt) if err != nil { ... }

Snapshotter.Prepare得到一组 mounts,key标识活动快照。将其挂载到临时位置:

if err := mount.All(mounts, tmpDir); err != nil { ... }

mount.All(core/mount/mount.go#L55-L64)会按顺序逐个挂载所有 mounts(子挂载须排在父挂载之后)。挂载完成后,临时位置就绪,可以捕获 diff——实践中这类似于一次文件系统事务。下一步解包层,unpackLayer会把层内容应用到目标位置,并计算解包后层的DiffID(这是 Docker 实现的要求):

layer, err := os.Open(layerPath) if err != nil { ... } digest, err := unpackLayer(tmpLocation, layer) // unpack into layer location if err != nil { ... }

完成后我们得到代表该层内容的文件系统。严谨的实现应校验 digest 与预期的DiffID一致。随后卸载 mounts:

unmount(mounts) // optional, for now

验证并解包层之后,把活动快照提交为name。示例中直接使用层 digest 作为名称,实践中更常用ChainID

if err := snapshotter.Commit(ctx, digest.String(), key, noGcOpt); err != nil { ... }

现在该层已存在于 Snapshotter 中,可通过提交时提供的 digest 访问。提交完成后,活动快照可以移除:

snapshotter.Remove(key)

导入下一层(Importing the Next Layer)

让新层依赖已有层,过程与上面完全一致,唯一区别是调用Snapshotter.Prepare时把父指定为上一层的标识,并假设使用干净的临时位置:

mounts, err := snapshotter.Prepare(ctx, key, parentDigest, noGcOpt)

然后像上面一样挂载、应用、提交。新快照将基于前一层的内容,形成层的链条。

运行容器(Running a Container)

运行容器时,只需把已提交的镜像快照作为父传给Snapshotter.Prepare。挂载后,准备好的路径可直接作为容器的文件系统:

mounts, err := snapshotter.Prepare(ctx, containerKey, imageRootFSChainID)

返回的 mounts 可直接传给容器运行时。若想从该文件系统创建新镜像,则调用Snapshotter.Commit

if err := snapshotter.Commit(ctx, newImageSnapshot, containerKey); err != nil { ... }

大多数容器运行场景下,则调用Snapshotter.Remove通知 Snapshotter 放弃这些变更:

snapshotter.Remove(containerKey)

从源码看 unpack 流程的印证

上述"导入层"流程在仓库中有多处直接实现:镜像解包器 core/unpack/unpacker.go 使用UnpackKeyPrefix = "extract"UnpackKeyFormat = "extract-%s %s"(snapshotter.go 常量)构造提取专用 key,并借助containerd.io/snapshot.ref标签实现远程快照协议的跳过逻辑;overlay 快照插件的Prepare/Commit实现(plugins/snapshots/overlay/overlay.go#L268-L318)则在写事务中完成目录创建(prepareDirectory)、元数据写入(storage.CreateSnapshot)、Commit时以fs.DiskUsage统计 upperdir 占用并调用storage.CommitActive

从设计到实现:overlay 快照插件如何落地

设计文档提出的模型在 plugins/snapshots 下落地为一系列实现:overlaynativebtrfsdevmapperblockfileerofswindowslcow。其中 overlay 是最典型的代表,值得对照源码理解设计如何变为现实。

overlay 快照的数据布局

以 plugins/snapshots/overlay/overlay.go 为例,NewSnapshotter(第 121-176 行)会在 root 下创建snapshots/目录存放各快照的 diff 数据,并创建metadata.db(BoltDB)保存快照元数据。它还做了若干防御性检查:

  • 校验底层文件系统支持d_type(如 xfs 需ftype=1格式化,否则报错);
  • 自动探测并追加userxattr选项(解决老内核 overlay 用户 xattr 问题);
  • 在内核支持index时追加index=off,且遵循"后写生效"规则避免覆盖用户显式配置的index=on

关键操作的实现映射

设计操作overlay 实现说明
PreparecreateSnapshot(KindActive, ...)(第 268-270 行)写事务中创建快照目录 + 写入元数据,返回 overlay/bind mounts
ViewcreateSnapshot(KindView, ...)(第 272-274 行)同上,但返回只读 mounts
Commit事务中storage.CommitActive(第 300-318 行)先统计 upperdir 磁盘占用,再把活动快照重命名为已提交名称
Remove事务中storage.Remove+ 目录清理(第 323-351 行)支持asyncRemove选项:延迟到Cleanup时异步回收磁盘,key 可立即复用

此外,overlay 插件还实现了Cleaner接口(Cleanup),用于清理已移除或废弃快照遗留的磁盘目录(第 373-387 行),对应接口层定义的 Cleaner。通过WithUpperdirLabel选项,Stat/Walk还会把 upperdir 位置以containerd.io/snapshot/overlay.upperdir标签暴露出来。

可配置选项一览

overlay 插件通过选项函数(Opt)暴露配置能力,overlay.go 第 56-106 行:

  • AsynchronousRemove:异步删除,key 即时可复用,磁盘延迟回收;
  • WithUpperdirLabel:为快照附加 upperdir 位置标签;
  • WithMountOptions(options):定义 overlay mount 的默认挂载选项(不作用于 bind mount);
  • WithMetaStore(ms):外部注入元数据存储;
  • WithRemapIDs/WithSlowChown:用于用户命名空间 ID 映射与慢速 chown 场景。

设计价值与后续阅读

总结这份历史设计文档的核心观点:把"快照"从"镜像/容器"中彻底剥离,用最小、挂载导向的 API 表达层的本质。这一设计至今仍是 containerd 的基石——core/snapshots定义语义,各插件提供实现,core/mount提供通用挂载能力,core/snapshots/storage用 BoltDB 维护快照图元数据。

若想继续深入,同一目录下的姊妹文档可对照阅读:mounts.md(Mount 对象的细节)、lifecycle.md(快照/镜像生命周期)、data-flow.md(数据流)与 architecture.md(整体架构)。接口的权威 Go 文档即 core/snapshots/snapshotter.go 中的Snapshotter接口注释,其中保留了与本文完全一致的层导入、容器运行示例代码。对实现细节感兴趣的读者,可以从 plugins/snapshots/overlay/overlay.go 与 core/snapshots/storage/bolt.go 开始逐行研读。

【免费下载链接】containerdAn open and reliable container runtime项目地址: https://gitcode.com/GitHub_Trending/co/containerd

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 15:25:11

CX9020中文字体配置全指南:Windows CE 6.0字体子集化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:24:00

集合论不是数学课,是工程师的思维操作系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:22:25

嵌入式工程师的I2C总线完全指南:从物理层到故障排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:20:30

前端转AI应用开发:基于Next.js和LangChain.js的低成本落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 15:16:51

Matlab实现FastICA语音盲分离:原理、代码与实验

简介:面向语音信号处理与盲源分离方向,该Matlab源码资源基于FastICA算法完成了语音信号盲分离任务,适合本科、硕士阶段的教研学习,也可直接用作信号处理与模式识别课程的实验范例。代码在Matlab 2019a环境下编写,压缩包…

作者头像 李华
网站建设 2026/9/13 15:14:50

零基础学PLC:从电工实操到产线调试的7个硬核问题

1. 这不是“学PLC”,而是帮你把电工经验变成自动化生产力的实操路径“PLC培训,新手最关心的7个问题”——这句话背后站着三类人:刚考完低压电工证、在配电房拧了三年螺丝的老师傅;大专机电专业毕业、简历上写着“熟悉PLC”但连S7-…

作者头像 李华