docker系列文章直达链接:docker专题
在docker中,镜像是分层的,一个镜像是由一个层或多个层组成的,每个层都有对应的数据,镜像中的完整数据是由所有层的数据组合而成的,这样说可能不太容易理解,不如咱们先把镜像分层的概念放一边,先来看一个魔术玩具,这个魔术玩具也是分层的,通过这个玩具去类比着理解镜像的层,会比较直观一点,下图就是这个玩具的照片

上图中的魔术玩具由很多塑料透明卡片组成,每个卡片上都印有很多扑克牌图案,一个卡片被分成很多格子,有的格子中印有扑克的图案,有的格子中没有,有扑克图案的格子是白色底色的,没有扑克图案的格子是透明的,由于每张卡片中所印刷的扑克图案的位置和花色都不是完全相同的,所以,当把多张卡片叠加在一起时,就会出现“叠加混合”的展示效果,如下图
当多个卡片叠加在一起时,会看到一个由很多扑克图案混合组成的“整体”,上层卡片中的扑克图案会挡住下层卡片中的图案,上层卡片中的透明部分会显示出下层卡片中的扑克图案,最终,呈现在我们眼中的是一个“叠加混合的图案”。
如果觉得我描述的不够清楚,可以在网上搜索“思维透视卡魔术”,有一些视频,可以更加直观的看到这种魔术玩具的样子。
其实,docker镜像的结构和这个玩具的结构很像,镜像的结构也是分层的,一个镜像由一层或多层组成,docker镜像中的“一层”就像这个玩具的“一张卡片”一样,不同的层中有不同的数据,就像不同的卡片中有不同的扑克图案一样,当把这些镜像层“叠加混合”在一起后,就形成了我们看到的镜像,镜像中的数据来自各个层,镜像的示意图如下:
示意图中的镜像由两个层组成,镜像层A和镜像层B,A层和B层中分别有不同的数据(示意图中,绿色图标代表文件夹,白色图标代表文件),当我们把A层和B层”叠加混合”后,就组成了我们看到的镜像,换句话说就是,镜像中的数据其实都来自于组成镜像的各个层。
上图中,如果在A层和B层中有同名的文件(绝对路径相同的两个文件),那么在镜像中只能看到B层中的文件,因为B层在A层的上面,上层的同名文件会遮挡住下层中的同名文件,最终展示到镜像中,如果A层和B层中有同样的文件夹(绝对路径相同的两个目录),那么两层中这个目录下的文件会合并到一起展示到镜像中。
示意图中,A层中的文件可能是系统文件(比如centos7、ubuntu的相关文件),B层中的文件可能是nginx或者redis的相关文件,当把A层和B层中的文件“叠加混合”后,就变成了一个可运行nginx或者可运行redis的镜像。
当我们基于一个镜像创建容器后,会发现,无论在已有容器中怎样修改数据,都不会影响镜像中的数据,也不会影响到基于同一个镜像启动的其他容器,当基于镜像创建新的容器时,刚刚新创建的容器中的数据总是和镜像中的数据是一样的,这是怎样做到的呢?
这是因为,镜像中的层是“只读”的,我们无法修改镜像只读层中的数据,镜像层中的数据只能读取出来使用,当我们基于镜像启动一个容器时,会在镜像只读层的基础上添加一个新的层,这个新的层是“可读可写”的,这个“读写层”不属于镜像,只属于当前容器,在容器中的操作都在这个“读写层”中完成的,而我们在容器中看到的数据,是由“读写层”和“只读层”叠加后呈现出来的,举个例子:
假设,一个镜像由两层组成,这两层是只读的,示意图如下
上图中,A层中有两个文件,File1和File2,B层中也有两个文件,File2和File3,当这些层叠加后,我们会看到三个文件,File1、File2和File3,我们眼中看到的File2是B层中的File2,因为之前说过,如果两个层中有同名的文件,那么最终以上层中的文件内容为准。
当我们基于这个镜像启动一个容器时,会在镜像“只读层”的上方创建一个“容器层”,这个容器层是可读可写的。
当我们在容器中修改File1时,并不会直接修改镜像层中的File1文件,而是会先把镜像层中的File1复制一份到容器层,然后修改容器层中的副本。
这种不在源文件上修改,而是在需要修改时复制出副本,在副本上进行修改的方法,被称之为“写时复制”,写时复制的英文为copy-on-write,首字母简称COW。
由于容器层在镜像层的上面,所以,当镜像层和容器层叠加后,我们看到的File1其实是容器层中的File1(以上层为准)。
同理,如果想要在容器中修改File2,则会先从镜像层B中将File2拷贝到容器层(A层、B层都有File2,但是以上层为准,所以从B层拷贝File2),然后修改容器层中的副本,如下图所示
如果我们想要在容器中删除File3,并不会直接删除镜像层中的File3,而是在容器层中,标记镜像层中的File3不可见。
如上图所示,当我们在容器中删除File3时,容器层会为File3创建一个whiteout文件,这个whiteout文件就是用来标记File3文件不可用的,镜像层中的File3文件仍然存在,只是我们看不到了。我在翻译网站上翻译whiteout这个词,看到的解释是“临时性失明”。
如果我们想要在容器中创建一个新文件File4,那么File4会在容器层中被创建。
总之,镜像中的层是只读的,可以理解为只用来提供原始数据,如果牵扯到“写”的操作,都是在容器层中完成的,当基于同一个镜像启动多个容器时,镜像层是共享的,容器层则属于各个容器本身。
上面说的都是原理,在实际的应用中,怎样实现这种分层结构呢?如果想要实现这种分层的结构,需要借助一种叫“联合文件系统”的东西,联合文件系统有很多种,常见的有aufs、device mapper、btrfs等等,曾经docker默认使用的联合文件系统是aufs,现在,docker默认使用的是overlayfs,overlayfs是一种堆叠文件系统,它建立在其他文件系统(xfs或ext4)之上,overlayfs有两个版本,overlay和overlay2,docker推荐默认使用overlay2,使用overlay2的前提是,linux内核版本需要大于等于4.0,或者使用的RHEL和CentOS的版本是3.10.0-514的更高版本,我在centos7.9的测试机中执行uname -r命令,执行结果如下,满足使用overlay2的使用要求
[root@kvm32docker2 ~]# uname -r
3.10.0-1160.el7.x86_64
在docker主机上执行docker info命令,可以看到当前使用的存储驱动是overlay2
[root@kvm32docker2 ~]# docker info | grep -i "storage"
Storage Driver: overlay2
我们最好来实际使用一下overlay2文件系统,以便更好的理解它的分层结构,但是,在使用它之前,需要先了解一些overlay2的相关概念,overlay2是一个叠加文件系统,它能把不同层的数据混合叠加在一起,通过一个统一的视图展现给用户,在统一的视图下,就像在其他文件系统中一样,用户可以对文件进行各种增删改查操作。在overlay2中,不同的层有不同的作用,不同位置的层被分类,并按照一定的规则运行,这么说不太容易理解,不用害怕,其实它的运行规则和大部分概念我们都已经理解了,只是换了个名字罢了,我们来看一下下面这张overlay概念图
上图中一共有三层,最下层在overlay文件系统中被称为”lower层”,lower层中的数据是“只读”的,也就是说,这些数据只能被查看,不能被修改,在overlay2中,lower层可以是一层,也可以是多层,lower层上面的一层被称之为”upper层”,upper层是可读可写层,在upper层之上还有一层,称之为”merged层”,merged层就是用户看到的叠加混合后的视图,聪明如你肯定已经看出来了,这些在overlay中的概念,正与刚才所说的镜像层和容器层的概念对应,lower层对应镜像层,upper层对应容器层,merged层对应我们最终看到的数据。
那么现在我们就来创建一些测试文件,实际感受一下overlay2,在docker主机上执行如下命令
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower{1,2,3}
[root@kvm32docker2 ~]# mkdir -p /tmp/test/{work,upper,merged}
[root@kvm32docker2 ~]# ls /tmp/test/
lower1 lower2 lower3 merged upper work
我创建了一个测试目录/tmp/test/,用于测试overlay2,在测试目录中,lower1、lower2、lower3三个目录用于模拟lower层,upper目录和merged目录分别模拟upper层和merged层,work目录是overlay文件系统需要使用的临时工作目录,在挂载时需要使用,准备好对应的目录后,在不同的lower目录中创建一些文件和目录,模拟不同的层中的文件
[root@kvm32docker2 ~]# echo 'file1 - lower1' > /tmp/test/lower1/file1
[root@kvm32docker2 ~]# echo 'file2 - lower1' > /tmp/test/lower1/file2
[root@kvm32docker2 ~]# echo 'file2 - lower2' > /tmp/test/lower2/file2
[root@kvm32docker2 ~]# echo 'file3 - lower3' > /tmp/test/lower3/file3
[root@kvm32docker2 ~]#
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower2/tdir
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower3/tdir
[root@kvm32docker2 ~]# echo 'file1 - tdir - lower2' > /tmp/test/lower2/tdir/file1
[root@kvm32docker2 ~]# echo 'file2 - tdir - lower3' > /tmp/test/lower3/tdir/file2
[root@kvm32docker2 ~]#
[root@kvm32docker2 ~]# tree /tmp/test
/tmp/test
├── lower1
│ ├── file1
│ └── file2
├── lower2
│ ├── file2
│ └── tdir
│ └── file1
├── lower3
│ ├── file3
│ └── tdir
│ └── file2
├── merged
├── upper
└── work
8 directories, 6 files
测试文件创建完毕后,使用如下命令创建一个overlay2挂载点。
#为了把挂载命令写的尽量简短一些,这里使用相对路径进行挂载,先进入测试目录,以便使用相对路径
# cd /tmp/test
#执行如下mount命令,指定使用overlay文件系统进行挂载,通过-o选项指定挂载选项
# mount -t overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work overlay merged
#上述命令的lowerdir选项用于指定lower层对应的目录,
#在overlay2中,lower层可以是一层,也可以是多层,
#如果是多层,每层之间用冒号':'隔开,冒号左侧的层高于冒号右侧的层,
#上例中,lower1层高于lower2层高于lower3层,
#upperdir选项用于指定upper层对应的目录,
#workdir选项用于指定overlay的临时工作目录,
#最后指定通过overlay的方式将数据混合到merged目录中。
完成上述操作后,可以通过mount命令确定一下已经正常挂载
[root@kvm32docker2 test]# mount | grep overlay | grep /tmp/test
overlay on /tmp/test/merged type overlay (rw,relatime,lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work)
正常挂载后,进入merged目录,目录结构和文件内容内容如下
[root@kvm32docker2 merged]# pwd
/tmp/test/merged
[root@kvm32docker2 merged]# tree
.
├── file1
├── file2
├── file3
└── tdir
├── file1
└── file2
1 directory, 5 files
[root@kvm32docker2 merged]# cat file1
file1 - lower1
[root@kvm32docker2 merged]# cat file2
file2 - lower1
[root@kvm32docker2 merged]# cat file3
file3 - lower3
[root@kvm32docker2 merged]# cat tdir/file1
file1 - tdir - lower2
[root@kvm32docker2 merged]# cat tdir/file2
file2 - tdir - lower3
正如之前所说,高层中的文件会遮挡住低层中的文件,所以,我们最终看到的file2是lower1中的file2,当不同的层中有相同的目录时,目录中的文件会合并到目录下。
现在,我们在merged目录中修改一下file1文件的内容,将原有内容改为如下内容
注意:如下所有操作均在merged目录中进行
[root@kvm32docker2 merged]# vim file1
[root@kvm32docker2 merged]# cat file1
file1 - merged
修改merged/file1以后,我们查看一下lower1/file1,如下,lower1/file1中的内容没有发生任何改变。
[root@kvm32docker2 merged]# cat ../lower1/file1
file1 - lower1
这是因为lower层中的数据对于merged层来说,是只读的,当merged层想要修改来自lower层中的文件时,会利用刚才提到的写时复制技术,将lower层中的文件拷贝到upper层中,在upper层中对拷贝的副本进行修改,最终,我们看到的是upper层中修改过的副本,如下
[root@kvm32docker2 merged]# ls ../upper/
file1
[root@kvm32docker2 merged]# cat ../upper/file1
file1 - merged
如果再次修改merged/file1,其实是直接在upper/file1上修改的,因为upper层是可读可写层,可以直接修改其中的文件。示例如下
[root@kvm32docker2 merged]# vim file1
[root@kvm32docker2 merged]# cat file1
file1 - merged
file1 - merged
[root@kvm32docker2 merged]# cat ../upper/file1
file1 - merged
file1 - merged
这里需要注意的是,如果直接在lower目录中修改文件,也是可以修改的(相当于直接在xfs或者ext4文件系统中进行写操作,写操作没有经过overlay文件系统),不要这样操作,这里所谓的lower层“只读”,是针对merged层来说的,当我们在merged层中修改来自lower层中的文件时,这些lower层中的文件是“只读”的,需要借助COW机制来完成修改。
现在,我们在merged层中,删除file3文件,可以发现upper层中多了一个file3的whiteout文件
[root@kvm32docker2 merged]# ls
file1 file2 file3 tdir
[root@kvm32docker2 merged]# rm file3
rm: remove regular file ‘file3’? y
[root@kvm32docker2 merged]# ls
file1 file2 tdir
[root@kvm32docker2 merged]# ll ../upper/
total 4
-rw-r--r-- 1 root root 30 Apr 2 11:46 file1
c--------- 1 root root 0, 0 Apr 2 12:04 file3
可见whiteout文件是一种文件类型为字符设备的文件,它的主次设备号均为0,那么我们在upper层手动创建一个whiteout文件,看看merged层的变化。
[root@kvm32docker2 merged]# pwd
/tmp/test/merged
[root@kvm32docker2 merged]# ls
file1 file2 tdir
[root@kvm32docker2 merged]# mknod ../upper/tdir c 0 0
[root@kvm32docker2 merged]# ls
file1 file2
如上,我们使用mknod命令在upper层手动创建了一个whiteout文件,这个whiteout是针对tdir创建的,whiteout文件创建后,merged层不再显示tdir目录了。
如果在merged层创建一个全新的文件,其实是直接在upper层创建的,后面的更改或者删除操作也是直接在upper层中进行,这里就不演示了。
总之,merged层就是我们看到的视图,我们所做的操作也应该在这个层中进行,其实,容器中最终挂载的就是merged层。
通过上述实验,我们应该已经对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层和挂载操作的存在,避免了这些信息进入到容器的可读写层,所以可以保障我们基于容器创建镜像时,得到的镜像是“纯净”的。
我先歇歇~,兄弟萌先看着~
docker系列文章直达链接:docker专题
在docker中,镜像是分层的,一个镜像是由一个层或多个层组成的,每个层都有对应的数据,镜像中的完整数据是由所有层的数据组合而成的,这样说可能不太容易理解,不如咱们先把镜像分层的概念放一边,先来看一个魔术玩具,这个魔术玩具也是分层的,通过这个玩具去类比着理解镜像的层,会比较直观一点,下图就是这个玩具的照片

