containerd 的 graph driver 去哪了?

Apr 17, 2022 21:00 · 1901 words · 4 minute read Container Linux FileSystem

原文:https://blog.mobyproject.org/where-are-containerds-graph-drivers-145fc9b7255

答案是 containerd 并没有 graph driver,但是它拥有 snapshotter。这样搞并非想发明一些新东西,而是因为 graph driver 的设计长久以来引发了很多问题。首先来看一下 graph driver 的历史——它们最初是如何被开发出来的。

容器文件系统类型

容器世界中使用两种文件系统:overlay 和 snapshot。AUFS 和 OverlayFS 都是 overlay 文件系统,有多个目录为镜像中的每一层提供文件 diff。而 snapshot 文件系统包括 devicemapper、btrfs 和 ZFS,它们在块级处理文件 diff。overlay 通常工作在 EXT4 和 XFS 这类常见的文件系统上,而 snapshot 文件系统只在为其格式化的卷上运行。

graph driver 的历史

什么是 graph driver?它们起源于何时?为啥被称作 graph driver?

要回答这些问题,我们必须回到多年前 Docker 0.8 所在的时间线。当时的 Docker 只支持 Ubuntu,因为它是唯一搭载了 AUFS 的发行版,Docker 使用这种 overlay 文件系统来形成镜像和容器的读写层。为了让 Docker 在老版本的内核运行,就需要 Docker 除 AUFS 外支持更多的文件系统。因此,对 device mapper(LVM thinpool)的支持是取代 AUFS 的一个选项。

起初,device mapper 看上去解决了所有的问题。让 Docker 所有内核和发行版上运行的愿望确实由 device mapper 实现了。甚至还想取消对 AUFS 的支持,只给容器文件系统使用 device mapper,但在测试过最初的实现后,这并不是期望的解决方案。发行版之间存在各种各样的问题。目标仍然是在不同发行版上运行 Docker,但不为削弱其在 Ubuntu 上的性能妥协。

为了让更多发行版的用户用上 Docker,文件系统的支持就得是可插拔的。Solomon 设计了一个新的驱动 API 以支持 Docker 中的多个文件系统。Solomon mock 并设计了 API,而 Crosby 则移植 AUFS,来验证该接口是否正常工作。

他们称这个新 API 为 graph driver,因为 Docker 将镜像各层的关系建模在“图”中,而文件系统主要存储镜像。命名困难症。。。

最初的 graph driver 接口非常简单而且运作良好。但是随着时间的推移,越来越多的需求被加入。graph driver API 开始支持这些功能:

  • 构建优化,例如缓存和共享层以加速构建
  • 内容可寻址性以确保文件系统被安全地识别
  • 运行时从 LXC 变更为 runc

这些变更的结果是 graph driver API 及其底层实现开始与它们所支持的高级功能交织在一起,导致了一些问题:

  • graph driver API 变得复杂
  • 驱动程序中都有内置的优化构建编码
  • 驱动程序与容器生命周期紧密耦合
  • 难以维护,因为 graph driver 的范围很广

一开始就预见到这些设计问题是不可能的,因为没有如今这些需求。活到老学到老。 ¯\_(ツ)_/¯

snapshotter 横空出世

是时候重新思考下 graph driver 如何与容器一起工作了。

API 复杂度

为了解决 graph driver 已经发展成为复杂 API 的问题,需要研究 overlay 和 snapshot 文件系统的功能需要什么。我们知道 snapshot 不如 overlay 文件系统灵活,因为快照有严格的父子关系。因为它们在块级工作,所以在创建子快照前必须有一个父快照。

在编写系统软件时,一般我们尽量找在 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 命名空间内而非 host;我们不希望 snapshotter 挂载任何东西——如果它们不挂载,那也就不会卸载。

这样有几个好处:

  • 调用者作为 builder 或执行组件,可以决定何时需要挂载 rootfs;何时执行结束,这样就可以卸载。
  • 在一个容器的 mount 命名空间中挂载,当容器死亡时,内核将卸载该命名空间中的所有东西。这消除了一些 graph driver 陈旧文件句柄的问题。

我们通过设计快照 API 返回一个序列化的挂载调用来实现,由调用者挂载和卸载至他们选择的位置。这被 containerd 中的执行组件所使用来在 container-shim 中挂载容器的 rootfs,并在任务执行结束后卸载。

维护

最后,我们希望确保 snapshotter 可被长期支持。这点通过一个更间接的接口来实现,允许我们去掉大部分调用者的具体要求,这样 snapshotter 就可以专注于成为一个 snapshotter。

归根到底,snapshotter 是 graph driver 的一种演变。这也算修复用户面临的长期存在的 graph driver 问题,而且还是一种可以支持多年的方式。