containerd Blockfile Snapshotter 实战指南:为虚拟机内容器构建块设备级镜像快照
2026/9/13 12:07:59 网站建设 项目流程

containerd Blockfile Snapshotter 实战指南:为虚拟机内容器构建块设备级镜像快照

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

Blockfile snapshotter 是 containerd 中一类特殊的快照器:它不再在主机文件系统上为每个快照准备目录,而是为每个快照生成一个"原始块文件"(raw block file),并通过 loopback 挂载或直接作为块设备挂接给虚拟机使用。本指南以 docs/snapshotters/blockfile.md 为主线,结合仓库源码与测试,完整讲解其适用场景、配置参数、scratch 文件创建、容器运行方式以及逐层拷贝的底层原理,读完你可以独立在 containerd 中启用并验证该快照器。

快照器在 containerd 中的角色

快照器(snapshotter)负责把 OCI 镜像从镜像仓库解包出来,生成容器可用的根文件系统快照。它需要完成三件事:准备底层基础设施(目录或其他文件系统形态)、将各层(layer)应用合并成单一可挂载的根目录、在容器启动时挂载进去。

containerd 最常用且默认的是 overlayfs snapshotter,它直接在主机文件系统上产出目录,再以 bind-mount 方式挂载进容器。而 blockfile snapshotter 走的是完全不同的路径:它把每一层都写进一个磁盘镜像文件,最终产出的不是目录,而是一个完整的文件系统镜像文件——这个文件可以被 loopback 挂载,也可以作为块设备直接附给虚拟机。

适用场景:容器跑在虚拟机里

blockfile snapshotter 针对的核心场景是"容器运行在虚拟机内部"。在这种模式下,OCI 镜像依然是容器的文件系统(与普通容器一致),但容器本体运行在 VM 的 guest 中。由于虚拟机无法 bind-mount 宿主机的目录,blockfile snapshotter 便为每个快照创建一个块设备文件,再以块设备形式挂接到 VM 上,从而把镜像内容送进 guest。

从源码注册信息也能印证这一点:plugin.go 中ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec()),该快照器仅在宿主机平台提供支持。

替代方案对比

原文档明确指出,想把目录挂载进 VM 还有两条可选路径:

  • virtiofs:前提是你的 VMM(虚拟化监控器)支持 virtiofs 驱动,可将宿主机目录以共享文件系统方式暴露给 guest;
  • 9p:同样依赖 VMM 支持,通过 9p 协议把本地目录挂载进 VM。

此外,仓库还提供了另一类"镜像文件快照"方案:devicemapper snapshotter,它把快照建在 devicemapper thin-pool 中的文件系统镜像上。三者的取舍点在于:VMM 是否支持对应协议、是否希望使用成熟的块设备生态。

检查 blockfile snapshotter 是否可用

在配置之前,先确认 containerd 已编译并加载了 blockfile 插件:

$ ctr plugins ls | grep blockfile

该插件通过空白导入在 containerd 启动时完成注册,Linux 构建中由 cmd/containerd/builtins/builtins_linux.go 引入:

_ "github.com/containerd/containerd/v2/plugins/snapshots/blockfile/plugin"

插件在注册表中以Type: plugins.SnapshotPluginID: "blockfile"声明(见 plugin.go),因此配置节名即为io.containerd.snapshotter.v1.blockfile

配置文件与全部参数解析

在 containerd 的config.toml中加入如下配置节,修改后需重启 containerd 生效:

[plugins.'io.containerd.snapshotter.v1.blockfile'] scratch_file = "/opt/containerd/blockfile" root_path = "/somewhere/on/disk" fs_type = 'ext4' mount_options = [] recreate_scratch = true

各参数说明如下:

