Docker 镜像与分层
一直对Docker中的镜像(image),分层(layer)及文件(file)三者之间的关系一直很好奇,这篇文章讲述就是这三者之间的关系。
环境说明:
虚拟机:VirtualBox 5.1.38
操作系统:Ubuntu 16.04.5 LTS
docker:18.06.1-ce
Storage Driver:overlay2
1、镜像存储位置
Docker里面存放镜像的目录主要有两个:**/var/lib/docker/image/{graph_driver}/及/var/lib/docker/{graph_driver}/**,前一个主要是存放镜像的元数据,存储逻辑上的镜像和镜像层,后一个主要是存放镜像的数据文件,包括只读层和可读写层的文件。
2、镜像的元数据管理
以overlay2存储驱动为例,/var/lib/docker/image/overlay2/目录下结构为:
1 | # tree -L 1 overlay2/ |
- distribution目录存放layer的diff_id与digest的对应关系,关于二者的关系后面内容再描述;
- imagedb、layerdb分别存放镜像及分层的元数据;
- repositories.json存放镜像仓库的相关数据;
2.1 repositories.json
repositories.json 中记录了和本地 image 相关的 repository 信息,主要是 name 和 image id 的对应关系,当 image 从 registry 上被 pull 下来后,就会更新该文件。
1 | # cat repositories.json | python -mjson.tool |
- 存放了repository名称haproxy、tag(latest) 及image id(08d602…)d的信息;
- 存放了repository image id与digest的对应关系,haproxy:latest签名为:haproxy@sha256:530809…,可以通过两种方式pull镜像,即名称(haproxy:latest)和digest;
- image id是Image的唯一标示,其数值根据该镜像的元数据配置文件采用sha256算法的计算获得。
镜像的元数据配置文件存放在 /var/lib/docker/image/overlay2/imagedb/content/sha256/ 目录下,文件为Image id,对该文件内容计算sha256摘要即为image id。
1 | # sha256sum 08d602ba77646c322b68eee5f6e436f4438e1ae3a1ad3cba26f9baa4c148d5ec |
2.2 imagedb
imagedb存放了image的元数据配置文件,该文件记录了image元数据信息,包括镜像架构、操作系统、镜像默认配置、构建该镜像的容器ID和配置、创建时间、创建该镜像的Docker版本、构建镜像的历史信息及rootfs。通过构建镜像的历史信息及rootfs中的diff_ids,可以将image和layer关联起来,还可以计算出该镜像层的存储索引chainID,计算方式见后面内容。
镜像元数据配置文件目录:**/var/lib/docker/image/overlay2/imagedb/content/sha256/{image_id}**,查看haproxy元数据配置文件如下:
1 | # cat 08d602ba77646c322b68eee5f6e436f4438e1ae3a1ad3cba26f9baa4c148d5ec | python -mjson.tool |
- 在rootfs中diff_ids包含了三个分层layer,从上到下依次是从底层到顶层,最底层是6744a…,最顶层是28de1…,通过diff_ids,可以将镜像与分层关联起来,如何关联见下面内容。
- 在该镜像中,有三个只读层,再加上一个init层,一个可读写层,总共有五个分层layer。
2.3 layerdb
docker中的镜像是分层的,划分为只读层和读写层。其中docker定义了roLayer接口来描述只读的镜像层,定义了mountLayer来描述可读写的容器层。这两个层分别存放在了两个目录中:
roLayer:/var/lib/docker/image/overlay2/layerdb/sha256/{chainID}
mountLayer:/var/lib/docker/image/overlay2/layerdb/mounts/{container_id}
2.3.1 layer只读层
在layer的属性中,diffID采用SHA256算法,基于分层文件包中的内容计算得到。而chainID是基于内容存储的索引,这是根据当前层与所有祖先镜像层diffId计算出来的,具体算法如下:
- 如果该镜像层是最底层(没有父镜像层),该层的diffID便是chainID;
- 如果不是最底层,chainID的计算公式为:chainID(n)=SHA256(chain(n-1) diffID(n)),也就是第n层的chainID根据父层的chainID加上一个空格和当前的diffID,再计算SHA256摘要。
根据计算公式,haproxy对应的chainID分别为:
第一层:6744ca1b11903f4db4d5e26145f6dd20f9a6d321a7f725f1a0a7a45a4174c579
第二层:68972fe3e03c5b26652f08aa8af0b06702c02208f987da2b1b873e057d456467
1 | # echo -n "sha256:6744ca1b11903f4db4d5e26145f6dd20f9a6d321a7f725f1a0a7a45a4174c579 sha256:885806cf466d56e36824a1623f362349202c6e4e8f7aab64e174519b66484fea"|sha256sum - |
第三层:b5e0c75383b6d3a7dc43abebb31431017676f1e4209d4704963f52ce0b32b96b
1 | # echo -n "sha256:68972fe3e03c5b26652f08aa8af0b06702c02208f987da2b1b873e057d456467 sha256:28de19851da2a6dbe597cf23e1637b14d4c51f7074ae01dd9818e131c62e430e"|sha256sum - |
以第二层为例,查看第二层的目录/var/lib/docker/image/overlay2/layerdb/sha256/68972fe3e03c5b26652f08aa8af0b06702c02208f987da2b1b873e057d456467
1 | # tree -L 1 68972fe3e03c5b26652f08aa8af0b06702c02208f987da2b1b873e057d456467 |
cache-id:由宿主机随即生成的一个uuid,与镜像层文件一一对应,指向真正存放 layer 文件的地方;
1
2# more cache-id
9c58547be8d230a221107f2ac644c713eb9b8c820e1a22b351ce3e686b730b1bdiff:镜像层校验ID、根据该镜像层的打包文件校验获得;
1
2# more diff
sha256:885806cf466d56e36824a1623f362349202c6e4e8f7aab64e174519b66484feaparent:父镜像层的chainID(最底层不含该文件);
1
2# more parent
sha256:6744ca1b11903f4db4d5e26145f6dd20f9a6d321a7f725f1a0a7a45a4174c579size:当前layer的大小,单位是字节;
1
2# more size
16815636tar-split.json.gz:layer 压缩包的 split 文件,通过这个文件可以还原 layer 的 tar 包,详情可参考 https://github.com/vbatts/tar-split。
2.3.2 容器可读写层
容器可读写层在容器启动之后挂载在以容器id为目录的目录下。
1 | # docker run -it --name HAProxy -p 6301:6301 haproxy /bin/bash |
根据haproxy的容器3c262b6de295(前12位),查看/var/lib/docker/image/overlay2/layerdb/mounts/3c262b6de2955f531994d547f2902752bf52f88f1172ca0c6c29fbfa60ac4b7a目录:
1 | # tree -L 1 3c262b6de2955f531994d547f2902752bf52f88f1172ca0c6c29fbfa60ac4b7a |
init-id:容器init层的mount-id,指向了layer数据文件目录;
1
2# more init-id
c3deeb4325373415ca2d2043aed5b28c5098169e72b3f25788b1d5eb1b4f420e-initmount-id:读写层的mount-id,指向了layer数据文件目录;
1
2# more mount-id
c3deeb4325373415ca2d2043aed5b28c5098169e72b3f25788b1d5eb1b4f420eparent:容器层的父镜像层的chainID,对应只读层中的最顶层即第三层。
1
2# more parent
sha256:b5e0c75383b6d3a7dc43abebb31431017676f1e4209d4704963f52ce0b32b96b
根据
3、layer数据文件
layer镜像分层的数据文件存储在/var/lib/docker/overlay2/{mount-id}/下,根据元数据目录下的cache-id就是mount-id,如下所示:
1 | # more 6744ca1b11903f4db4d5e26145f6dd20f9a6d321a7f725f1a0a7a45a4174c579/cache-id |
3.1 最底层layer本地文件
chainId为6744ca1b11903f4db4d5e26145f6dd20f9a6d321a7f725f1a0a7a45a4174c579的layer对应的cacheID为367818b00c1569667c3f0eb8b0580c770251612d4ab16a477c5349dd20a6fd65,查看/var/lib/docker/overlay2/367818b00c1569667c3f0eb8b0580c770251612d4ab16a477c5349dd20a6fd65:
1 | # tree -L 2 367818b00c1569667c3f0eb8b0580c770251612d4ab16a477c5349dd20a6fd65/ |
- diff:该目录存放了真实的数据;
- link:该文件存放了该层的符号链接名称,该符号链接更短,主要用来避免挂载时超出页面大小的限制,指向diff目录;
1
2# more link
7UCN35ON2ZKX3DX5BA2E6NIBNF
符号链接定义如下:
1 | # ll l/7UCN35ON2ZKX3DX5BA2E6NIBNF |
3.2 第二层Layer本地文件
1 | # tree -L 1 9c58547be8d230a221107f2ac644c713eb9b8c820e1a22b351ce3e686b730b1b/ |
- 在第二层多了一个lower文件及work目录,其中lower文件内容是所有祖先layer diff目录的短符号链接名称, work 目录则是用来完成如 copy-on_write 的操作。文件的内容即为父层diff目录的短符号连接文件。
1
2# more lower
l/7UCN35ON2ZKX3DX5BA2E6NIBNF
第三层同第二层,略
3.3 init层本地文件
根据init-id文件中的内容,定位到该本地文件目录:
1 | # tree -L 1 c3deeb4325373415ca2d2043aed5b28c5098169e72b3f25788b1d5eb1b4f420e-init |
- lower文件存放了三个只读层的符号链接文件;
1
2# more lower
l/FU73TC6QKNZAZFGL5OLEHODVJY:l/Z34ST5SRLSB2UZTL6AUMICY7NI:l/7UCN35ON2ZKX3DX5BA2E6NIBNF
3.4 读写层本地文件
1 | # tree -L 1 c3deeb4325373415ca2d2043aed5b28c5098169e72b3f25788b1d5eb1b4f420e |
- lower :该文件存放了init层及三个只读层diff目录的短符号链接文件名称;
- merged :每当启动一个容器时,会将 link 指向的镜像层目录以及 lower 指向的镜像层目录联合挂载到 merged 目录,因此,容器内的视角就是 merged 目录下的内容。
4 结论
通过镜像文件的元数据信息我们可以找到分层Layer及本地数据文件之间的关联,对理解docker文件系统有很大的帮助:
- 在本地镜像仓库文件repositories.json找到镜像的imageid,根据imageid找到image的元数据配置文件;
- 在image元数据配置文件rootfs元素中找到layer的diffid;
- 根据计算公式,将diffid转化为chaninid,chainid即为只读分层元数据文件的目录;
- 在chainid目录下,cache-id文件存储了该分层数据文件的挂载目录cacheid;
- 根据cacheid在overlays存储驱动下即可找到该层的数据文件;
- 根据启动之后的contanerid,找到可读写层的元文件目录,init-id文件内容即为init层的挂载目录,而mount-id即为可读写层的挂载目录;
- 根据可读写层的挂载目录即可找到相应的数据文件。