《containerd 系列》一文了解 containerd 中的 snapshot
本文内容节选自 《containerd 原理剖析与实战》
1. containerd 中的镜像存储
一个正常的镜像从制作出来到通过容器运行时启动大概会经历如下几个步骤,如下图所示。
基于 Dockerfile 制作镜像。
推送到镜像 Registry 中,如 dockerhub 或者自建的 harbor 仓库。
从镜像 Registry 拉取到本地。
本地容器运行时管理镜像的存储,并将镜像转化为容器运行所需的 rootfs。
交付 rootfs 给容器时在启动容器前进行挂载。
<p align=center>图 镜像从制作到启动容器前的流程</p>
在上述的镜像流转过程中,containerd 参与的主要是步骤 3 、4,即拉取镜像、解压镜像、将镜像准备为容器 rootfs ,并提供 rootfs 挂载信息供后续运行容器使用,如下图所示。
<p align=center>图 containerd 拉取镜像到准备容器 rootfs </p>
containerd 中涉及镜像、容器 rootfs 持久化的主要模块主要是 Content
、Metadata
、Snapshot
。其中 metada
主要用于存储元数据,containerd 会将镜像 manifest 和镜像 layer 拉取并保存到 content 目录。
注意存储在 content 中的镜像 layer 是不可以变的,其存储格式也是没法直接使用的,常见的格式是 tar+gzip
。tar+gzip
是没法直接挂载给容器使用。
2. containerd 中的 snapshot
因此为了使用 content 中存储的镜像层,containerd 抽象出了 snapshot (快照) 概念。
每个镜像层生成对应的一个 snapshot,同时 snapshot 有父子关系。子 snapshot 会继承 父 snapshot 中文件系统的内容,即叠加在父 snapshot 内容之上进行读写操作。
snapshot 代表的是文件系统的状态,snapshot 的生命周期中共有三种类型,committed
、active
、view
:
committed: committed 状态的 snapshot 通常是由 active 状态的 snapshot 通过 Commmit 操作之后产生的。committed 状态的 snapshot 不可变。
active: active 状态的 snapshot 通常是由 committed 状态的 snapshot 通过 Prepare 或 View 操作之后产生的。不同于 committed 状态,active 状态的 snapshot 是可以进行读写修改等操作的。对 active 状态的 snapshot 进行 Commit 操作会产生 committed 状态的 新 snapshot,同时会继承该 snapshot 的 parrent。
view:view 状态的 snapshot 是父 snapshot 的只读视图,挂载后是不可被修改的。
2.1 snapshot 生命周期
snapshot 的生命周期如下图所示。
<p align=center>图 snapshot 生命周期</p>
从上图可以看到:
状态为
Committed
的 snapshot A0,经过Prepare
调用后生成了Active
状态的 snapshot a。Acitve
状态的 snapshot a 是可读写的,可以挂载到指定目录进行操作,snapshot 中的文件系统经过修改后变为 a' (并没有生成新的 snapshot a',只是相比于初始 snapshot a 发生了变化,暂且称为 a')。a' 经过
Commit
操作后,生成Committed
状态的 snapshot A1,以 a 为名的 snapshot 则会被删除 (Remove
)。A0 是 A1 的父 snapshot。Committed
snapshot A0,还可以经过View
调用后生成view
状态的 snapshot b,snapshot b 是只读的,挂载后的文件系统不可被修改。
2.2 snapshot 是如何存储的
还是以 redis:5.0.9 为例,介绍 snapshot 是如何存储的,在 /var/lib/containerd
目录中可以看到多个 io.containerd.snapshotter.v1.<type>
命名的文件夹:
在 containerd 中,snapshot 的管理是由 snapshotter 来做的,containerd 中支持多种 snapshotter 插件。如 containerd 默认支持的 overlay
snapshotter,它管理的 snapshotter 就保存在 /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs
中。不同于 content 目录,snapshot 目录中的内容不是以 sha256 命名的,而是从 1 开始的 index 命名的:
snapshot 可以通过 ctr snapshot ls
查看,可以看到 snapshot 之间的 parent 关系,第一层 snapshot 的 parent 为空。
注意,snapshot key 中 sha256 的值并不是镜像 layer content 解压之后的 sha256,而是每一层镜像 layer content 解压后再叠加 parent snapshot 中的内容,重新计算得到的 sha256 的值。如下图所示。
图 snapshot sha256 计算方法
对于第一层 snapshot 而言(parent 为空的那层)和镜像 layer content 解压之后的 sha 256 一致(其实是上述镜像 config 文件中的 diff_id): d0fe97fa8b8cefdffcef1d62b65aba51a6c87b6679628a2b50fc6a7a579f764c
启动 redis 容器,可以看到多了一层 active
状态的 snapshot,这层 active
的 snapshot 就是对应容器的读写层。
镜像的每一层都会被创建成 committed 状态的 snapshot,committed 表示该镜像层不可变,在启动容器时,将为每个容器创建一个可读写的 active snapshot,这一层是可读写的。下图是镜像 layer 与 snapshot 的对应关系。
<p align=center>图 镜像 layer 与 snapshot </p>
接下来介绍 snapshot 的管理工具 snapshotter
。
3. graphdriver 与 snapshotter
在 docker 中一直使用 graphdriver 中来管理镜像的存储,但在 graphdriver 设计使用以来,引发了很多问题。于是在 containerd 从 docker 中贡献出来后,原有的 graphdriver 便重新设计,变为 snapshotter。
3.1 graphdriver 的历史
最早的 Docker 只支持 Ubuntu, 因为 Ubuntu 是唯一搭载了 Aufs 的发行版,Docker 使用 aufs 作为 镜像容器 rootfs 的 unionfs 文件系统格式,此时为了让 Docker 在老版本的内核中运行,便需要 Docker 支持持除了 aufs 之外的其他文件系统,如支持了基于 **LVM 精简卷 (Thin provisioned)**的 device mapper 。
device mapper 的出现让 Docker 在所有的内核和发行版上运行成为了可能。为了让更多的 Linux 发行版用上 Docker,Docker 创始人所罗门(solomon)设计了一个新的 API 来支持 Docker 中的多个文件系统,这个新的 API 就是 graph driver。起初 graph driver 接口非常简单,但随着时间推移,加入了越来越多的特性:
构建优化,基于构建缓存加速构建过程
内容可寻址。
运行时由 LXC 变为 runc 。
随着这些特性的加入,graph driver 也变得越来越臃肿:
graph driver API 变得越来越复杂。
driver 中都有内置的构建优化代码。
driver 与容器的生命周期紧密耦合。 因此,containerd 的开发者决定重新重构 graph driver 来解决其过于复杂的问题,毕竟 containerd 作为一个新生儿并没有历史包袱。
3.2 snapshotter 的诞生
在 Docker 中,容器中使用的文件系统有两类: 覆盖文件系统 (overlay) 和 快照文件系统 (snapshot) 。AUFS 和 OverlayFS 都是 overlay 文件系统,每一层镜像 layer 对应一个目录,通过目录为镜像中的每一层提供文件差异。snapshot 类型文件系统则包括 devicemapper
、btrfs
和 zfs
,快照文件系统在块级别处理文件差异。overlay
需要依赖于 ext4
或 xfs
等现有的文件系统上,而 snapshot 文件系统只能运行在格式化好的卷上。
snapshot 文件系统相比于 overlay 文件系统而言,灵活性稍差,因为 snapshot 需要有严格的父子关系。创建子快照时必须要有一个父快照。而通常在接口设计时,优先寻找最不灵活的实现来创建接口。因此 containerd 中对接不同文件系统的 API 定义为 snapshotter
。
相比于 graph driver,snapshotter
并不负责 rootfs 挂载和卸载动作的实现,这样做有几个好处:
调用者作为镜像构建组件或容器执行组件,可以决定何时需要挂载 rootfs;何时执行结束,以便进行卸载。
在一个容器的 mount 命名空间中挂载,当容器死亡时,内核将卸载该命名空间中的所有挂载。这改善了一些 graph driver 陈旧文件句柄的问题。
snapshotter
返回的 rootfs 的挂载信息 (如 rootfs 的 path,类型等),由 containerd 决定在 containerd-shim
中挂载容器的 rootfs,并在任务执行后进行卸载。containerd 与 snapshotter
交互的逻辑如下图所示。
<p align=center>图 containerd 与 snapshotter 交互 </p>
snapshotter
是 graphdriver 的演进,以一种更加松耦合的方式提供 containerd 中容器和镜像存储的实现,同时 containerd 中也提供了 out of tree 形式的 snapshotter
插件扩展机制:proxy snapshotter
, 通过 grpc 的形式对接用户自定义的 snapshotter
。
3.3 snapshotter 概述
通过上面的介绍我们了解了 Docker 中 graph driver 的来源以及 containerd 中 snapshotter
的创建历史,了解到 snapshotter
是 containerd 中用来准备 rootfs 挂载信息的组件。接下来就详细介绍 snapshotter
组件。
在 containerd 整体架构中,containerd 设计上为了解耦,划分成了不同的组件(Core 层的 Service
、Metadata
和 Backend
层的 Plugin),每个组件都以插件的形式集成到 containerd 中,每种组件都由一个或多个模块之间协作完成各自的功能。containerd 架构图如下图所示。
<p align=center>图 containerd 架构图[ 图片来源https://containerd.io/] </p>
在本节中我们主要关注 Snapshots service 和 snapshotter plugin 模块。
3.4 snapshotter 接口
snapshotter 的主要工作是为 containerd 运行容器准备 rootfs 文件系统。通过将镜像 layer 逐层依次解压挂载到指定目录,最终提供给 containerd 启动容器时使用。 为了管理 snapshot 的生命周期,所有的 snapshotter
都会实现以下 10 个接口:
snapshotter 接口的详细说明如下表所示。
表 snapshotter 接口的详细说明
3.5 snapshotter 准备容器 rootfs 过程
在 snapshotter
准备容器 rootfs 的过程中,比较关键的几个方法是 Prepare
、Commit
方法,接下来以具体的例子进行介绍。
snapshotter
准备目录是根据镜像 layer 一层一层准备的。例如第 1 层直接解压到指定目录作为第 1 层 snapshot;准备第 2 层镜像 layer 时,在第 1 层 snapshot 的文件系统内容之上再解压第 2 层 镜像 layer 作为第 2 层 snapshot,以此类推在准备第 n 层 snapshot 时,是在 n-1 层 snapshot 基础上解压第 n 层镜像 layer 进行实现的。
在准备 snapshot 时先通过 Prepare
创建可读写的 Active
snapshot,将该 snapshot 挂载后,解压镜像到 snapshot 中,而后将该 snapshot 提交为只读的 Committed
snapshot。如下图所示。
<p align=center>图 snapshotter 准备容器 rootfs 的过程</p>
我们知道,镜像是有多层只读层 layer 组成,容器 rootfs 则是由镜像只读层 layer 加一层读写层来实现的。snapshotter
的实现机制与镜像 layer 一一对应。
上图中镜像为一个 3 层 layer 的镜像,snapshotter
在准备该镜像的 snapshots 时:
首先通过
Prepare
创建一个Active
状态的 snapshot 1',该调用返回一个空目录(以 parent 为 "")的挂载信息,挂载后为可读写的文件夹。将第一层镜像 layer (layer0) 解压到该文件夹中,调用
Commit
生成Committed
状态的 snapshot1,snapshot1 的 父 snapshot 为空,随后Remove
snapshot 1'。此时对 snapshot1 调用Mount
、Prepare
、View
操作返回的挂载信息挂载后,其中的文件系统内容为镜像 layer 0 解压后的内容。通过
Prepare
以 snapshot1 为父 snapshot,创建一个Active
状态的 snapshot 2',将 snapshot 2' 挂载后,目录中会含有第一层镜像 layer 的内容。此时将第二层镜像 layer (layer1)解压到挂载 snapshot 2' 的目录中,再次调用Commit
生成Committed
状态的 snapshot2。此时对 snapshot2 调用Mount
、Prepare
、View
操作返回的挂载信息挂载后,其中的文件系统内容为镜像 layer 0、镜像 layer1 依次解压后的内容。以此类推,通过
Prepare
以 以 snapshot2 为父 snapshot,创建一个 Active 状态的 snapshot 3',再将第三层镜像 layer (layer2)解压到其中,最终生成为Committed
状态的 snapshot3。注意,snapshot1、snapshot2、snapshot3 均是只读的,不可变的。此时对 snapshot2 调用Mount
、Prepare
、View
操作返回的挂载信息挂载后,其中的文件系统内容为镜像 layer 0、镜像 layer1 、镜像 layer2 依次解压后的内容。在启动容器时,调用
snapshotter
的Prepare
接口以 snapshot3 为父 snapshot,创建一个Active
状态的 snapshot4。此时将Prepare
调用返回的挂载信息挂载后即是容器的 rootfs 目录。rootfs 目录中含有的内容是镜像 layer 0、1、2 依次叠加之后的内容,同时由于该 snapshot 是Active
状态,目录是可读写的。
关于 Mount
Prepare
返回的挂载信息为 Mount
结构体,用于 Linux 挂载调用的参数,如:
【注意】
Target
结构体是 containerd 1.7.0 之后添加的,是为了实现某个snapshotter
,具体的 issue 可以参考 Github 上的 issue[ https://github.com/containerd/containerd/issues/7839]。
可以通过 ctr snapshots mounts <target> <key>
查看某个 snapshot
对应的挂载信息,代码如下。
4. containerd 中的 snapshotter
containerd 支持的 snapshotter
可以通过 ctr plugin ls
查看。代码如下。
可以发现 containerd 内置的 snapshotter 有 aufs
、btrfs
、devmapper
、native
、overlayfs
、zfs
几种。其中 containerd 默认支持的 overlay
snapshotter。同时 containerd 也支持通过自定义实现 snapshotter 插件来支持,不必重新编译 containerd。
containerd 默认支持的 snapshotter
是 overlay
,等同于 Docker 中的 overlay2 gradriver
驱动,如何指定特定的 snapshotter
呢,接下来分别通过以下几种途径进行介绍。
nerdctl
ctr
CRI Plugin
containerd Client SDK
1. nerdctl
通过 nerdctl
拉取或推送镜像时通过指定 --snapshotter
来指定特定的 snapshotter
。 指定 snapshotter
拉取镜像采用下面的命令:
指定 snapshotter
启动容器,采用下面的命令:
2 ctr
指定 snapshotter
拉取镜像,代码如下。
指定 snapshotter
启动容器,代码如下。
还是以 redis:5.0.9
为例,以 native snapshotter plugin
拉取镜像和启动容器,代码如下。
指定 snapshotter
操作 snapshot
,代码如下。
ctr snapshot
支持的操作如下。
3. CRI Plugin
对接 Kubernetes 的场景下,可以通过 containerd config 文件(/etc/containerd/config
)进行配置,如下。
4. containerd Client SDK
拉取镜像和启动容器时通过指定 option
函数选择指定的 snapshotter
,代码如下。
上面就是 containerd 中 snapshot 和 snapshotter 的介绍。
其中 containerd 中内置的 snapshotter
有 aufs
、btrfs
、devmapper
、native
、overlayfs
、zfs
。
针对每种 snapshotter
的功能,后续将继续通过本公众号的文章依次展开介绍。
以上内容节选自新书 《containerd 原理剖析与实战》
本文使用 文章同步助手 同步
版权声明: 本文为 InfoQ 作者【公众号:云原生Serverless】的原创文章。
原文链接:【http://xie.infoq.cn/article/cefa758a7989cfc7885a7fb04】。文章转载请联系作者。
评论