原来docker 镜像竟是这么存储的

1,028 阅读16分钟

你将从本篇文章中了解到

  • overlay2如何存储docker镜像

  • 创建容器后,容器层是怎么来

  • 什么是layerID、diffID、chainID、cacheID,他们之间的关系是什么

  • overlay2是如何读写的

overlay2是docker镜像存储和容器运行时的一种文件系统,因为docker镜像是分层的,overlay2文件系统可以将镜像层通过某种关系组织在一起。

从远程仓库拉取镜像时,镜像是按照层为单位拉取的,一层一个请求,当镜像层拉到本地后要经过解压,然后在本地存储,如果docker使用overlay2文件系统存储,那么镜像层默认存储路径就是/var/lib/docker/overlay2,一个镜像层一个目录。

我们知道docker镜像做成分层结构,主要就是出于镜像层共享考虑,如有两个镜像,他们的某一层是一样的(文件内容的sha256值一致),那么如果本地存在该层后,再拉取另外一个也有该层的镜像就不需要再向远程拉取,复用本地就可以了,这即节约网络带宽也节约了本地存储。这就是镜像层共享。

那么镜像层是怎么通过overlay2组织在一起的呢?比如通过docker run一个容器,docker怎么在本地找到各个层的?

image.png


# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
3f9582a2cbe7: Pull complete
9a8c6f286718: Pull complete
e81b85700bc2: Pull complete
73ae4d451120: Pull complete
6058e3569a68: Pull complete
3a1b8f201356: Pull complete
Digest: sha256:aa0afebbb3cfa473099a62c4b32e9b3fb73ed23f2a75a65ce1d4b4f55a5c2ef2
Status: Downloaded newer image for nginx:latest

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              904b8cb13b93        3 weeks ago         142MB

/var/lib/docker/overlay2# ll
total 36
drwx------  9 root root 4096 Mar 23 08:02 ./
drwx--x--x 14 root root 4096 Mar  8 16:15 ../
drwx------  4 root root 4096 Mar 23 08:02 04a8ed05648716ec2864fc22ba28ec6aeb7cb3ad93216e3c3de28624e1399d57/
drwx------  4 root root 4096 Mar 23 08:02 1f447f16ee8c11607b02e7b4258fc10ffb239691556ea662e71251ea98aa577e/
drwx------  4 root root 4096 Mar 23 08:02 2cbd2a9c036675e2d1f6d2f70c17eb0aa0a03a925abe5bb8ab2792a0826e0e36/
drwx------  3 root root 4096 Mar 23 08:02 a122831db070651fc070f2b8935108afa0142319cefda4dd22e73fca0a1402a8/
drwx------  4 root root 4096 Mar 23 08:02 c656b1c47012843249928da57f2d95aeedf832fc4f2cc8e3fb1e16f092686fcd/
drwx------  4 root root 4096 Mar 23 08:02 f99fd2c792289b5071db57864bfe62add53f483c511f53f35748900bfcd02c5b/
drwxr-xr-x  2 root root 4096 Mar 23 08:02 l/

               latest              904b8cb13b93        3 weeks ago         142MB

查看 /var/lib/docker/overlay2 目录下面已经多了6个目录,正好我们拉取的镜像也是6个目录,每个镜像层在 /var/lib/docker/overlay2 目录中都有一个对应目录。

找一个目录看下里面的内容

# ll 04a8ed05648716ec2864fc22ba28ec6aeb7cb3ad93216e3c3de28624e1399d57/
total 24
drwx------  4 root root 4096 Mar 23 08:02 ./
drwx------ 10 root root 4096 Mar 23 08:44 ../
drwxr-xr-x  8 root root 4096 Mar 23 08:02 diff/
-rw-r--r--  1 root root   26 Mar 23 08:02 link
-rw-r--r--  1 root root   28 Mar 23 08:02 lower
drwx------  2 root root 4096 Mar 23 08:02 work/

