时间拉回到五年前,当安装docker时,都会看到grahp driver这一目录,但在docker将libe-containerd独立,并捐献成为containerd项目后,为何不见了grahp driver?
Containerd 并没有 graph driver,但是它有snapshotter。这样处理不是为了造新轮子,而是为了填坑,毕竟当初 graph driver 的设计,一直多为诟病。先来回顾一下 graph driver 的历史。
该文目前不介绍docker、containerd的演化转变史,后续会单独出文逐一介绍。
容器文件系统类型
容器中使用两种文件系统:overlay 和 snapshotting。AUFS 和 OverlayFS 都为 overlay 文件系统,有多个目录为镜像中的每一层提供文件 diff。而 snapshot 文件系统包括 devicemapper、btrfs 和 ZFS,它们在块层级处理文件 diff。overlay 通常工作在 EXT4 和 XFS 这类文件系统上,而 snapshot 文件系统只在其格式化的卷上。
graph driver的历史
探究graph driver的历史,必然有此番定体问:什么是 graph driver?为什么有?为何叫 graph driver?
当然要回答这些问题,需要把时间线拉得更长一点,回到 Docker 0.8 所在的时间线。当时的 Docker 只支持 Ubuntu,因为它是唯一支持 AUFS 的Linux发行版,Docker 使用 overlay 文件系统来构建镜像和容器的读写层。为了让 Docker 能够支持老版本的内核,需要 Docker 除 AUFS 以为更多的文件系统。因此,对支持device mapper(LVM thinpool)成为替换 AUFS 兼容老版本的可选项。
起初,device mapper 似乎能够解决所有的问题,当然 Docker 支持所有内核运行的目标确实由 device mapper 实现了。当时社区一度想取消对 AUFS 的支持,只让容器的文件系统使用 device mapper,但在测试过POC后,发现这并不是理想的解决方案。发行版之间存在诸多问题。社区确实需要实现支持Docker兼容多架构多Linux发行版的目标,但前提是不能以牺牲现有 Ubuntu 用户运行的性能为代价。
为了让更多Linux发行版用户用上 Docker,文件系统的支持必须是可插拔的。Solomon 设计了一个新的驱动 API 以支持 Docker 中的多个文件系统。Solomon设计了 API,而 Crosby 则改造 AUFS实现,以验证API的完备性。
他们将新 API 命名为 graph driver,因为 Docker 将镜像各层的关系建模在“图”中,而文件系统主要存储镜像。
起初 graph driver 接口设计的简易且运作稳定。但是随着时间的推移,需求日益增多,graph driver API 逐步支持下述功能:
-
构建方面的优化,例如缓存和共享层以加速构建
-
内容可寻址性特性,以确保文件系统被安全地识别
-
运行时从 LXC 变更为 runc
这些变更导致 graph driver API 及其底层实现与它们所支持的高级功能逐渐交织糅合到一起,从而导致了一些问题:
-
graph driver API 日渐复杂
-
每个驱动中都有内置的代码以优化构建
-
驱动程序与容器生命周期紧密耦合
-
难以维护,因为 graph driver 的范围很广
没有如今这些需求,一开始是不可能预见到这些设计问题的。
破局者snapshotter
直到捐献Containerd,才有时间来厘清这些长期存在的问题,并尝试去解决其中的一部分问题,确实也是时候重新构思 graph driver 应该如何与容器协作。
API 复杂度
为了解决 graph driver 巨型API 的问题,首先需要明确 overlay 和 snapshot 文件系统理应支持的功能。正如所知道的,snapshot 不如 overlay 文件系统灵活,因为快照有严格的层级关系。因为snapshot 在块层级工作,因此需要在创建子快照前必须生成一个父快照。
尽量在 I/O 方面最不灵活的地方抽象一个接口,这是编码实现的基本路线,这也是为什么 containerd 将文件系统组件称为 snapshotter,因为是以它们为模型设计了接口。在为 snapshotter 开发了最初的 API 后,支持多种 overlay 文件系统就变得易如反掌。
可以在这看到完整的接口以及每种方法的相关信息:https://pkg.go.dev/github.com/containerd/containerd/snapshot?utm_source=godoc#Snapshotter
容器生命周期的变化
在使用graph driver时,一直是驱动为容器挂载和卸载 root文件系统。这源于 当初Docker 还在使用 LXC 时,必须在完全挂载的 rootfs 中执行容器。在切到 runc 后,这个问题也就不存在了。
我们希望所有挂载都发生在 mount 命名空间内而非宿主机上,同时我们不希望 snapshotter 挂载任何东西,这样有以下几点好处:
-
调用者作为 builder 或执行组件,可以决定何时需要挂载 rootfs;何时执行结束,这样就可以卸载。
-
在容器的命名空间挂载,当容器消亡时,内核将卸载该命名空间中的所有东西。这解决了一些 graph driver 遗留文件句柄的问题。
设计snapshotter API 返回一个序列化的mount调用来实现,由调用者挂载和卸载。containerd 中的执行组件 containerd-shim 挂载容器的 rootfs,并在任务执行结束后卸载。
社区维护
最后,我们希望确保 snapshotter 可被长期维护。这点通过一个简易的接口来实现,允许我们去掉大部分调用者的需求,这样 snapshotter 就可以专注于成为一个 snapshotter。
从这一点来看,snapshotter 可以视作 graph driver 的一个演变版本。这也算修复graph driver用户面临的长期未决的一部分问题。
参考文献
1.https://blog.mobyproject.org/where-are-containerds-graph-drivers-145fc9b7255