menu Ga1@xy's Wor1d
浅析 Docker overlay2 文件结构
859 浏览 | 2021-11-20 | 阅读时间: 约 12 分钟 | 分类: 杂七杂八 | 标签: 杂七杂八
请注意,本文编写于 61 天前,最后修改于 60 天前,其中某些信息可能已经过时。

本篇文章是在我复现取证时,由于取证检材中一个FTP服务器涉及到了Docker容器的问题,在服务器中寻找docker文件时对容器文件夹的命名成因比较好奇,就稍微深入了解了一下容器目录下到底为什么呈现出这样的状态……

前言

rootfs

在讲 overlay2 之前,我们需要先简单了解下什么是 rootfs:
rootfs 也叫 根文件系统,是 Linux 使用的最基本的文件系统,是内核启动时挂载的第一个文件系统,提供了根目录 /,根文件系统包含了系统启动时所必须的目录和关键性文件,以及使其他文件系统得以挂载所必要的文件。在根目录下有根文件系统的各个目录,例如 /bin、/etc、/mnt 等,再将其他分区挂载到 /mnt,/mnt 目录下就有了这个分区的各个目录和文件。

docker 容器中使用的同样也是 rootfs 这种文件系统,当我们通过 docker exec 命令进入到容器内部时也可以看到在根目录下有 /bin、/etc、/tmp 等目录,但是在 docker 容器中与 Linux 不同的是,在挂载 rootfs 后,docker deamon 会利用联合挂载技术在已有的 rootfs 上再挂载一个读写层,容器在运行过程中文件系统发生的变化只会在读写层进行修改,并通过 whiteout 文件隐藏只读层中的旧版本文件。

whiteout 文件:
whiteout 概念存在于联合文件系统(UnionFS)中,代表某一类占位符形态的特殊文件,当用户文件夹与系统文件夹的共通部分联合到一个目录时(例如 bin 目录),用户可删除归属于自己的某些系统文件副本,但归属于系统级的原件仍存留于同一个联合目录中,此时系统将产生一份 whiteout 文件,表示该文件在当前用户目录中已删除,但系统目录中仍然保留。

联合挂载技术

所谓联合挂载技术(Union Mount),就是将原有的文件系统中的不同目录进行合并(merge),最后向我们呈现出一个合并后文件系统。在 overlay2 文件结构中,联合挂载技术通过联合三个不同的目录来实现:lower目录、upper目录和work目录,这三个目录联合挂载后得到merged目录

  • lower目录:只读层,可以有多个,处于最底层目录
  • upper目录:读写层,只有一个
  • work目录:工作基础目录,挂载后内容被清空,且在使用过程中其内容不可见
  • merged目录:联合挂载后得到的视图,其中本身并没有实体文件,实际文件都在upper目录和lower目录中,在merged目录中对文件进行编辑,实际会修改upper目录中文件,而在upper目录与lower目录中修改文件,都会影响我们在merged目录中看到的结果

overlayFS

在介绍 docker 中使用的 overlay2 文件结构前,我们先通过对 overlay 文件系统进行简单的操作演示以便更深入理解不同层不同目录之间的关系

先创建几个文件夹和文件

使用 mount命令挂载成 overlayFS 文件系统,格式如下

mount -t overlay overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work merged_dir

在这个例子中,我们用 A 和 B 两个文件夹作为 lower 目录,用 C 作为 upper 目录,worker 作为 work 目录,挂载到 /mnt/merged 目录下

mkdir merged
mount -t overlay overlay -o lowerdir=A:B,upperdir=C,workdir=worker /mnt/merged

挂载后我们可以查看一下 merged 目录下的文件

可以看到我们原本 A B C 三个目录下的文件已经被合并,相同文件名的文件将会选择性的显示,在 merged 中会显示离 merged 层更近的文件,upper 层比 lower 层更近,同样的 lower层中,排序靠前比排序靠后更近,在这个例子中就是 A 比 B 更靠近 merged 层

根据这个规律,我们可以先分析下 merged 层中的文件来源,a.txt 在 A 和 B 中都有,但 A 比 B 更优先,所以 merged 中的 a.txt 应该来自 A 目录,b.txt 在 A 和 C 中都有,但 C 是 upper 层,所以 b.txt 应该来自 C 目录,我们可以核实一下

接下来我们可以看下 upper层、lower层 和 merged层之间的关系,上文已经提到了 upper 层是 读写层 而 lower 层是 只读层,merged 层是联合挂载后的视图,那如果我们在 merged 层中对文件进行操作会发生什么