能够看到这个这个目录下有个lower文件,这个文件内容表示该层下面的所有镜像层id,称为父镜像层。文件内容如下,该内容是 /var/lib/docker/overlay2/l目录下的一个软连接,链接到该镜像层 /var/lib/docker/overlay2目录下的文件,这么做的原因是镜像/var/lib/docker/overlay2下面的镜像层ID很长,创建容器时会对所有镜像层进行联合挂载,但是这么多层镜像的名字容易导致mount时参数超过限制,所以使用软连接缩短文件名。不过最底层的镜像层就没有这个lower文件了,它没有父镜像层。


cat lower
l/CLJHYHRORFIA3QXCMQOKNLYZIY:l/QOOAW2D37UYV55JTOJ47WSHLO7

link文件内容就是上面说的该镜像层的软连接文件名。

work目录是overlay2内部使用的,我们不需要关注。

diff目录里面就是该镜像层实际的内容了。


ll diff/
total 32
drwxr-xr-x  8 root root 4096 Mar 23 08:02 ./
drwx------  4 root root 4096 Mar 23 08:02 ../
drwxr-xr-x  2 root root 4096 Mar  2 02:43 docker-entrypoint.d/
drwxr-xr-x 18 root root 4096 Mar  2 02:43 etc/
drwxr-xr-x  4 root root 4096 Feb 27 08:00 lib/
drwxrwxrwt  2 root root 4096 Mar  2 02:43 tmp/
drwxr-xr-x  7 root root 4096 Feb 27 08:00 usr/
drwxr-xr-x  5 root root 4096 Feb 27 08:00 var/

该目录下面的文件看起来似曾相识。没错,就是linux根目录下的一些目录了。那根目录下的目录怎么会在镜像层中出现呢?为了说明这个问题,我们来说说镜像构建。假设有下面一个dockerfile


FROM ubuntu
COPY a.txt /tmp
RUN echo "hello" >> /tmp/a.txt
RUN rm -f /tmp/a.txt
CMD ["/bin/bash"]

image.png

镜像构建时,每运行一条构建命令,都会在上一个构建命令构建出来的镜像层的基础上创建一个可读写层,然后运行一个容器,在容器内执行这条构建命令。如上面的第二行,则会在ubuntu的镜像基础上创建一个可读写层,然后运行容器,将宿主机的a.txt文件拷贝到容器内的/tmp目录下,然后这一层就构建完了。此时/tmp/a.txt实际上是在新创建出来的那层上,而不是修改ubuntu基础镜像/tmp目录。这就是镜像只读规则。我们通过docker inspect命令查看基础镜像ubuntu的层信息:

image.png

可以看到ubuntu只有一层。再看下刚刚构建出来的镜像层信息

image.png

lower一共三层,upper一层,一共四层,其中的一个层是基础镜像ubuntu的层,这正好和dockerfile文件能吻合,我们运行了3条命令出来3个层。那我们按照构建命令看下/tmp/a.txt文件内容:

image.png

我们从最顶层开始看,我们看到cat这个文件的时候提示文件不存在,这是符合预期的,因为我们删除了这个文件。等等,我们ls还能看到这个文件,这是怎么回事?我们再执行stat看下

image.png

image.png

这个文件已经变成了一个字符设备,且主次设备号都是0。overlay2 fs正是删除lower目录提供的文件或目录时,在upper目录创建主次设备号都为0的字符设备文件,用来表示文件、目录已被删除,这就是whiteout。用户在查找文件时,会从顶层镜像一直往下找,如果发现该文件是whiteout,则停止查找,认为该文件不存在。

然后看第二层,文件内容在原先的基础上加了一行hello,正是我们dockerfile的第二个命令,而第三层的文件内容为world,没有发生变化。说明在修改下层已经存在的镜像层时,并不会修改下层镜像的内容,而是复制下层镜像对应的文件到可写层然后进行修改。这就是overlay2 fs的写时复制