参数含义说明
root_path块文件存放目录必须对 containerd 进程可写;未配置时默认使用插件属性plugins.PropertyRootDir提供的根目录(见 plugin.go)
scratch_file空块文件路径作为所有块文件的基础底片,首次使用前该文件必须存在(除非开启recreate_scratch
fs_type块文件内的文件系统类型目前支持ext4xfs;留空时源码默认取ext4(见 blockfile.go)
mount_options挂载块文件时附加的挂载选项未配置时默认["loop"],且无论是否显式给出,都会强制追加loop(见 blockfile.go)
recreate_scratch是否重建 scratch 文件true时若 scratch 缺失则自动重建;为false时若缺失直接失败

源码层面的对应关系非常清晰:plugin.go 中的Config结构体以toml标签严格映射这五个配置项;其中scratch_filefs_typemount_options仅在非空/非零时才转换为快照器选项,而recreate_scratch总是被传入(plugin.go)。

默认值背后的实现细节

从 blockfile.go 的NewSnapshotter可以看到三个关键默认行为:

  1. 根目录不存在时自动创建(os.MkdirAll(root, 0700));
  2. scratch 文件默认路径是root_path/scratch,即root_pathscratch_file参数共同决定实际使用的底片文件;
  3. mount_options即使被配置为空,最终也至少包含loop,这是块文件挂载的硬性要求。

NewSnapshotter还会在root_path下初始化metadata.db(元数据存储)与snapshots/目录(块文件存放目录),每个已提交快照对应snapshots/<id>一个块文件(见getBlockFile,blockfile.go)。

创建 scratch 文件

scratch 文件是"空的"块设备底片,所有快照都从拷贝它开始。以下示例创建一个 500MB 的 ext4 scratch 文件:

$ # make a 500M file $ dd if=/dev/zero of=/opt/containerd/blockfile bs=1M count=500 500+0 records in 500+0 records out 524288000 bytes (524 MB, 500 MiB) copied, 1.76253 s, 297 MB/s $ # format the file with ext4 $ sudo mkfs.ext4 /opt/containerd/blockfile mke2fs 1.47.0 (5-Feb-2023) Discarding device blocks: done Creating filesystem with 512000 1k blocks and 128016 inodes Filesystem UUID: d9947ecc-722d-4627-9cf9-fa2a3b622106 Superblock backups stored on blocks: 8193, 24577, 40961, 57345, 73729, 204801, 221185, 401409 Allocating group tables: done Writing inode tables: done Creating journal (8192 blocks): done Writing superblocks and filesystem accounting information: done

若使用 xfs,则将mkfs.ext4替换为mkfs.xfs并确保fs_type = 'xfs'。测试代码 blockfile_loopsetup_test.go 演示了同样的流程:创建文件 →Truncate指定大小 →mkfs.ext4格式化 → 尝试挂载验证可用性。

运行一个容器

先确保镜像已存在(它就是一个普通 OCI 镜像),再用ctr指定快照器运行:

$ # ensure that the image we are using exists; it is a regular OCI image $ ctr image pull docker.io/library/busybox:latest $ # run the container with the provides snapshotter $ ctr run -rm -t --snapshotter blockfile docker.io/library/busybox:latest hello sh

通过 Go client API 使用的方式与其它快照器完全一致,只需把快照器名设为"blockfile"

import ( "context" "github.com/containerd/containerd" "github.com/containerd/containerd/snapshots" ) // create a new client client, err := containerd.New("/run/containerd/containerd.sock") snapshotter := "blockfile" cOpts := []containerd.NewContainerOpts{ containerd.WithImage(image), containerd.WithImageConfigLabels(image), containerd.WithAdditionalContainerLabels(labels), containerd.WithSnapshotter(snapshotter) } container, err := client.NewContainer(ctx, containerID, cOpts...)

工作原理:逐层拷贝块文件

blockfile snapshotter 的整体流程与其它快照器类似:逐层解包镜像,每一层都基于其父层内容构建。它的独特之处有两点:

  1. 层被应用到磁盘镜像文件内部,而不是主机文件系统上;
  2. 每一层都会创建一个新的块镜像文件,并把上一层内容叠加在其上。

因此流程结束时得到的不是一个包含内容的目录,而是一个承载完整文件系统镜像的单一文件,它可以被 loopback 挂载,或直接附给虚拟机。

三层镜像的处理过程

对于一个拥有 A、B、C 三层的镜像,处理过程如下:

  1. Layer A
    1. 把 scratch 文件拷贝为 Layer A 的新块文件;
    2. Loopback 挂载 Layer A 的块文件;
    3. 将 Layer A 应用到挂载点;
    4. 卸载 Layer A 的块文件。
  2. Layer B
    1. 把 Layer A 的块文件拷贝为 Layer B 的新块文件;
    2. Loopback 挂载 Layer B 的块文件;
    3. 将 Layer B 应用到挂载点;
    4. 卸载 Layer B 的块文件。
  3. Layer C
    1. 把 Layer B 的块文件拷贝为 Layer C 的新块文件;
    2. Loopback 挂载 Layer C 的块文件;
    3. 将 Layer C 应用到挂载点;
    4. 卸载 Layer C 的块文件。

每一层的解包都在前一层内容之上构建出新的块文件,最终块文件即为完整文件系统镜像。

这段逻辑在源码createSnapshot中有精确对应(blockfile.go):当存在父层(len(s.ParentIDs) > 0)时,把父层块文件copyFileWithSync到新块文件;当没有父层(首个层)时,则把scratch拷贝为新块文件。copyFileWithSync(blockfile.go)在 Linux 上通过io.Copy完成拷贝并在关闭前Sync落盘,在 Darwin 上则使用fs.CopyFile(clonefile)以保留稀疏文件特性。

各层块文件的内容与挂载差异

由于逐层拷贝叠加,每层最终都会对应一个块文件:

  1. Layer A 块文件:包含 Layer A 的内容;
  2. Layer B 块文件:包含 Layer A + Layer B 的内容;
  3. Layer C 块文件:包含 Layer A + Layer B + Layer C 的内容。

挂载时(mounts方法,blockfile.go),可写快照(Active)使用自己的块文件并追加rw选项,只读视图(View)直接复用父层块文件并追加ro选项,挂载类型即fs_type。这一细节意味着 View 类型的快照不会额外拷贝块文件,进一步节省空间。

稀疏文件带来的空间效率

只要底层文件系统与宿主 OS 支持,该过程会尽量使用稀疏文件(sparse file)能力,即块文件只占用实际内容所需的空间。

继续沿用 500MB scratch 示例:假设每层新增 25MB,则各层块文件实际占用的空间为:

  1. Layer A 块文件:25MB(Layer A);
  2. Layer B 块文件:50MB(Layer A + B);
  3. Layer C 块文件:75MB(Layer A + B + C)。

总占用为 25+50+75=150MB,远小于每层都占满 500MB 时的 1500MB。

值得注意的是,Usage统计(blockfile.go)也是基于这一近似模型:以"当前块文件大小减去父层块文件大小"估算该层增量,源码注释同时指出该估算未考虑文件系统对共享 extent 的支持差异,属于近似计算。

源码中的验证与约束

仓库为 blockfile snapshotter 提供了完整的测试支撑:

  • blockfile_test.go 通过testutil.RequiresRoot(t)要求 root 权限,并直接复用testsuite.SnapshotterSuite通用快照器测试套件,验证 Prepare/View/Commit/Remove 等全生命周期行为;
  • blockfile_loopsetup_test.go 用 8MB/16MB 的 scratch 文件实测mkfs.ext4+ loopback 挂载链路,其默认挂载选项为{"loop", "direct-io", "sync"},可作为生产配置的参考;
  • 测试中还通过withViewHookHelper处理只读视图挂载可能遇到的 ext4 日志恢复问题("recovery required on readonly filesystem"),源码注释(blockfile.go)提示该问题在慢速存储上更易复现。

使用前提需要明确:blockfile snapshotter 依赖 loopback 挂载能力,因而仅适用于支持 loopback 的 Linux 类宿主机环境(测试文件头部//go:build !windows && !darwin也印证了平台限制),且运行容器前必须完成 scratch 文件的创建与格式化。

小结

blockfile snapshotter 为"VM 内容器"这一特定场景提供了完整的块设备级快照方案:以 scratch 文件为底片、逐层拷贝叠加、稀疏文件节省空间、最终产出可直挂虚拟机的文件系统镜像文件。结合 config.toml 配置节、scratch 创建与 ctr 运行命令,即可在具备 loopback 能力的 Linux 宿主机上快速启用;而 blockfile.go 与 plugin.go 则完整揭示了默认值、挂载选项与逐层拷贝的实现细节,供需要深度定制或排查问题的开发者继续深入。

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

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询