我们修改 merged 层的 a.txt 文件,可以看到 merged 层的 a.txt 内容虽然改变,但 A 目录(只读层)下的 a.txt 内容并没有发生变化,而在 C 目录(读写层)下多了一个 a.txt 文件,内容就是我们修改过的 a.txt 的内容,这就是只读层与读写层的关系,在 merged 目录对文件进行修改并不会影响到只读层的源文件,只会在读写层进行编辑

如果我们在 merged 目录删除文件会发生什么

可以看到在 merged 目录中已经没有 c.txt 文件,但在 C 目录下却多了一个 c.txt ,这个文件就是我们在一开始提到的 whiteout文件,它是一种主/次设备号都为0的字符设备,overlay 文件结构通过使用这种特殊文件来实现文件删除功能,在 merged 目录下使用 ls 命令来查看文件时,overlay 会自动过滤掉 upper 目录下的 whiteout 文件以及在 lower 目录下的同名文件,以此来实现文件删除效果

还有一个值得提到的点:overlay在对文件进行操作时用到了写时复制(Copy on Write)技术,在没有对文件进行修改时,merged 目录直接使用 lower 目录下的文件,只有当我们在 merged 目录对文件进行修改时,才会把修改的文件复制到 upper 目录

Docker overlay2

有了对 overlayFS 的基本了解,我们接下来就可以着手分析 Docker 的 overlay2 文件结构了,实际上 Docker 支持的存储驱动有很多种:overlay、overlay2、aufs、vfs 等,在 Ubuntu 较新版本的 Docker 中普遍采用了 overlay2 这种文件结构,其具有更优越的驱动性能,而 overlay 与 overlay2 的本质区别就是二者在镜像层之间的共享数据方法不同:

  • overlay 通过 硬链接 的方式共享数据,只支持,增加了磁盘 inode 负担
  • overlay2 通过将多层 lower 文件联合在一起

简而言之,overlay2 就是 overlay 的改进版本,我们可以通过 docker info 命令查看

