docker - image/layer 文件管理

Dockerfile由多条指令构成,随着深入研究Dockerfile与镜像的关系,很快大家就会发现,Dockerfile中的每一条指令都会对应于Docker镜像中的一层。

 继续以如下Dockerfile为例:

FROM ubuntu:14.04
ADD run.sh /
VOLUME /data
CMD ["./run.sh"]

通过docker build以上Dockerfile的时候,会在Ubuntu:14.04镜像基础上,添加三层独立的镜像,依次对应于三条不同的命令。镜像示意图如下:

 

Dockerfile中命令与镜像层一一对应,那么是否意味着docker build完毕之后,镜像的总大小=每一层镜像的大小总和呢?答案是肯定的。依然以上图为例:如果ubuntu:14.04镜像的大小为200MB,而run.sh的大小为5MB,那么以上三层镜像从上到下,每层大小依次为0、0以及5MB,那么最终构建出的镜像大小的确为0+0+5+200=205MB。

虽然最终镜像的大小是每层镜像的累加,但是需要额外注意的是:Docker镜像的大小并不等于容器中文件系统内容的大小(不包括挂载文件,/proc、/sys等虚拟文件)。个中缘由,就和联合文件系统有很大的关系了。

假设本地镜像存储中只有一个ubuntu:14.04的镜像,我们以两个Dockerfile来说明镜像复用:

FROM ubuntu:14.04
RUN apt-get update

FROM ubuntu:14.04
ADD compressed.tar /

假设最终docker build构建出来的镜像名分别为image1和image2,由于两个Dockerfile均基于ubuntu:14.04,因此,image1和image2这两个镜像均复用了镜像ubuntu:14.04。 假设RUN apt-get update修改的文件系统内容为20MB,最终本地三个镜像的大小关系应该如下:

ubuntu:14.04: 200MB

image1:200MB(ubuntu:14.04)+20MB=220MB

image2:200MB(ubuntu:14.04)+100MB=300MB

如果仅仅是单纯的累加三个镜像的大小,那结果应该是:200+220+300=720MB,但是由于镜像复用的存在,实际占用的磁盘空间大小是:200+20+100=320MB,足足节省了400MB的磁盘空间。在此,足以证明镜像复用的巨大好处。

下面具体分析一下docker image 文件系统:

docker支持多种graphDriver,包括vfs、devicemapper、overlay、overlay2、aufs等等,其中最常用的就是aufs了,但随着linux内核3.18把overlay纳入其中,overlay的地位变得更重目前docker默认的存储类型就是overlay2,docker版本是1.8,如下

docker默认的存储目录是/var/lib/docker,我们只关心image和overlay2,image:主要存放镜像中layer层的元数据overlay2:各层的具体信息

找一个实验镜像:

 这里的关键地方是imagedblayerdb目录,看这个目录名字,很明显就是专门用来存储元数据的地方,那为什么区分image和layer呢?因为在docker中,image是由多个layer组合而成的,换句话就是layer是一个共享的层,可能有多个image会指向某个layer。

那如何才能确认image包含了哪些layer呢?答案就在imagedb这个目录中去找。比如上面启动的nginx容器,我们可以先找到这个容器对应的镜像:

 可以看到,imageID是573040df7059,再次记住这个id,我们打印/var/lib/docker/image/overlay2/imagedb/content/sha256这个目录:

 第一行的573040df70596555bbbbd2bb113272101b4d7c873b8eed075fcbc0a951636094正是记录镜像元数据的文件,接下来cat一下这个文件,得到一个长长的json:

cat 573040df70596555bbbbd2bb113272101b4d7c873b8eed075fcbc0a951636094 | python  -mjson.tool

 可以看到rootfs的diff_ids是一个包含了3个元素的数组,其实这3个元素正是组成镜像的9个layerID,从上往下看,就是底层到顶层,也就是说a2ae92ffcd29f7ededa0320f4a4fd709a723beae9a4e681696874932db7aee2c是image的最底层。既然得到了组成这个image的所有layerID,那么我们就可以带着这些layerID去寻找对应的layer了。 接下来,我们返回到上一层的layerdb中,先打印一下这个目录:

 在这里,我们仅仅发现a2ae92ffcd29f7ededa0320f4a4fd709a723beae9a4e681696874932db7aee2c这个最底层的layer,那么剩余的ayer为什么会没有呢?那是因为docker使用了chainID的方式去保存这些layer,简单来说就是chainID=sha256sum(H(chainID) diffid),也就是a2ae92ffcd29f7ededa0320f4a4fd709a723beae9a4e681696874932db7aee2c..的上一层的sha256 id是:

 echo -n "sha256:a2ae92ffcd29f7ededa0320f4a4fd709a723beae9a4e681696874932db7aee2c sha256:0eb22bfb707db44a8e5ba46a21b2ac59c83dfa946228f04be511aba313bdc090" |sha256sum -

这个时候,你能看到4cbc0ad7007fe8c2dfcf2cdc82fdb04f35070f0e2a04d5fa35093977a3cc1693这个layer层的目录了吧?依次类推,我们就能找出所有的layerID的组合。

但是上面我们也说了,/var/lib/docker/image/overlay2/layerdb存的只是元数据,那么真实的rootfs到底存在哪里呢?其中cache-id就是我们关键所在了。我们打印一擦cat /var/lib/docker/image/overlay2/layerdb/sha256/4cbc0ad7007fe8c2dfcf2cdc82fdb04f35070f0e2a04d5fa35093977a3cc1693/cache-id:

 引申一下:

docker save:

Produces a tarred repository to the standard output stream. Contains all parent layers, and all tags + versions, or specified repo:tag, for each argument provided.

docker load:

如果load前有相同的layer层,实际还会导入这么多吗,例如多个jar程序,都是基于java镜像构建。

Docker的“层”解释了为什么Docker镜像只在第一次下载时那么慢,而之后的镜像都很快,并且明明每份镜像看起来都几百兆,但是最终机器上的硬盘缺没有占用那么多的原因。更小的磁盘空间、更快的加载速度,让Docker的复用性有了非常显著的提升。

参考:

https://blog.51cto.com/u_12182612/2476386

https://blog.csdn.net/shlazww/article/details/47375009

https://www.cnblogs.com/powertoolsteam/p/14954314.html

posted on 2021-09-14 20:43  TrustNature  阅读(1231)  评论(0编辑  收藏  举报