所以我们总结下镜像的读写规则:

  • 在可写层修改一个下层(父层)镜像文件,只会拷贝该文件到读写层(写时复制),然后修改,后续访问该文件时只能看到读写层的该文件,下层的该文件被覆盖

  • 删除一个下层镜像文件,不会真正删除文件,而是在读写层的同目录创建一个同名的主次设备号都为0的字符设备文件,来遮蔽(whiteout)镜像层文件

上面我们提到了容器的可写层,那么什么是可写层?运行容器的时候,镜像的各个层是如何组织在一起的?

overlay2通过联合挂载技术将所有镜像层挂载到同一个目录下,提供一个统一的视图,使得容器中能看到一个完整的Linux文件系统。我们运行一个上面构建的my_ubuntu进行分析。

docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
de089e4d2296        my_ubuntu:1.0       "sh"                34 seconds ago      Up 32 seconds                           naughty_antonelli
# mount | grep overl
overlay on /var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/merged type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/YL5NBJUXEUWMU5VIHU4O6H7FCG:/var/lib/docker/overlay2/l/E76NPCQTO7TENVPM5I3GZS5TYO:/var/lib/docker/overlay2/l/ZJB4N35GX4J64OR3KHS4G6Y7QT:/var/lib/docker/overlay2/l/JLTWJXNR2KW3WIED5UABYBU525:/var/lib/docker/overlay2/l/N2PNOP72PSPA6ZK5674JVHUBXM,upperdir=/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff,workdir=/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/work)

通过mount命令看到,镜像层被挂载在了/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/merged目录下,该目录如下:

image.png

这个目录就是一个完整的Linux的文件系统了,my_ubuntu:1.0该镜像有4层,这四层被联合挂载在了同一个目录下。那么可写层在哪里呢?我们执行docker inspect看看:


 ...
 "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a-init/diff:/var/lib/docker/overlay2/8d1ef9e87b285bbe1db242981c2b572a7c7aa8676cf4e0f6e5b9be55ab226f2b/diff:/var/lib/docker/overlay2/0bac552c989db9bc70b1eddbe121dc33ee605dc46902b497e39712f0231a1dea/diff:/var/lib/docker/overlay2/344b9f2cacd19dd1576a247b2badbc88dc928f13cd819e712d495322ef5c810c/diff:/var/lib/docker/overlay2/4f1ccaf4b2f7d761b1f6e6a86c19c10a091da43d18b8d9691ed5499a91f0a36d/diff",
                "MergedDir": "/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/merged",
                "UpperDir": "/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff",
                "WorkDir": "/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/work"
            },
            "Name": "overlay2"
        },
 ....

我们前面说过,upperdir就是容器层,/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff",该层为读写层。在容器中任何文件的变化,都会体现在这个目录下。因为现在还没有在容器内做任何文件操作,所以该目录目前是空的


 # ll /var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff
total 8
drwxr-xr-x 2 root root 4096 Mar 24 08:10 ./
drwx------ 5 root root 4096 Mar 24 08:10 ../

我们现在尝试在/opt目录下新建一个文件看看,会发生什么变化:

图片

然后再在宿主机上看下读写层目录,可以看到读写层此时已经将/opt/test文件创建出来了:

图片

如果我们停止容器的话,这个读写层就会被释放掉


# docker stop de089e4d2296
# docker rmi de089e4d2296
# ll /var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff/
ls: cannot access '/var/lib/docker/overlay2/3a6a9c17524c7a24c2b58d06d1f9c366a9f5070cc1d926a8583891f4e7eec86a/diff/': No such file or directory

以上就是基于overlay2的容器的读写和镜像存储原理了。

现在来说说layerID、diffID、chainID、cacheID是什么,他们之间的关系。为了寿命这个问题,我下载一个nginx的镜像。

图片