上图中的魔术玩具由很多塑料透明卡片组成,每个卡片上都印有很多扑克牌图案,一个卡片被分成很多格子,有的格子中印有扑克的图案,有的格子中没有,有扑克图案的格子是白色底色的,没有扑克图案的格子是透明的,由于每张卡片中所印刷的扑克图案的位置和花色都不是完全相同的,所以,当把多张卡片叠加在一起时,就会出现“叠加混合”的展示效果,如下图
当多个卡片叠加在一起时,会看到一个由很多扑克图案混合组成的“整体”,上层卡片中的扑克图案会挡住下层卡片中的图案,上层卡片中的透明部分会显示出下层卡片中的扑克图案,最终,呈现在我们眼中的是一个“叠加混合的图案”。
如果觉得我描述的不够清楚,可以在网上搜索“思维透视卡魔术”,有一些视频,可以更加直观的看到这种魔术玩具的样子。
其实,docker镜像的结构和这个玩具的结构很像,镜像的结构也是分层的,一个镜像由一层或多层组成,docker镜像中的“一层”就像这个玩具的“一张卡片”一样,不同的层中有不同的数据,就像不同的卡片中有不同的扑克图案一样,当把这些镜像层“叠加混合”在一起后,就形成了我们看到的镜像,镜像中的数据来自各个层,镜像的示意图如下:
示意图中的镜像由两个层组成,镜像层A和镜像层B,A层和B层中分别有不同的数据(示意图中,绿色图标代表文件夹,白色图标代表文件),当我们把A层和B层”叠加混合”后,就组成了我们看到的镜像,换句话说就是,镜像中的数据其实都来自于组成镜像的各个层。
上图中,如果在A层和B层中有同名的文件(绝对路径相同的两个文件),那么在镜像中只能看到B层中的文件,因为B层在A层的上面,上层的同名文件会遮挡住下层中的同名文件,最终展示到镜像中,如果A层和B层中有同样的文件夹(绝对路径相同的两个目录),那么两层中这个目录下的文件会合并到一起展示到镜像中。
示意图中,A层中的文件可能是系统文件(比如centos7、ubuntu的相关文件),B层中的文件可能是nginx或者redis的相关文件,当把A层和B层中的文件“叠加混合”后,就变成了一个可运行nginx或者可运行redis的镜像。
当我们基于一个镜像创建容器后,会发现,无论在已有容器中怎样修改数据,都不会影响镜像中的数据,也不会影响到基于同一个镜像启动的其他容器,当基于镜像创建新的容器时,刚刚新创建的容器中的数据总是和镜像中的数据是一样的,这是怎样做到的呢?
这是因为,镜像中的层是“只读”的,我们无法修改镜像只读层中的数据,镜像层中的数据只能读取出来使用,当我们基于镜像启动一个容器时,会在镜像只读层的基础上添加一个新的层,这个新的层是“可读可写”的,这个“读写层”不属于镜像,只属于当前容器,在容器中的操作都在这个“读写层”中完成的,而我们在容器中看到的数据,是由“读写层”和“只读层”叠加后呈现出来的,举个例子:
假设,一个镜像由两层组成,这两层是只读的,示意图如下
上图中,A层中有两个文件,File1和File2,B层中也有两个文件,File2和File3,当这些层叠加后,我们会看到三个文件,File1、File2和File3,我们眼中看到的File2是B层中的File2,因为之前说过,如果两个层中有同名的文件,那么最终以上层中的文件内容为准。
当我们基于这个镜像启动一个容器时,会在镜像“只读层”的上方创建一个“容器层”,这个容器层是可读可写的。
当我们在容器中修改File1时,并不会直接修改镜像层中的File1文件,而是会先把镜像层中的File1复制一份到容器层,然后修改容器层中的副本。
这种不在源文件上修改,而是在需要修改时复制出副本,在副本上进行修改的方法,被称之为“写时复制”,写时复制的英文为copy-on-write,首字母简称COW。
由于容器层在镜像层的上面,所以,当镜像层和容器层叠加后,我们看到的File1其实是容器层中的File1(以上层为准)。
同理,如果想要在容器中修改File2,则会先从镜像层B中将File2拷贝到容器层(A层、B层都有File2,但是以上层为准,所以从B层拷贝File2),然后修改容器层中的副本,如下图所示
如果我们想要在容器中删除File3,并不会直接删除镜像层中的File3,而是在容器层中,标记镜像层中的File3不可见。
如上图所示,当我们在容器中删除File3时,容器层会为File3创建一个whiteout文件,这个whiteout文件就是用来标记File3文件不可用的,镜像层中的File3文件仍然存在,只是我们看不到了。我在翻译网站上翻译whiteout这个词,看到的解释是“临时性失明”。
如果我们想要在容器中创建一个新文件File4,那么File4会在容器层中被创建。
总之,镜像中的层是只读的,可以理解为只用来提供原始数据,如果牵扯到“写”的操作,都是在容器层中完成的,当基于同一个镜像启动多个容器时,镜像层是共享的,容器层则属于各个容器本身。
上面说的都是原理,在实际的应用中,怎样实现这种分层结构呢?如果想要实现这种分层的结构,需要借助一种叫“联合文件系统”的东西,联合文件系统有很多种,常见的有aufs、device mapper、btrfs等等,曾经docker默认使用的联合文件系统是aufs,现在,docker默认使用的是overlayfs,overlayfs是一种堆叠文件系统,它建立在其他文件系统(xfs或ext4)之上,overlayfs有两个版本,overlay和overlay2,docker推荐默认使用overlay2,使用overlay2的前提是,linux内核版本需要大于等于4.0,或者使用的RHEL和CentOS的版本是3.10.0-514的更高版本,我在centos7.9的测试机中执行uname -r命令,执行结果如下,满足使用overlay2的使用要求
[root@kvm32docker2 ~]# uname -r
3.10.0-1160.el7.x86_64
在docker主机上执行docker info命令,可以看到当前使用的存储驱动是overlay2
[root@kvm32docker2 ~]# docker info | grep -i "storage"
Storage Driver: overlay2
我们最好来实际使用一下overlay2文件系统,以便更好的理解它的分层结构,但是,在使用它之前,需要先了解一些overlay2的相关概念,overlay2是一个叠加文件系统,它能把不同层的数据混合叠加在一起,通过一个统一的视图展现给用户,在统一的视图下,就像在其他文件系统中一样,用户可以对文件进行各种增删改查操作。在overlay2中,不同的层有不同的作用,不同位置的层被分类,并按照一定的规则运行,这么说不太容易理解,不用害怕,其实它的运行规则和大部分概念我们都已经理解了,只是换了个名字罢了,我们来看一下下面这张overlay概念图
上图中一共有三层,最下层在overlay文件系统中被称为”lower层”,lower层中的数据是“只读”的,也就是说,这些数据只能被查看,不能被修改,在overlay2中,lower层可以是一层,也可以是多层,lower层上面的一层被称之为”upper层”,upper层是可读可写层,在upper层之上还有一层,称之为”merged层”,merged层就是用户看到的叠加混合后的视图,聪明如你肯定已经看出来了,这些在overlay中的概念,正与刚才所说的镜像层和容器层的概念对应,lower层对应镜像层,upper层对应容器层,merged层对应我们最终看到的数据。
那么现在我们就来创建一些测试文件,实际感受一下overlay2,在docker主机上执行如下命令
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower{1,2,3}
[root@kvm32docker2 ~]# mkdir -p /tmp/test/{work,upper,merged}
[root@kvm32docker2 ~]# ls /tmp/test/
lower1 lower2 lower3 merged upper work
我创建了一个测试目录/tmp/test/,用于测试overlay2,在测试目录中,lower1、lower2、lower3三个目录用于模拟lower层,upper目录和merged目录分别模拟upper层和merged层,work目录是overlay文件系统需要使用的临时工作目录,在挂载时需要使用,准备好对应的目录后,在不同的lower目录中创建一些文件和目录,模拟不同的层中的文件
[root@kvm32docker2 ~]# echo 'file1 - lower1' > /tmp/test/lower1/file1
[root@kvm32docker2 ~]# echo 'file2 - lower1' > /tmp/test/lower1/file2
[root@kvm32docker2 ~]# echo 'file2 - lower2' > /tmp/test/lower2/file2
[root@kvm32docker2 ~]# echo 'file3 - lower3' > /tmp/test/lower3/file3
[root@kvm32docker2 ~]#
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower2/tdir
[root@kvm32docker2 ~]# mkdir -p /tmp/test/lower3/tdir
[root@kvm32docker2 ~]# echo 'file1 - tdir - lower2' > /tmp/test/lower2/tdir/file1
[root@kvm32docker2 ~]# echo 'file2 - tdir - lower3' > /tmp/test/lower3/tdir/file2
[root@kvm32docker2 ~]#
[root@kvm32docker2 ~]# tree /tmp/test
/tmp/test
├── lower1
│ ├── file1
│ └── file2
├── lower2
│ ├── file2
│ └── tdir
│ └── file1
├── lower3
│ ├── file3
│ └── tdir
│ └── file2
├── merged
├── upper
└── work
8 directories, 6 files
测试文件创建完毕后,使用如下命令创建一个overlay2挂载点。
#为了把挂载命令写的尽量简短一些,这里使用相对路径进行挂载,先进入测试目录,以便使用相对路径
# cd /tmp/test
#执行如下mount命令,指定使用overlay文件系统进行挂载,通过-o选项指定挂载选项
# mount -t overlay -o lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work overlay merged
#上述命令的lowerdir选项用于指定lower层对应的目录,
#在overlay2中,lower层可以是一层,也可以是多层,
#如果是多层,每层之间用冒号':'隔开,冒号左侧的层高于冒号右侧的层,
#上例中,lower1层高于lower2层高于lower3层,
#upperdir选项用于指定upper层对应的目录,
#workdir选项用于指定overlay的临时工作目录,
#最后指定通过overlay的方式将数据混合到merged目录中。
完成上述操作后,可以通过mount命令确定一下已经正常挂载
[root@kvm32docker2 test]# mount | grep overlay | grep /tmp/test
overlay on /tmp/test/merged type overlay (rw,relatime,lowerdir=lower1:lower2:lower3,upperdir=upper,workdir=work)
正常挂载后,进入merged目录,目录结构和文件内容内容如下
[root@kvm32docker2 merged]# pwd
/tmp/test/merged
[root@kvm32docker2 merged]# tree
.
├── file1
├── file2
├── file3
└── tdir
├── file1
└── file2
1 directory, 5 files
[root@kvm32docker2 merged]# cat file1
file1 - lower1
[root@kvm32docker2 merged]# cat file2
file2 - lower1
[root@kvm32docker2 merged]# cat file3
file3 - lower3
[root@kvm32docker2 merged]# cat tdir/file1
file1 - tdir - lower2
[root@kvm32docker2 merged]# cat tdir/file2
file2 - tdir - lower3
正如之前所说,高层中的文件会遮挡住低层中的文件,所以,我们最终看到的file2是lower1中的file2,当不同的层中有相同的目录时,目录中的文件会合并到目录下。
现在,我们在merged目录中修改一下file1文件的内容,将原有内容改为如下内容
注意:如下所有操作均在merged目录中进行
[root@kvm32docker2 merged]# vim file1
[root@kvm32docker2 merged]# cat file1
file1 - merged
修改merged/file1以后,我们查看一下lower1/file1,如下,lower1/file1中的内容没有发生任何改变。
[root@kvm32docker2 merged]# cat ../lower1/file1
file1 - lower1
这是因为lower层中的数据对于merged层来说,是只读的,当merged层想要修改来自lower层中的文件时,会利用刚才提到的写时复制技术,将lower层中的文件拷贝到upper层中,在upper层中对拷贝的副本进行修改,最终,我们看到的是upper层中修改过的副本,如下
[root@kvm32docker2 merged]# ls ../upper/
file1
[root@kvm32docker2 merged]# cat ../upper/file1
file1 - merged
如果再次修改merged/file1,其实是直接在upper/file1上修改的,因为upper层是可读可写层,可以直接修改其中的文件。示例如下
[root@kvm32docker2 merged]# vim file1
[root@kvm32docker2 merged]# cat file1
file1 - merged
file1 - merged
[root@kvm32docker2 merged]# cat ../upper/file1
file1 - merged
file1 - merged
这里需要注意的是,如果直接在lower目录中修改文件,也是可以修改的(相当于直接在xfs或者ext4文件系统中进行写操作,写操作没有经过overlay文件系统),不要这样操作,这里所谓的lower层“只读”,是针对merged层来说的,当我们在merged层中修改来自lower层中的文件时,这些lower层中的文件是“只读”的,需要借助COW机制来完成修改。
现在,我们在merged层中,删除file3文件,可以发现upper层中多了一个file3的whiteout文件
[root@kvm32docker2 merged]# ls
file1 file2 file3 tdir
[root@kvm32docker2 merged]# rm file3
rm: remove regular file ‘file3’? y
[root@kvm32docker2 merged]# ls
file1 file2 tdir
[root@kvm32docker2 merged]# ll ../upper/
total 4
-rw-r--r-- 1 root root 30 Apr 2 11:46 file1
c--------- 1 root root 0, 0 Apr 2 12:04 file3
可见whiteout文件是一种文件类型为字符设备的文件,它的主次设备号均为0,那么我们在upper层手动创建一个whiteout文件,看看merged层的变化。
[root@kvm32docker2 merged]# pwd
/tmp/test/merged
[root@kvm32docker2 merged]# ls
file1 file2 tdir
[root@kvm32docker2 merged]# mknod ../upper/tdir c 0 0
[root@kvm32docker2 merged]# ls
file1 file2
如上,我们使用mknod命令在upper层手动创建了一个whiteout文件,这个whiteout是针对tdir创建的,whiteout文件创建后,merged层不再显示tdir目录了。
如果在merged层创建一个全新的文件,其实是直接在upper层创建的,后面的更改或者删除操作也是直接在upper层中进行,这里就不演示了。
总之,merged层就是我们看到的视图,我们所做的操作也应该在这个层中进行,其实,容器中最终挂载的就是merged层。
通过上述实验,我们应该已经对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层和挂载操作的存在,避免了这些信息进入到容器的可读写层,所以可以保障我们基于容器创建镜像时,得到的镜像是“纯净”的。
我先歇歇~,兄弟萌先看着~