ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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 快照插件 与 元数据存储层 的源码带你理解Prepare、View、Commit、Remove等核心操作的生命周期以及导入镜像层、运行容器的完整调用链最终具备独立阅读快照插件源码与二次开发 Snapshotter 的能力。背景从 Docker 的 graphdriver 到分层文件系统Docker 容器自诞生之初就构建在一种名为层layers的快照方法论之上。层提供了一种能力fork 一个文件系统、做出修改、再把变更集保存为新的层。历史上这套机制以名为graphdriver的组件被深度集成在 Docker daemon 中使得 Docker daemon 可以在多个操作系统上运行同时保持提交和分发镜像变更时大体一致的快照语义。但graphdriver与镜像的导入导出深度绑定包括管理层之间的关系以及容器运行时文件系统其行为甚至反向决定了镜像格式的传输方式。设计文档指出这种紧密耦合带来两个问题驱动实现的表面积过大导致各驱动之间行为不一致序列化、哈希、解包、打包、挂载等职责全部堆在一个组件里难以演进。基于此containerd 提出一个更灵活的层管理模型只提供基础快照功能的最小 API不耦合镜像结构与标识用更小的实现表面积换取各驱动实现之间更一致的行为。Snapshotter 与 graphdriver 的本质区别维度graphdriverDockerSnapshottercontainerd对镜像/容器的认知深度集成管理层关系与容器运行文件系统无任何认知只面向准备目录 提交目录与 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总是通过Prepare或View从一个已提交快照包括空快照创建已提交快照Committed总是通过Commit从一个活动快照创建活动快照永远不会变成已提交快照反之亦然KindActive/KindView/KindCommitted在 Kind 枚举 中区分所有快照都可以被移除。挂载活动快照后可以对其做出修改提交Commit动作会创建一个已提交快照该已提交快照继承活动快照的父快照随后可作为新活动快照的父快照。活动快照永远不能作为父快照使用。下面的示意图展示了快照之间的关系图中可以清楚看到活动快照a通过调用Prepare以已提交快照P₀为父创建修改后a变为a再调用Commit创建已提交快照P₁a可继续修改为a再次调用Commit创建第二个已提交快照P₂注意P₂的父是P₀而不是P₁——因为Commit只把活动快照固化下来并不会改变其父指针。操作OperationsPrepare / 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资源回收必须调用RemovecommitCommit(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更新快照的可变属性如 labelsUsage返回快照自身不含父的磁盘占用与 inode 数Usage{Inodes, Size}Mounts在调用View/Prepare之后恢复 mountsWalk带过滤器遍历快照图支持name、parent、kind、labels.(label)过滤Close释放内部资源。Info结构core/snapshots/snapshotter.go#L127-L141包含Kind、Name、Parent、Labels、Created、Updated等字段。值得注意的是只有以containerd.io/snapshot/为前缀的标签才会被Prepare、View、Commit继承例如containerd.io/snapshot.ref远程快照协议中的目标 chainID、containerd.io/snapshot/diff-id、containerd.io/snapshot/uidmapping、containerd.io/snapshot/gidmapping以及containerd.io/snapshot/max-size块设备/配额上限提示。图元数据Graph metadata支持对快照图的查询随着快照被导入容器系统会形成一个由快照及其父关系构成的图。对该图的查询必须是受支持的操作。这正是Walk、Stat、Storage.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得到一组 mountskey标识活动快照。将其挂载到临时位置if err : mount.All(mounts, tmpDir); err ! nil { ... }mount.Allcore/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一致。随后卸载 mountsunmount(mounts) // optional, for now验证并解包层之后把活动快照提交为name。示例中直接使用层 digest 作为名称实践中更常用ChainIDif 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.Commitif err : snapshotter.Commit(ctx, newImageSnapshot, containerKey); err ! nil { ... }大多数容器运行场景下则调用Snapshotter.Remove通知 Snapshotter 放弃这些变更snapshotter.Remove(containerKey)从源码看 unpack 流程的印证上述导入层流程在仓库中有多处直接实现镜像解包器 core/unpack/unpacker.go 使用UnpackKeyPrefix extract与UnpackKeyFormat extract-%s %ssnapshotter.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 下落地为一系列实现overlay、native、btrfs、devmapper、blockfile、erofs、windows、lcow。其中 overlay 是最典型的代表值得对照源码理解设计如何变为现实。overlay 快照的数据布局以 plugins/snapshots/overlay/overlay.go 为例NewSnapshotter第 121-176 行会在 root 下创建snapshots/目录存放各快照的 diff 数据并创建metadata.dbBoltDB保存快照元数据。它还做了若干防御性检查校验底层文件系统支持d_type如 xfs 需ftype1格式化否则报错自动探测并追加userxattr选项解决老内核 overlay 用户 xattr 问题在内核支持index时追加indexoff且遵循后写生效规则避免覆盖用户显式配置的indexon。关键操作的实现映射设计操作overlay 实现说明PreparecreateSnapshot(KindActive, ...)第 268-270 行写事务中创建快照目录 写入元数据返回 overlay/bind mountsViewcreateSnapshot(KindView, ...)第 272-274 行同上但返回只读 mountsCommit事务中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 mountWithMetaStore(ms)外部注入元数据存储WithRemapIDs/WithSlowChown用于用户命名空间 ID 映射与慢速 chown 场景。设计价值与后续阅读总结这份历史设计文档的核心观点把快照从镜像/容器中彻底剥离用最小、挂载导向的 API 表达层的本质。这一设计至今仍是 containerd 的基石——core/snapshots定义语义各插件提供实现core/mount提供通用挂载能力core/snapshots/storage用 BoltDB 维护快照图元数据。若想继续深入同一目录下的姊妹文档可对照阅读mounts.mdMount 对象的细节、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),仅供参考
返回列表