从上图看,每一层下载的时候都会显示一个ID,这个ID叫layerID(也叫digestID),这个ID是每一层镜像压缩后的sha256值。

然后我们在docker inspect看下镜像的具体内容,这里我们主要关注下面这一部分:

图片

通过docker inspect nginx:latest出来的ID好像跟拉取镜像的时候显示的ID(layerID)不太一样。这个docker inspect显示出来的ID叫diffID,这个ID是docker镜像的每一层解压后的sha256的值。这两个ID可以在/var/lib/docker/image/overlay2/distribution目录下找到他们的映射关系,通过一个ID去找另外一个ID。

图片

那么docker inspect显示的ID是存储在哪里呢?

执行docker inspect nginx:latest时,docker会去 /var/lib/docker/image/overlay2/repositories.json文件中找到镜像的ID(该文件记录了本地所有镜像的ID ),该镜像的ID sha256:ac232364af842735579e922641ae2f67d5b8ea97df33a207c5ea05f60c63a92d ,然后拿着这个ID去 /var/lib/docker/image/overlay2/imagedb/content/sha256目录(该目录保存了所有本地所有镜像的元数据信息)下找到该镜像的元数据文件,该文件就保存了镜像的信息,包括我们前面的diffID。

到为止,只知道了各个层元数据的目录,还不知道最终存镜像实际内容的目录在哪。这里引出一个chainID的概念。layer.ChainID只用本地,根据layer.DiffID计算,并用于layerdb的目录名称,chainID唯一标识了一组diffID的hash值,包含了这一层和它的父层(底层),当然最底层是没有父层的,也就是chainID(layer0)=diffID(layer0);对于上层的镜像,ChainID(layerN) = SHA256hex(ChainID(layerN-1) + " " + DiffID(layerN)),我们还拿上面的nginx举例子:

图片

第一个ID就是镜像的最底层,chainID(layer0)=diffID(layer0),我们可以在/var/lib/docker/image/overlay2/layerdb/sha256目录找到该chainID代表的目录,该目录有如下文件:

ll 3af14c9a24c941c626553628cf1942dcd94d40729777f2fcfbcd3b8a3dfccdd6/
total 284
drwx------  2 root root   4096 Mar 27 21:57 ./
drwxr-xr-x 22 root root   4096 Mar 27 21:57 ../
-rw-r--r--  1 root root     64 Mar 27 21:57 cache-id
-rw-r--r--  1 root root     71 Mar 27 21:57 diff
-rw-r--r--  1 root root      8 Mar 27 21:57 size
-rw-r--r--  1 root root 269333 Mar 27 21:57 tar-split.json.gz

cache-id里面的内容就是该镜像层在 /var/lib/docker/overlay2/ (该目录存储了所有镜像层的实际内容)下面的目录,diff里面的内容就是上面所说的diffID。那么第二层根据上面的计算公式:


echo -n "sha256:3af14c9a24c941c626553628cf1942dcd94d40729777f2fcfbcd3b8a3dfccdd6 sha256:e65242c66bbe3550450dc1ce55da200fdd36a82246b3702cc2f0dbe
5ff9ade84"
cc6b5ded0d53019de2911c55226548c7707a29af8cd6a90eb51a437255aa0b42  -
/var/lib/docker/image/overlay2/layerdb/sha256# ll | grep cc6b5ded0d53019de2911c55226548c7707a29af8cd6a90eb51a437255aa0b42
drwx------  2 root root 4096 Mar 27 21:57 cc6b5ded0d53019de2911c55226548c7707a29af8cd6a90eb51a437255aa0b42/

看到计算出来的ID,在本地确实存在目录,内容和上面的类似。所以根据里面的cache-id就能找到该层在 /var/lib/docker/overlay2/下的目录了。

docker在运行容器时,就是根据diffID,然后计算出chainID 来获得cache-id,然后得到镜像实际的存储目录,最后按照顺序做挂载的。