在 Docker 中,我们日常操作主要涉及两个层面:镜像层与容器层,镜像层就是我们通过 docker pull 等命令下载到本机中的镜像,而容器层则是我们通过 docker exec 等命令进入的交互式终端,如果你使用过 Docker,你会发现我们只用一个镜像,通过 docker run 可以产生很多个容器,这就可以类比 upper 与 lower 两层,镜像作为 lower 层,只读提供文件系统基础,而容器作为 upper 层,我们可以在其中进行任意文件操作,只用同一个镜像就可以引申出不同的容器,这也是一种节约空间资源的方式吧(我的推测

接下来我们稍微详细地探讨下镜像层与容器层,还有他们的元数据

镜像层

我们可以通过 docker image inspect [IMAGE ID] 来查看镜像配置

其中的 GraphDriver 字段中有关于 overlay2 文件结构的目录信息

每一层的对应都在配置信息中体现的非常清楚,但是有一点问题,我们在实际查看文件夹的时候,可以发现镜像层其实并没有 /merged 目录,我的理解是镜像层作为 Docker 容器的最底层(只读层)并不需要有视图的功能,我们在实际使用过程中也并不会直接对镜像进行操作,所以其在配置信息中显示也仅仅是为了呈现完整的 overlay2 文件结构(不一定对

可以看到镜像的目录是在 /var/lib/docker/overlay2 下,我们打开一个镜像层看一看其中都有哪些文件

其中我们关注下 diff 目录、link 和 lower 文件

diff 目录

在这个目录中存放的是当前镜像层的文件,刚刚在介绍 over个lay2 与 overlay 区别的时候提到了 overlay2 是将多个 lower 层联合到一起,在上面的图中也可以看到,多个 lower 层之间用 : 分割,在这些层中每一层都有一部分文件,把他们联合到一起就得到了完整的 rootfs

link 文件

link 文件中的内容是当前层的软链接名称

这些链接都在 /var/lib/docker/overlay2/l 目录下

使用软链接的目的是为了避免达到 mount 命令参数的长度限制

lower 文件

lower 文件中的内容是在此层之下的所有层的软链接名称,最底层不存在该文件,我们知道 upper 层在 lower 层之上,而 lower 层中越靠后则越在底层

我们查看 upper 层对应目录下 lower 文件,可以发现其中有9个软链接

恰好 lower 目录中有9个镜像层

在 lower 层中,处于最底层的则应该是在 : 最后的目录,即

/var/lib/docker/overlay2/ce13b630606113c23903890567e0d79301c3bddce03d1e4abe28e822415b0400

查看这一目录下的文件,可以发现它并没有 lower 文件

这一层对应的软链接即 link 文件内容为 YTESJVNLFIGI3C6A7OQFMQDIWT,我们查看其上一层的 lower 文件内容

可以发现确实对应了最底层目录的软链接

元数据

Docker 的元数据存储目录为 /var/lib/docker/image/overlay2

我们主要看 imagedb 和 layerdb 这两个文件夹

imagedb

这个文件夹中存储了镜像相关的元数据,具体位置是在 /imagedb/content/sha256 目录下,这个目录下的文件以 IMAGE ID 来命名

这个文件的内容就是我们通过 docker image inspect [IMAGE ID] 命令查看到的信息,其中我们关注 RootFS 字段

可以看到这个字段中有许多 sha256 值,这些哈希值称为 diff_id,其从上至下的顺序就表示镜像层最底层到最顶层,也就是说每个 diff_id 都对应了一个镜像层,实际上,对应每一个镜像层的还有另外两个 id:cache_idchain_id

  • cache_id 就是在 docker/overlay2 目录下看到的文件夹名称,也是我们通过 docker image inspect [IMAGE ID] 命令查看 GraphDriver 字段对应不同的 Dir,其本质是宿主机随机生成的uuid

  • chain_id 是通过 diff_id 计算出来的,是 Docker 内容寻址机制采用的索引 ID

    • chain_id 在目录 /docker/image/overlay2/layerdb/sha256 查看
    • 如果当前镜像层为最底层,则其 chain_id 与 diff_id 相同
    • 如果当前镜像层不是最底层,则其 chain_id 计算方式为:sha256(上层chain_id + " " + 本层diff_id)

这三个 id 之间存在一一对应的关系,我们可以通过 diff_id 计算得到 chain_id,又可以通过 chain_id 找到对应的 cache_id,下面我们举个栗子说明一下:

我们刚刚提到了 diff_id 从上至下是最底层到最顶层

查看 chain_id

可以看到其中确实有一个 chain_id 与 最底层的 diff_id 相同(红框标出),有了最底层的 chain_id 我们就可以计算出下一层的 chain_id,至于具体如何计算,以及如何通过 chain_id 找到对应的 cache_id,我们需要先了解 layerdb 目录下的内容

layerdb

我们现在已知 Docker 的镜像层作为只读层,容器层作为读写层,而 Docker 实际上定义了 roLayer 接口与 mountLayer 接口,分别用来描述(只读)镜像层与(读写)容器层,这两个接口的元数据就在目录 docker/image/overlay2/layerdb

roLayer

rolayer 接口用来描述镜像层,元数据的具体目录在 layerdb/sha256/ 下,在此目录下每个文件夹都以每个镜像层的 chain_id 命名

在文件夹中主要有这5个文件,我们简单介绍一下:

  • cache-id:当前 chain_id 对应的 cache_id,用来索引镜像层
  • diff:当前 chain_id 对应的 diff_id
  • parent:当前 chain_id 对应的镜像层的下一层(父层)镜像 chain_id,最底层不存在该文件
  • size:当前 chain_id 对应的镜像层物理大小,单位是字节
  • tar-split.json.gz:当前 chain_id 对应镜像层压缩包的 split 文件,可以用来还原镜像层的 tar 包,通过 docker save 命令导出镜像时会用到

我们在上一节中已经判断出了最底层镜像对应的 chain_id,不妨查看下对应目录下的文件

可以看到该目录下确实没有 parent 文件,那么我们再查看其下一层,通过 diff_id 的顺序我们可以得知其下一层的 diff_id 为 71931e5ac1f875e61a93d6c43aab8176bc1be6b38fed0e1681e5d38c196732a5,通过计算 sha256,我们可以得出下一层的 chain_id

计算得到最底层的下一层镜像 chain_id 为 7d06128b1d3a4f6aef4eae94afb7bdf3759f981e80e0dd0e4a4bc0cfa84a6640

确实存在该目录,证明计算无误,再查看此目录下 diff 文件与 parent 文件内容

可以看到与我们计算用到的两个值也完全相同

mountLayer

mountLayer 接口用来描述容器层,元数据的具体目录在 layerdb/mounts/,在此目录下的文件夹以每个容器的容器ID(CONTAINER ID)命名

在这个文件夹下只有3个文件,内容如下

简单介绍一下这3个文件:

  • init-id:对应容器 init 层目录名,源文件在 /var/lib/docker/overlay2 目录下
  • mount-id:容器层存储在 /var/lib/docker/overlay2 目录下的名称
  • parent:容器的镜像层最顶层镜像的 chain_id

我们可以查看 parent 这个文件中 chain_id 对应目录下的 diff 文件

根据 diff_id 从上至下的顺序,我们可以确定这个 diff_id 的确是镜像层的最顶层

在这里我们引入了一个叫做 init层 的概念,实际上,一个完整的容器分为3层:镜像层、init层和容器层,镜像层提供完整的文件系统基础(rootfs),容器层提供给用户进行交互操作与读写权限,而 init 层则是对应每个容器自己的一些系统配置文件,我们可以看一下 init 层的内容

可以看到在 diff 目录中有一些 /etc/hosts、/etc/resolv.conf 等配置文件,需要这一层的原因是当容器启动的时候,有一些每个容器特定的配置文件(例如 hostname),但由于镜像层是只读层无法进行修改,所以就在镜像层之上单独挂载一层 init 层,用户通过修改每个容器对应 init 层中的一些配置文件从而达到修改镜像配置文件的目的,而在 init 层中的配置文件也仅对当前容器生效,通过 docker commit 命令创建镜像时也不会提交 init 层

容器层

最后我们来看一看容器层的构造,刚刚我们在 mountLayer 一节的讲述中提到了 mount-id 这个文件,而这个文件的内容就是容器层目录的名称,我们通过 docker inspect [CONTAINER ID] 命令也可以判断

可以看到其实容器层的目录与镜像层、init层都在同一目录下,其实也就说明了他们在文件结构上都是相同的

同样都是这几个文件,但不同的是,我们可以看到在容器层确实有了 merged 这个目录,与我们在文章一开始实现的 overlayFS 是相同的

merged 目录

在 merged 目录下展现了完整的 rootfs 文件系统,这就是 overlay2 通过联合挂载技术,将镜像层、init层与容器层挂载到一起呈现的结果,这也是我们通过 docker exec 命令进入容器的交互式终端看到的结果,也就是所谓的视图

link & lower 文件

我们在镜像层的时候已经讲过这两个文件了,在容器层中这两个文件与镜像层作用是相同的,不过我们可以看一下 lower 文件的内容

前面讲过,lower 文件的内容是在此层之下的所有层的软链接名称,我们已知此镜像的镜像层共10层(lower 层9个,upper 层1个),但是我们从上图可以看到在容器层之下有11个其他层,那多出来的一个就是我们在上一节中提到的 init 层,init 层也有其对应的软链接(看上一节中的图),所以在 docker/overlay2/l 目录下实际上有12个软连接(10个镜像层,1个init层,1个容器层)

而通过 docker inspect [CONTAINER ID] 命令我们也可以判断出容器层是最顶层,其次是 init 层,最下面是镜像层,也对应了 lower 文件中软链接的顺序

diff 目录

这个目录实际上就是 overlayFS 文件结构中的 upper 层(上图中也能看到),所以它的用途就是保存用户在容器中(merged 层)对文件进行的编辑

很明显这些文件,例如 BitLocker 恢复密钥都不可能是镜像本身就有的,而是操作者在容器中后添加的,我们也可以自己尝试在容器中编辑一个文件

我们在容器内的 /etc 目录下创建了一个 test.txt 文件,可以看到在 diff 目录下也体现了出来,我们再尝试在容器中删除原本镜像自带的文件看一看效果

我们在容器中删除 /etc 目录下的 shadow 文件,可以看到在 diff 目录下的 /etc 中多了一个 shadow 文件,而这个文件实际上就是我们在文章一开始讲到的 whiteout 文件,用来隐藏我们已经删掉的 shadow 文件,而实际上镜像层的 shadow 文件并没有被删除

至此,我们对于 Docker 使用的 overlay2 文件结构分析结束。

后记

写这篇文章的最初原因是我在看了网上的一些文章后深有感触,就也想写一篇来记录自己学习的过程,然后不知不觉就写了一篇分析性文章(x,希望这篇文章的内容能帮助你对 Docker 的文件结构有进一步的了解,文章中其实也有很多是我自己在学习过程中的理解,我并不敢保证完全正确,所以如果您发现文章中的错误还请您一定要指出并联系我,我会第一时间进行改正。文章略长,非常感谢您能够看到这里,如果文章的内容能对您的学习带来或多或少的帮助,我会非常开心。

参考文章

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!