转载自
-
浅析 Docker overlay2 文件结构
-
docker(5): 层 朱双印
目的
-
有这个问题困惑着我:容器删除 docker rm 后,docker cp 到容器内的文件,在 /var/lib/docker/overlay2 目录中是否还会存在
-
答案:容器删除后 docker rm 后, 会自动删除 /var/lib/docker/overlay2 中对应的文件
-
实验步骤
-
docker run --name test-container -it busybox:latest sh # 主机拷贝文件到容器 docker cp test-file.txt test-container:/tmp # 去 /var/lib/docker/overlay2 查看是否有此文件 cd /var/lib/docker/overlay2 find ./ -name test-file.txt # 发现在容器运行时 该文件存在与 diff 和 merged 目录 # 容器内执行 exit 退出容器,容器会处于 exited 状态,或者 docker stop # 发现在容器停止或 exited 状态,此时该文件会存在于 diff 文件中 # 删除容器后,此文件就不存在了 # 因此就好奇这几个目录的意义 # 同时发现 /var/lib/docker/overlay2 中的 hash id 与容器 id 并不对应,因此考虑就和 overlay 文件系统有关了
-
给答案
-
#如下命令的返回信息较长,横向拖动滚动条查看完整信息 [root@kvm32docker2 ~]# docker inspect nginx-demo1 | jq '.[].GraphDriver' { "Data": { "LowerDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe-init/diff:/var/lib/docker/overlay2/c664c481c1a39215cde43d969f2649d260e925ae8d36d8fdac92053635214b45/diff:/var/lib/docker/overlay2/d9a6541fa1de8d066aa9a8352cf6e172dff35e86c2f79951c246c2db4cb7db07/diff:/var/lib/docker/overlay2/777bcd6002d1dde7ab312f356f9a195c614a91704d009ee755b50c67ca0acb1f/diff:/var/lib/docker/overlay2/7992c0933aeffbd9ff62c628df59b8095df980f95faecac0160234ca300f1f9a/diff:/var/lib/docker/overlay2/e5064a8306c3cc0e995031a618ad16be571a78a72651a12d3f5eb88ffd456998/diff:/var/lib/docker/overlay2/7053cd873a61626df3fb61f2448bade40072d04167008db291a7de1162fa3093/diff", "MergedDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/merged", "UpperDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/diff", "WorkDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/work" }, "Name": "overlay2" } # 分析 # 基础层 基础镜像 可以看出多个 init ,而且挂载很长,这是对应 From 的基础镜像(所有只读层) "LowerDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe-init/diff # 容器读写层 "UpperDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/diff", # 联合挂载层 将 基础层和容器读写层 挂载在一起 展示一个统一的视图 "MergedDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/merged", # 还不太清楚,应该就是个挂载作用 "WorkDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/work"
实例了解
通过上述实验,我们应该已经对overlay2有一定认识了,现在,我们来看看overlay2是怎样和docker中的镜像层以及容器层结合在一起的。
当我们通过docker pull
命令拉取一个镜像时,可以看到镜像每一层被拉取的过程(第一次拉取时可以看到,重复拉取时不会显示),示例如下
[root@kvm32docker2 ~]# docker pull nginx:latest
latest: Pulling from library/nginx
a2abf6c4d29d: Pull complete
a9edb18cadd1: Pull complete
589b7251471a: Pull complete
186b1aaa4aa6: Pull complete
b4df32aa5a72: Pull complete
a0bcbecc962e: Pull complete
Digest: sha256:0d17b565c37bcbd895e9d92315a05c1c3c9a29f762b011a10c54a66cd53c9b31
Status: Downloaded newer image for nginx:latest
docker.io/library/nginx:latest
如上可见,此处拉取的nginx:latest
镜像一共有6层,每一层拉取完毕后,都会显示Pull complete
,每一层都有一个ID号,比如上例中的a2abf6c4d29d
就是层的ID,这个ID其实是层的哈希值的前12位,你在参考本文进行实验时,看到的ID可能不一样,因为nginx:latest对应的镜像可能已经更新了。
下载镜像后,我们可以通过docker inspect
命令查看镜像的详细信息,在镜像的详细信息中找到RootFS段,可以看到当前镜像包含的层,如下
#为了查询出的信息可读性更强,我提前安装好了jq命令,jq命令是一个json格式化工具
[root@kvm32docker2 ~]# docker inspect nginx | jq '.[].RootFS'
{
"Type": "layers",
"Layers": [
"sha256:2edcec3590a4ec7f40cf0743c15d78fb39d8326bc029073b41ef9727da6c851f",
"sha256:e379e8aedd4d72bb4c529a4ca07a4e4d230b5a1d3f7a61bc80179e8f02421ad8",
"sha256:b8d6e692a25e11b0d32c5c3dd544b71b1085ddc1fddad08e68cbd7fda7f70221",
"sha256:f1db227348d0a5e0b99b15a096d930d1a69db7474a1847acbc31f05e4ef8df8c",
"sha256:32ce5f6a5106cc637d09a98289782edf47c32cb082dc475dd47cbf19a4f866da",
"sha256:d874fd2bc83bb3322b566df739681fbd2248c58d3369cb25908d68e7ed6040a6"
]
}
如上所示,nginx镜像的RootFS段中一共有6个层,这6个层就是刚才docker pull
拉取下来的层,RootFS中的每个层也是用一个哈希值表示的,细心如你肯定已经发现了,RootFS中的层的哈希值的前12位和刚才docker pull
命令中的ID根本就对应不上,这是因为docker pull
中显示的ID是层在压缩状态下计算出的哈希值,当层被下载到本地后,会自动解压,而RootFS中的层哈希值不是在压缩状态下计算的,所以它们两个是不一样的,如果想要确定它们之间的对应关系,可以通过/var/lib/docker/image/overlay2/distribution/diffid-by-digest或者v2metadata-by-diffid/sha256/
目录中的文件来查看它们之间的对应关系,此处就不赘述了。
在RootFS所显示的层中,第一行是最底层,最后一行是最上层,RootFS显示的层顺序和在镜像中的实际顺序是相反的,在上例中,2edcec3590a4是镜像的最底层,d874fd2bc83b是镜像的最上层。
既然这些层已经下载到本地,那么这些层对应的文件到底存放在哪里呢?我们通过如下命令,可以查看这些层的实际存放位置
#将如下命令中的nginx换成你想要查看的镜像名称即可,如下命令仍是借助docker inspect命令查询的,只是通过其他命令,对返回的信息进行了筛选和排序。
[root@kvm32docker2 ~]# docker inspect nginx -f '{{.GraphDriver.Data}}' | awk -v RS=' ' '{print}' | nl | sort -nr | cut -f2 | awk -v RS=':' '{print}' | grep diff
/var/lib/docker/overlay2/c664c481c1a39215cde43d969f2649d260e925ae8d36d8fdac92053635214b45/diff
/var/lib/docker/overlay2/d9a6541fa1de8d066aa9a8352cf6e172dff35e86c2f79951c246c2db4cb7db07/diff
/var/lib/docker/overlay2/777bcd6002d1dde7ab312f356f9a195c614a91704d009ee755b50c67ca0acb1f/diff
/var/lib/docker/overlay2/7992c0933aeffbd9ff62c628df59b8095df980f95faecac0160234ca300f1f9a/diff
/var/lib/docker/overlay2/e5064a8306c3cc0e995031a618ad16be571a78a72651a12d3f5eb88ffd456998/diff
/var/lib/docker/overlay2/7053cd873a61626df3fb61f2448bade40072d04167008db291a7de1162fa3093/diff
如上所示,这些层实际存放在/var/lib/docker/overlay2/层哈希值/diff
目录中,很明显,上述命令查询出的路径中的层哈希值和之前docker pull
或者RootFS
中显示的哈希值都不一样,上例路径中的的哈希值是根据一定规律,层层递进计算出来的,如果对这些哈希值之间的关系和计算方法感兴趣,可以去搜索“docker layerID diffID chainID cacheID”这些关键字,这并不是此处要关注的重点,所以不用纠结这些细节,我们只要知道,这些查出来路径就是镜像层实际的存放路径即可。
由于上例命令已经完成了排序,所以我们看到的层的顺序就是对应层在镜像中的位置,也就是说,上例中查询出的c664c481c1a3是最上层,对应RootFS中的d874fd2bc83b,上例中的7053cd873a61是最下层,对应RootFS中的2edcec3590a4,总之,镜像的层对应的文件实际存放在diff目录中。
其实聊了半天,无非都是在说镜像的层而已,现在咱们基于镜像,创建一个容器,看看容器层是怎么和镜像层结合的。
此处基于nginx:latest镜像创建一个nginx-demo1容器
[root@kvm32docker2 ~]# docker run --name nginx-demo1 -d nginx
使用docker inspect命令查看容器的详细信息,在详细信息的GraphDriver段可以看到容器的层信息
#如下命令的返回信息较长,横向拖动滚动条查看完整信息
[root@kvm32docker2 ~]# docker inspect nginx-demo1 | jq '.[].GraphDriver'
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe-init/diff:/var/lib/docker/overlay2/c664c481c1a39215cde43d969f2649d260e925ae8d36d8fdac92053635214b45/diff:/var/lib/docker/overlay2/d9a6541fa1de8d066aa9a8352cf6e172dff35e86c2f79951c246c2db4cb7db07/diff:/var/lib/docker/overlay2/777bcd6002d1dde7ab312f356f9a195c614a91704d009ee755b50c67ca0acb1f/diff:/var/lib/docker/overlay2/7992c0933aeffbd9ff62c628df59b8095df980f95faecac0160234ca300f1f9a/diff:/var/lib/docker/overlay2/e5064a8306c3cc0e995031a618ad16be571a78a72651a12d3f5eb88ffd456998/diff:/var/lib/docker/overlay2/7053cd873a61626df3fb61f2448bade40072d04167008db291a7de1162fa3093/diff",
"MergedDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/merged",
"UpperDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/diff",
"WorkDir": "/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/work"
},
"Name": "overlay2"
}
仔细观察上例的返回信息,你会发现,nginx-demo1容器其实就是使用了overlay2文件系统,将nginx镜像各个层的diff目录作为LowerDir只读层(在这个基础上添加了一层init只读层,之后再聊它),将容器的diff目录(9cd5a29ca37a文件夹中的diff目录)作为UpperDir可读可写层,叠加后呈现在了MergedDir层(MergedDir是9cd5a29ca37a文件夹中的merged目录),而我们在容器中看到的、操作的文件,其实就是MergedDir中的内容。
我们从宿主机的挂载信息中,也能侧面的印证这一点,在容器启动的情况下,执行如下命令,查看对应的overlay2挂载点信息
#根据容器目录9cd5a29ca37a过滤出对应的overlay2挂载点,如下挂载信息非常长,因为层多、路径多
[root@kvm32docker2 ~]# mount | grep overlay | grep 9cd5a29ca37a
overlay on /var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/SEAP5TZNN4RBH5JBBDBLLJ26MW:/var/lib/docker/overlay2/l/RVDTF4D6NJS2IG4Z2MY2JMUYIP:/var/lib/docker/overlay2/l/CKCDGBMS6PLF2UHMI5JL4V72KD:/var/lib/docker/overlay2/l/F54PBI7MRL27F2VVQD3PGEN2AR:/var/lib/docker/overlay2/l/2RMPUKDAWMIIMZP4EWIFM6VW2V:/var/lib/docker/overlay2/l/DWALAR6VTJ7KTODYNTFYWTFJT3:/var/lib/docker/overlay2/l/HD4DT3Z4CWMSWGQODMGVDPACT4,upperdir=/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/diff,workdir=/var/lib/docker/overlay2/9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe/work)
#上述挂载点中的有很多/var/lib/docker/overlay2/l/下的路径,查看这些路径,会发现这些路径都是软链接,软连接指向的路径就是那些diff目录
[root@kvm32docker2 ~]# ll /var/lib/docker/overlay2/l/SEAP5TZNN4RBH5JBBDBLLJ26MW
lrwxrwxrwx 1 root root 77 Apr 4 12:45 /var/lib/docker/overlay2/l/SEAP5TZNN4RBH5JBBDBLLJ26MW -> ../9cd5a29ca37a208659e5ce8954ed145019e91a1be2d28c257e1a46fc0166c5fe-init/diff
[root@kvm32docker2 ~]#
[root@kvm32docker2 ~]# ll /var/lib/docker/overlay2/l/RVDTF4D6NJS2IG4Z2MY2JMUYIP
lrwxrwxrwx 1 root root 72 Apr 3 13:14 /var/lib/docker/overlay2/l/RVDTF4D6NJS2IG4Z2MY2JMUYIP -> ../c664c481c1a39215cde43d969f2649d260e925ae8d36d8fdac92053635214b45/diff
看到这里,我们应该已经能够完全理解镜像层、容器层、overlay2文件系统是怎样融合在一起的了,首先,镜像下载到本地后,各个镜像层的文件存放在对应diff目录中,当我们基于镜像创建容器时,docker引擎会为容器创建对应的各种目录,比如diff、work、merged目录,然后将镜像层的diff目录作为overlay中的lowerDir,将容器的diff目录作为overlay中upperDir,将叠加后的结构挂载到了merged目录中,最后,docker通过mount namespace
技术,将merged目录隔离挂载到容器中。
现在,再看下图是不是一目了然了
你可以做一些实验,比如,在容器中创建一些文件,修改一些文件,看看容器的diff目录中的变化情况,因为容器的diff目录就是读写层,当在容器中进行写操作时,最直接的变化会体现到容器的diff目录中,但是,你可能会遇到一些“意外情况”,比如,你在容器中修改了/etc/hosts文件,发现容器的diff目录中并没有对应的/etc/hosts文件出现,这是因为有一个特殊的层存在,这个层就是我们刚才看到的”-init层”。当我们创建一个容器时,docker会为容器进行一些初始化工作,其中就包括生成hosts信息、生成hostname等,你会发现,即使你在容器中修改了/etc/host文件,重启容器后,hosts文件也会变成原来的样子(通过其他方法可以永久修改),因为/etc/hosts、/etc/hostname、/etc/resolv.conf文件中的信息都是docker生成的,docker认为这些信息应该是针对容器当前的状态而存在的,以hosts文件为例来说,如果容器没有固定的IP地址,那么重启容器后,容器的IP可能会发生变化,所以每次重启容器时docker都会重新生成hosts内容,避免之前生成的hosts与当前状态所需要的hosts不符,当我们在容器中修改/etc/hosts文件时,会发现宿主机中的/var/lib/docker/containers/容器ID/
目录下的hosts文件内容也发生了同样的变化,其实,docker就是将宿主机中的/var/lib/docker/containers/容器ID/hosts
文件挂载到了容器中的,既然这些状态应该属于容器,那么当我们基于容器创建镜像时,就不应该把容器中的这些信息带入到新创建的镜像中,当我们使用docker commit
命令基于容器创建镜像时,会把容器的可读写层变成新创建出的镜像的最上层,所以,如果容器的可读写层中包含hosts文件,新镜像中就会带入容器的hosts信息,而容器因为init层和挂载操作的存在,避免了这些信息进入到容器的可读写层,所以可以保障我们基于容器创建镜像时,得到的镜像是“纯净”的。
前言
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 文件系统进行简单的操作演示以便更深入理解不同层不同目录之间的关系
先创建几个文件夹和文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tp8WaGpW-1682672366012)(null)]
使用 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 目录下的文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-imT8ySZi-1682672362681)(https://i.loli.net/2021/11/20/5otWidQEHlUurZb.png “image-20211118001443155”)]
可以看到我们原本 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 目录,我们可以核实一下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HjfOwiYD-1682672362682)(https://i.loli.net/2021/11/20/KMoC1Eg9PqunfhQ.png “image-20211118002859548”)]
接下来我们可以看下 upper层、lower层 和 merged层之间的关系,上文已经提到了 upper 层是 读写层 而 lower 层是 只读层,merged 层是联合挂载后的视图,那如果我们在 merged 层中对文件进行操作会发生什么
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NEvgE9Tq-1682672362682)(https://i.loli.net/2021/11/18/jL9YmWBHUxXrEgv.png “image-20211118004407383”)]
我们修改 merged 层的 a.txt
文件,可以看到 merged 层的 a.txt
内容虽然改变,但 A 目录(只读层)下的 a.txt
内容并没有发生变化,而在 C 目录(读写层)下多了一个 a.txt
文件,内容就是我们修改过的 a.txt
的内容,这就是只读层与读写层的关系,在 merged 目录对文件进行修改并不会影响到只读层的源文件,只会在读写层进行编辑
如果我们在 merged 目录删除文件会发生什么
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WdckM0dU-1682672362683)(https://i.loli.net/2021/11/18/VIGR95wUb3xY1vX.png “image-20211118005920074”)]
可以看到在 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
命令查看
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KOChaM8B-1682672362683)(https://i.loli.net/2021/11/18/dlq7umCVpxRyDkH.png “image-20211118195149105”)]
在 Docker 中,我们日常操作主要涉及两个层面:镜像层与容器层,镜像层就是我们通过 docker pull
等命令下载到本机中的镜像,而容器层则是我们通过 docker exec
等命令进入的交互式终端,如果你使用过 Docker,你会发现我们只用一个镜像,通过 docker run
可以产生很多个容器,这就可以类比 upper 与 lower 两层,镜像作为 lower 层,只读提供文件系统基础,而容器作为 upper 层,我们可以在其中进行任意文件操作,只用同一个镜像就可以引申出不同的容器,这也是一种节约空间资源的方式吧(我的推测
接下来我们稍微详细地探讨下镜像层与容器层,还有他们的元数据
镜像层
我们可以通过 docker image inspect [IMAGE ID]
来查看镜像配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YNsWOsNg-1682672366094)(null)]
其中的 GraphDriver
字段中有关于 overlay2 文件结构的目录信息
每一层的对应都在配置信息中体现的非常清楚,但是有一点问题,我们在实际查看文件夹的时候,可以发现镜像层其实并没有 /merged 目录,我的理解是镜像层作为 Docker 容器的最底层(只读层)并不需要有视图的功能,我们在实际使用过程中也并不会直接对镜像进行操作,所以其在配置信息中显示也仅仅是为了呈现完整的 overlay2 文件结构(不一定对
可以看到镜像的目录是在 /var/lib/docker/overlay2
下,我们打开一个镜像层看一看其中都有哪些文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ObUIbbA9-1682672365870)(null)]
其中我们关注下 diff 目录、link 和 lower 文件
diff 目录
在这个目录中存放的是当前镜像层的文件,刚刚在介绍 over个lay2 与 overlay 区别的时候提到了 overlay2 是将多个 lower 层联合到一起,在上面的图中也可以看到,多个 lower 层之间用 :
分割,在这些层中每一层都有一部分文件,把他们联合到一起就得到了完整的 rootfs
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gemwVj7S-1682672362684)(https://i.loli.net/2021/11/18/8rJgxuARq27WImK.png “image-20211118205543708”)]
link 文件
link 文件中的内容是当前层的软链接名称
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-57o6mPpU-1682672366570)(null)]
这些链接都在 /var/lib/docker/overlay2/l
目录下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PHo86VtO-1682672373088)(null)]
使用软链接的目的是为了避免达到 mount 命令参数的长度限制
lower 文件
lower 文件中的内容是在此层之下的所有层的软链接名称,最底层不存在该文件,我们知道 upper 层在 lower 层之上,而 lower 层中越靠后则越在底层
我们查看 upper 层对应目录下 lower 文件,可以发现其中有9个软链接
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O2FqMgaq-1682672367645)(null)]
恰好 lower 目录中有9个镜像层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IXgyaJSG-1682672362685)(https://i.loli.net/2021/11/18/8jsqANeiYLzMQCt.png “image-20211118211011290”)]
在 lower 层中,处于最底层的则应该是在 :
最后的目录,即
/var/lib/docker/overlay2/ce13b630606113c23903890567e0d79301c3bddce03d1e4abe28e822415b0400
查看这一目录下的文件,可以发现它并没有 lower 文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gWyD1JFA-1682672362686)(https://i.loli.net/2021/11/18/5nVyADHcI6jkG7L.png “image-20211118211248371”)]
这一层对应的软链接即 link 文件内容为 YTESJVNLFIGI3C6A7OQFMQDIWT
,我们查看其上一层的 lower 文件内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2GMMs4UM-1682672372866)(null)]
可以发现确实对应了最底层目录的软链接
元数据
Docker 的元数据存储目录为 /var/lib/docker/image/overlay2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reX7vvf8-1682672374132)(null)]
我们主要看 imagedb 和 layerdb 这两个文件夹
imagedb
这个文件夹中存储了镜像相关的元数据,具体位置是在 /imagedb/content/sha256
目录下,这个目录下的文件以 IMAGE ID 来命名
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K9VMpc1x-1682672365899)(null)]
这个文件的内容就是我们通过 docker image inspect [IMAGE ID]
命令查看到的信息,其中我们关注 RootFS
字段
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-783xJXiK-1682672365986)(null)]
可以看到这个字段中有许多 sha256 值,这些哈希值称为 diff_id,其从上至下的顺序就表示镜像层最底层到最顶层,也就是说每个 diff_id 都对应了一个镜像层,实际上,对应每一个镜像层的还有另外两个 id:cache_id 和 chain_id
- cache_id 就是在
docker/overlay2
目录下看到的文件夹名称,也是我们通过docker image inspect [IMAGE ID]
命令查看 GraphDriver 字段对应不同的 Dir,其本质是宿主机随机生成的uuid
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nn1rffeD-1682672362687)(https://i.loli.net/2021/11/19/OI4WQMADjXGYkwh.png “image-20211119020142768”)]
-
chain_id 是通过 diff_id 计算出来的,是 Docker 内容寻址机制采用的索引 ID
- chain_id 在目录
/docker/image/overlay2/layerdb/sha256
查看 - 如果当前镜像层为最底层,则其 chain_id 与 diff_id 相同
- 如果当前镜像层不是最底层,则其 chain_id 计算方式为:
sha256(上层chain_id + " " + 本层diff_id)
- chain_id 在目录
这三个 id 之间存在一一对应的关系,我们可以通过 diff_id 计算得到 chain_id,又可以通过 chain_id 找到对应的 cache_id,下面我们举个栗子说明一下:
我们刚刚提到了 diff_id 从上至下是最底层到最顶层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RX7kiS5F-1682672362688)(https://i.loli.net/2021/11/19/c29yqJEOmH8saUl.png “image-20211119123031473”)]
查看 chain_id
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5cE27PGL-1682672362688)(https://i.loli.net/2021/11/19/ETR3p9yD4kzu27W.png “image-20211119135304490”)]
可以看到其中确实有一个 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 命名
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5XmfRW4q-1682672362688)(https://i.loli.net/2021/11/19/Mg2jQLAUfTsluF4.png “image-20211119143623782”)]
在文件夹中主要有这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,不妨查看下对应目录下的文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H9dXU0fx-1682672366510)(null)]
可以看到该目录下确实没有 parent 文件,那么我们再查看其下一层,通过 diff_id 的顺序我们可以得知其下一层的 diff_id 为 71931e5ac1f875e61a93d6c43aab8176bc1be6b38fed0e1681e5d38c196732a5
,通过计算 sha256,我们可以得出下一层的 chain_id
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97n3O4c7-1682672362689)(https://i.loli.net/2021/11/19/Sqsa6ZOcfkFKmGi.png “image-20211119170444704”)]
计算得到最底层的下一层镜像 chain_id 为 7d06128b1d3a4f6aef4eae94afb7bdf3759f981e80e0dd0e4a4bc0cfa84a6640
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f8aUHDgj-1682672362689)(https://i.loli.net/2021/11/19/a3kKALce9jr4n6Y.png “image-20211119170601998”)]
确实存在该目录,证明计算无误,再查看此目录下 diff 文件与 parent 文件内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DTouVpyk-1682672362689)(https://i.loli.net/2021/11/19/a5v8j9FGIBdzPno.png “image-20211119170809996”)]
可以看到与我们计算用到的两个值也完全相同
mountLayer
mountLayer 接口用来描述容器层,元数据的具体目录在 layerdb/mounts/
,在此目录下的文件夹以每个容器的容器ID(CONTAINER ID)命名
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5TDmiUJi-1682672362690)(https://i.loli.net/2021/11/19/4ZpF8YCRETPnaeJ.png “image-20211119172022786”)]
在这个文件夹下只有3个文件,内容如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zmT64o30-1682672365961)(null)]
简单介绍一下这3个文件:
- init-id:对应容器 init 层目录名,源文件在
/var/lib/docker/overlay2
目录下 - mount-id:容器层存储在
/var/lib/docker/overlay2
目录下的名称 - parent:容器的镜像层最顶层镜像的 chain_id
我们可以查看 parent 这个文件中 chain_id 对应目录下的 diff 文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ch0IppxT-1682672362690)(https://i.loli.net/2021/11/19/VZzcd8PQyXGnu3r.png “image-20211119174857477”)]
根据 diff_id 从上至下的顺序,我们可以确定这个 diff_id 的确是镜像层的最顶层
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Rk94fKFx-1682672362691)(https://i.loli.net/2021/11/19/cLuFSqTQjvktWR2.png “image-20211119175029637”)]
在这里我们引入了一个叫做 init层 的概念,实际上,一个完整的容器分为3层:镜像层、init层和容器层,镜像层提供完整的文件系统基础(rootfs),容器层提供给用户进行交互操作与读写权限,而 init 层则是对应每个容器自己的一些系统配置文件,我们可以看一下 init 层的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qk6L5NLr-1682672362691)(https://i.loli.net/2021/11/19/SApRa8mM2xbwyoH.png “image-20211119204824180”)]
可以看到在 diff 目录中有一些 /etc/hosts、/etc/resolv.conf 等配置文件,需要这一层的原因是当容器启动的时候,有一些每个容器特定的配置文件(例如 hostname),但由于镜像层是只读层无法进行修改,所以就在镜像层之上单独挂载一层 init 层,用户通过修改每个容器对应 init 层中的一些配置文件从而达到修改镜像配置文件的目的,而在 init 层中的配置文件也仅对当前容器生效,通过 docker commit
命令创建镜像时也不会提交 init 层
容器层
最后我们来看一看容器层的构造,刚刚我们在 mountLayer 一节的讲述中提到了 mount-id 这个文件,而这个文件的内容就是容器层目录的名称,我们通过 docker inspect [CONTAINER ID]
命令也可以判断
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VEwrK9h4-1682672362691)(https://i.loli.net/2021/11/19/bjx5iMPUS4rwBF8.png “image-20211119213012192”)]
可以看到其实容器层的目录与镜像层、init层都在同一目录下,其实也就说明了他们在文件结构上都是相同的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DziIU66j-1682672366538)(null)]
同样都是这几个文件,但不同的是,我们可以看到在容器层确实有了 merged 这个目录,与我们在文章一开始实现的 overlayFS 是相同的
merged 目录
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HQDAznum-1682672362692)(https://i.loli.net/2021/11/19/wbqWYVg1Akmt56h.png “image-20211119213337874”)]
在 merged 目录下展现了完整的 rootfs 文件系统,这就是 overlay2 通过联合挂载技术,将镜像层、init层与容器层挂载到一起呈现的结果,这也是我们通过 docker exec
命令进入容器的交互式终端看到的结果,也就是所谓的视图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UIeEGgpl-1682672365929)(null)]
link & lower 文件
我们在镜像层的时候已经讲过这两个文件了,在容器层中这两个文件与镜像层作用是相同的,不过我们可以看一下 lower 文件的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MAJQV9xl-1682672366900)(null)]
前面讲过,lower 文件的内容是在此层之下的所有层的软链接名称,我们已知此镜像的镜像层共10层(lower 层9个,upper 层1个),但是我们从上图可以看到在容器层之下有11个其他层,那多出来的一个就是我们在上一节中提到的 init 层,init 层也有其对应的软链接(看上一节中的图),所以在 docker/overlay2/l
目录下实际上有12个软连接(10个镜像层,1个init层,1个容器层)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4SDBvSXi-1682672362695)(https://i.loli.net/2021/11/19/DfzIGhBEHpx821V.png “image-20211119223350790”)]
而通过 docker inspect [CONTAINER ID]
命令我们也可以判断出容器层是最顶层,其次是 init 层,最下面是镜像层,也对应了 lower 文件中软链接的顺序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EagwXsIy-1682672366040)(null)]
diff 目录
这个目录实际上就是 overlayFS 文件结构中的 upper 层(上图中也能看到),所以它的用途就是保存用户在容器中(merged 层)对文件进行的编辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KkP1KPOY-1682672362696)(https://i.loli.net/2021/11/19/3kemVqYcljs2tb8.png “image-20211119224922222”)]
很明显这些文件,例如 BitLocker 恢复密钥都不可能是镜像本身就有的,而是操作者在容器中后添加的,我们也可以自己尝试在容器中编辑一个文件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXrhj9vP-1682672362697)(https://i.loli.net/2021/11/19/gfvz7EBZow4q89C.png “image-20211119225724753”)]
我们在容器内的 /etc 目录下创建了一个 test.txt 文件,可以看到在 diff 目录下也体现了出来,我们再尝试在容器中删除原本镜像自带的文件看一看效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mBdrVceC-1682672366069)(null)]
我们在容器中删除 /etc 目录下的 shadow 文件,可以看到在 diff 目录下的 /etc 中多了一个 shadow 文件,而这个文件实际上就是我们在文章一开始讲到的 whiteout 文件,用来隐藏我们已经删掉的 shadow 文件,而实际上镜像层的 shadow 文件并没有被删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X1AL0heY-1682672362699)(https://i.loli.net/2021/11/19/b3oaVpzG2nBhqCk.png “image-20211119230835906”)]
至此,我们对于 Docker 使用的 overlay2 文件结构分析结束。
后记
写这篇文章的最初原因是我在看了网上的一些文章后深有感触,就也想写一篇来记录自己学习的过程,然后不知不觉就写了一篇分析性文章(x,希望这篇文章的内容能帮助你对 Docker 的文件结构有进一步的了解,文章中其实也有很多是我自己在学习过程中的理解,我并不敢保证完全正确,所以如果您发现文章中的错误还请您一定要指出并联系我,我会第一时间进行改正。文章略长,非常感谢您能够看到这里,如果文章的内容能对您的学习带来或多或少的帮助,我会非常开心。