深入理解 Docker 容器镜像分层的原理
1. 准备工作
为了后面方便演示,我首先要拉取一个镜像,这里我选官方的 Docker Hub 上 ubuntu 镜像,拉取的时候,有三行 Pull complete
,每一行代表一个 RootFS 的镜像层
$ docker pull ubuntu:latest
latest: Pulling from library/ubuntu
a70d879fa598: Pull complete
c4394a92d1f8: Pull complete
10e6159c56c0: Pull complete
Digest: sha256:3c9c713e0979e9bd6061ed52ac1e9e1f246c9495aa063619d9d695fb8039aa1f
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest
并且可每一行前面都有一个 ID,这个 ID 是 Digest ID 前面 12个字母 ,每个 ID 对应一个 /var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256/{chainID}
文件,文件的内容是该 rootfs layer 的 diffID,有了这个 diffID 后面,就能找到 rootfs layer 的真实数据,这个后面会讲到,这里先不急。
$ cat /var/lib/docker/image/overlay2/distribution/diffid-by-digest/sha256/a70d879fa5984474288d52009479054b8bb2993de2a1859f43b5480600cecb24
sha256:0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505
然后我使用这个镜像创建一个容器,会返回一个唯一标识这个容器的容器 ID
$ docker run -d ubuntu:latest sleep 3600
3e62af6daae316011167b4599f75d3ea8d5d07b8ba9fe372b5aaa0cb7da8d2fe
环境准备好,接下来就可以开始演示了,不过在开始前,请你先安装 jq
这个 Linux 的 JSON 解析库
$ yum install jq -y
2. 容器镜像的分层
在不同的操作系统上,使用的分层技术可能不一样,如果你在 CentOS 机器上,docker 默认使用的是 overlay2 的存储驱动,而 Ubuntu类的系统默认采用的是AUFS
$ docker info | grep "Storage Driver"
Storage Driver: overlay2
但不管是哪一种分层驱动,镜像都会有三个层级。
后面的内容演示中,我都会使用的都是 overlay2 这个分层驱动,因此下面路径中的 <graph_driver>
你要对应替换成 overlay2
第一层:镜像层(只读层,roLayer),分层信息记录在 /var/lib/docker/image/{graph_driver}/layerdb/sha256/{chainID}
目录中
第二层:init 层,分层信息记录在 /var/lib/docker/image/{graph_driver}/layerdb/sha256/{chainID_init}
目录中
第三层:容器层(读写层,mountLayer),分层信息存放在 /var/lib/docker/image/{graph_driver}/layerdb/mounts/{container_id}
目录中
而不管哪一层,他们真实的数据都存储在 /var/lib/docker/{graph_driver}/{cacheID}/diff
目录下
3. 理解各种ID的含义
上面你会看到各种id,比如 chainID,cacheID 等等,这些 ID 非常重要,会贯穿本篇文章的始终,因此在开始之前,我有必要先给你统一介绍一下它们:
imageID:镜像的唯一标识,其数值根据该镜像的元数据配置文件采用sha256算法的计算获得。
$ docker image inspect ubuntu | jq ".[0].Id"
"sha256:26b77e58432b01665d7e876248c9056fa58bf4a7ab82576a024f5cf3dac146d6"
chainID:Docker内容寻址机制采用的索引ID,docker image inspect <image_id>
查到的 RootFS 的 ID 都是 chainID, 每一个镜像的 RootFS 的最低层的 chainID 都可以在 /var/lib/docker/image/overlay2/layerdb/sha256/
目录下找到对应的文件夹。
$ docker image inspect ubuntu | jq ".[0].RootFS"
{
"Type": "layers",
"Layers": [
"sha256:0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505",
"sha256:935f303ebf75656fcbf822491f56646c5a875bd0ad0bf2529671d31dd5456dfa",
"sha256:346be19f13b0ccad355ab89265edaa4ac5958a42b8bb0492d2d22d9e4538def4"
]
}
$ ls -l /var/lib/docker/image/overlay2/layerdb/sha256/
drwx------ 2 root root 4096 Apr 19 07:06 0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505
cacheID:由宿主机随即生成的一个uuid,存放于 /var/lib/docker/image/overlay2/layerdb/sha256/{chainID}/cache-id
文件中(容器层不会有 cacheID),每一个 cacheID 都对应着一个镜像层,每一个 cacheID 对应着 /var/lib/docker/overlay2/${cache-id}
目录
$ cat /var/lib/docker/image/overlay2/layerdb/sha256/9de65d1e8b2782409b2420bf9347003a43e91bb65c1e4c8fbd7d098d6234f359/cache-id
7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
$ ls -l /var/lib/docker/overlay2/ | grep 7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
drwx-----x 4 root root 4096 Apr 19 07:41 7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
diffID:镜像层校验ID,是根据该镜像层的打包文件校验获得。存放于 /var/lib/docker/image/overlay2/layerdb/sha256/{chainID}/diff
。
在 layer 的所有属性中,diffID 采用 SHA256 算法,基于镜像层文件包的内容计算得到。而 chainID 是基于内容存储的索引,它是根据当前层与所有祖先镜像层 diffID 计算出来的,具体算如下:
- 如果该镜像层是最底层(没有父镜像层),该层的 diffID 便是 chainID。
- 该镜像层的 chainID 计算公式为 chainID(n)=SHA256(chain(n-1) diffID(n)),也就是根据父镜像层的 chainID 加上一个空格和当前层的 diffID,再计算 SHA256 校验码。
containerID:容器的唯一标识,每一个容器对应一个 /var/lib/docker/image/overlay2/layerdb/mounts/{container_id}
目录
$ docker ps |grep ubuntu | awk '{print$1}'
a3f53bc24ea5
$ docker inspect a3f53bc24ea5 | jq ".[0].Id"
"a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734"
$ ls -l /var/lib/docker/image/overlay2/layerdb/mounts/ | grep a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734
drwxr-xr-x 2 root root 4096 Apr 23 09:14 a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734
4. image/overlay2 目录
在 /var/lib/docker/image/overlay2
目录下,有三个文件夹,和一个 json 文件
第一个文件夹:distribution
,建立了 diffID 和 v2metadata 的连接,可以根据其中一个id查得另一个id,目前我还不知道这样做的意义,以后再来补充
第二个文件夹:imagedb
,记录的是镜像的元数据,包括了镜像架构(如 amd64)、操作系统(如 linux)、镜像默认配置、构建该镜像的容器 ID 和配置、创建时间、创建该镜像的 docker 版本、构建镜像的历史信息以及 rootfs 组成,在 imagedb/content/sha256 目录下的每个文件对应一个镜像
每个文件的名字,都可以一一和镜像ID对应上
其内容就是一些包含镜像元数据的 JSON 字符串,如果你对该文件进行 sha256sum 计算后,会发现计算结果就是文件名
$ sha256sum 26b77e58432b01665d7e876248c9056fa58bf4a7ab82576a024f5cf3dac146d6
26b77e58432b01665d7e876248c9056fa58bf4a7ab82576a024f5cf3dac146d6 26b77e58432b01665d7e876248c9056fa58bf4a7ab82576a024f5cf3dac146d6
第三个文件夹:layerdb
:存储镜像层或者镜像链的信息,其中 mounts 记录 mountLayer (可读写层)的信息,而 sha256 记录 roLayer (只读层)的信息
$ pwd
/var/lib/docker/image/overlay2
$ tree layerdb/ -L 1
layerdb/
├── mounts
├── sha256
└── tmp
第一个文件:repositories.json
,保存着本地所有 docker 镜像的信息,非常的直观,由于数据太多,这里只截取一部分
5. 镜像链的查找逻辑
在上面所讲的三个目录中,layerdb
最为重要,是理解 Docker 镜像层及镜像链的关键所在。
我还是以上面创建的 Docker 容器为例 ,教大家如何从一个容器出发,去找出该容器使用的镜像的镜像层之间的链接关系
# 容器ID:a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734
ls -l /var/lib/docker/image/overlay2/layerdb/mounts/a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734/
-rw-r--r-- 1 root root 69 Apr 23 09:14 init-id
-rw-r--r-- 1 root root 64 Apr 23 09:14 mount-id
-rw-r--r-- 1 root root 71 Apr 23 09:14 parent cd /var/lib/docker/image/overlay2/layerdb/mounts/a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734/
cat init-id
2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b-init cat mount-id
2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
# init-id 和 mount-id
ls -l /var/lib/docker/overlay2/ | grep 2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
drwx-----x 5 root root 4096 Apr 23 09:14 2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
drwx-----x 4 root root 4096 Apr 23 09:14 2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b-init cat parent
sha256:9de65d1e8b2782409b2420bf9347003a43e91bb65c1e4c8fbd7d098d6234f359
ls -l /var/lib/docker/image/overlay2/layerdb/sha256/9de65d1e8b2782409b2420bf9347003a43e91bb65c1e4c8fbd7d098d6234f359
-rw-r--r-- 1 root root 64 Apr 19 07:06 cache-id
-rw-r--r-- 1 root root 71 Apr 19 07:06 diff
-rw-r--r-- 1 root root 71 Apr 19 07:06 parent
-rw-r--r-- 1 root root 1 Apr 19 07:06 size
-rw-r--r-- 1 root root 374 Apr 19 07:06 tar-split.json.gz cat cache-id
7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
ls -l /var/lib/docker/overlay2/7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
-rw------- 1 root root 0 Apr 23 09:14 committed
drwxr-xr-x 3 root root 4096 Apr 19 07:06 diff
-rw-r--r-- 1 root root 26 Apr 19 07:06 link
-rw-r--r-- 1 root root 57 Apr 19 07:06 lower
drwx------ 2 root root 4096 Apr 19 07:06 work cat link
GQ3GKOHS6M4NVGRDL4VD6A4IM7
# 发现 link 的目录就是上面的 diff 目录
$ ls -l /var/lib/docker/overlay2/l/GQ3GKOHS6M4NVGRDL4VD6A4IM7
lrwxrwxrwx 1 root root 72 Apr 19 07:06 /var/lib/docker/overlay2/l/GQ3GKOHS6M4NVGRDL4VD6A4IM7 -> ../7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13/diff
当你在容器里新建文件后,在宿主机上其实也可以看到,具体的路径,你可以通过如何命令获取
$ docker inspect a3f53bc24ea5 | jq ".[0].GraphDriver.Data"
{
"LowerDir": "/var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b-init/diff:/var/lib/docker/overlay2/7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13/diff:/var/lib/docker/overlay2/3432854e546c7e46fc22f2bb84c86b040901ae34bc5c519defab90482e5e178f/diff:/var/lib/docker/overlay2/fc6db682273b0e344d1cf05db0aea16e7aa1f4a69e8cc5f6c6f0234aa3f18e78/diff",
"MergedDir": "/var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/merged",
"UpperDir": "/var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/diff",
"WorkDir": "/var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/work"
}
可以看到基本上所有的细分目录都在 /var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/
下
$ ls -l /var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/
drwxr-xr-x 3 root root 4096 Apr 23 09:14 diff
-rw-r--r-- 1 root root 26 Apr 23 09:14 link
-rw-r--r-- 1 root root 115 Apr 23 09:14 lower
drwxr-xr-x 1 root root 4096 Apr 23 09:14 merged
drwx------ 3 root root 4096 Apr 23 10:21 work
你可能就会问了,这个目录路径里的 id 怎么那么长?这是怎么计算得出来的?不通过 inspect 有没有办法知道呢?
答案是当然可以,查询过程如下
# 先得知容器的id
docker ps | grep ubuntu | awk '{print1}'
a3f53bc24ea5
# 进入对应目录就会看到这个目录
ls -l /var/lib/docker/image/overlay2/layerdb/mounts/ | grep a3f53bc24ea5
drwxr-xr-x 2 root root 4096 Apr 23 09:14 a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734
# 直接进入这个目录 cd /var/lib/docker/image/overlay2/layerdb/mounts/a3f53bc24ea544af263d0374918c3b491d75b01233d0264b65cb358fe639f734
# 会看到三个文件
ls -l
-rw-r--r-- 1 root root 69 Apr 23 09:14 init-id
-rw-r--r-- 1 root root 64 Apr 23 09:14 mount-id
-rw-r--r-- 1 root root 71 Apr 23 09:14 parent
# 其中 init-id ,记录 init 层的 id cat init-id
2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b-init
# mount-id 记录的是容器层的 id
$ cat mount-id
2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
最后我们得到了 2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
这个id,就是我们上面 inspect 得到的id
自此,我们找到了容器层的 ID
下面再跟着这个思路往下找找镜像层的ID
在当前的目录下,有一个 parent 文件,它记录的就是父 layer 的信息,像这种以 sha256
开头的 ID ,称之为 chainID,专门用来记录镜像链的信息
$ cat parent
sha256:9de65d1e8b2782409b2420bf9347003a43e91bb65c1e4c8fbd7d098d6234f359
存放的内容,是 SHA256 计算出来的值,在 /var/lib/docker/image/overlay2/layerdb/sha256/
能找到对应的目录
$ ls -l /var/lib/docker/image/overlay2/layerdb/sha256/9de65d1e8b2782409b2420bf9347003a43e91bb65c1e4c8fbd7d098d6234f359
-rw-r--r-- 1 root root 64 Apr 19 07:06 cache-id
-rw-r--r-- 1 root root 71 Apr 19 07:06 diff
-rw-r--r-- 1 root root 71 Apr 19 07:06 parent
-rw-r--r-- 1 root root 1 Apr 19 07:06 size
-rw-r--r-- 1 root root 374 Apr 19 07:06 tar-split.json.gz
这个目录下有 5 个文件
cache-id
:记录 cacheID,与镜像层一一对应diff
:记录 diffID,最底层的Layer具有相同的chainID 和 diffIDparent
:记录父镜像层的 chainIDsize
:记录该层因数据变更而增加的镜像大小tar-split.json.gz
:layer层数据tar压缩包的split文件
于是我们就可以得到了三个镜像层里,最顶层的镜像层镜像的信息:
$ cat cache-id
7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13
$ cat parent
sha256:e0f8e3acb2bf7fe9384463ae7009179d299b211e7cf17c2bf9d8e5e248cfe5b0
同样的道理,这个 parent 的记录的是最顶层的镜像层的父镜像,也就是中层镜像,同样可以在 /var/lib/docker/image/overlay2/layerdb/sha256/
找到对应的目录。
$ ls -l /var/lib/docker/image/overlay2/layerdb/sha256/e0f8e3acb2bf7fe9384463ae7009179d299b211e7cf17c2bf9d8e5e248cfe5b0
-rw-r--r-- 1 root root 64 Apr 19 07:06 cache-id
-rw-r--r-- 1 root root 71 Apr 19 07:06 diff
-rw-r--r-- 1 root root 71 Apr 19 07:06 parent
-rw-r--r-- 1 root root 3 Apr 19 07:06 size
-rw-r--r-- 1 root root 1433 Apr 19 07:06 tar-split.json.gz
同时我们也得到了中层及中层的父镜像层,也就是根镜像层的信息
$ cat cache-id
3432854e546c7e46fc22f2bb84c86b040901ae34bc5c519defab90482e5e178f
$ cat parent
sha256:0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505
做为根镜像层,自然是没有 parent 文件了
$ ls -l /var/lib/docker/image/overlay2/layerdb/sha256/0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505
total 188
-rw-r--r-- 1 root root 64 Apr 19 07:06 cache-id
-rw-r--r-- 1 root root 71 Apr 19 07:06 diff
-rw-r--r-- 1 root root 8 Apr 19 07:06 size
-rw-r--r-- 1 root root 178358 Apr 19 07:06 tar-split.json.gz
$ cat cache-id
fc6db682273b0e344d1cf05db0aea16e7aa1f4a69e8cc5f6c6f0234aa3f18e78
到这里,你应该知道,如何从一个容器ID,逐渐推导出整个镜像链。
6. 镜像真实数据的存储
镜像的真实数据实现是存放在 /var/lib/docker/overlay2/${cache-id}
里
这边我以,容器层的目录为例进行演示
$ cd /var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b
$ ls -l
drwxr-xr-x 3 root root 4096 Apr 23 09:14 diff
-rw-r--r-- 1 root root 26 Apr 23 09:14 link
-rw-r--r-- 1 root root 115 Apr 23 09:14 lower
drwxr-xr-x 1 root root 4096 Apr 23 09:14 merged
drwx------ 3 root root 4096 Apr 23 13:19 work
这里面包含三个目录,两个文件
第一个目录:diff,容器的可读写层,也就容器层。
你在容器中创建的文件,都会在这个目录下,所有我先登陆容器,在 root 目录下,创建两个文件(hello 和 world)
$ docker exec -it a3f53bc24ea5 bash
root@a3f53bc24ea5:/# echo "ok" >/root/hello
root@a3f53bc24ea5:/# echo "ok" >/root/world
然后登陆容器,使用 tree 查看 diff 的目录结构,发现只有我们刚刚新创建的两个文件。
$ tree diff/
diff/
└── root
├── hello
└── world
第二个目录:merged,是容器层和镜像层的联合视图。
同时如果你的容器在运行中,那么上面新增的两个文件,也会出现在 /var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/merged
目录下,该目录下能看到的所有文件,和你在容器里能看到的完全一样。如果容器关闭了,那么 merged 这个目录就会被删除掉。
第三个目录:work,提供辅助功能,会被如copy_up之类的操作使用(sorry,暂时不知道什么用途)
第一个文件:lower,在 inspect 的时候,UpperDir 对应高层目录,LowerDir 对应低层目录。
一个容器的高层目录只有一个,而一个低层目录却不只一个,从上面的 inspect 的结果来看,LowerDir 有总共有四个目录,排除一个比较特殊的 init 目录后,还剩下三个目录,它们分别对应 ubuntu
原始镜像的三个 layer,中间用 :
分隔。
$ docker inspect a3f53bc24ea5 | jq ".[0].GraphDriver.Data.LowerDir" | sed "s/:/\n/g"
/var/lib/docker/overlay2/2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b-init/diff
/var/lib/docker/overlay2/7e9c882f0a646bf06ef53a7653e5facd254bb6ed2c05a0d4825603d964460c13/diff
/var/lib/docker/overlay2/3432854e546c7e46fc22f2bb84c86b040901ae34bc5c519defab90482e5e178f/diff
/var/lib/docker/overlay2/fc6db682273b0e344d1cf05db0aea16e7aa1f4a69e8cc5f6c6f0234aa3f18e78/diff
$ docker image inspect ubuntu | jq ".[0].RootFS.Layers"
[
"sha256:0e64bafdc7ee828d0f3995bebfa388ced52a625ad2969eeb569f4a83db56d505",
"sha256:935f303ebf75656fcbf822491f56646c5a875bd0ad0bf2529671d31dd5456dfa",
"sha256:346be19f13b0ccad355ab89265edaa4ac5958a42b8bb0492d2d22d9e4538def4"
]
至于两边的 ID 怎么对应上,我上面都已经介绍了。
第二个文件:link,文本的内容就是一个 ID,这些 ID 在 /var/lib/docker/overlay2/l/
目录下都能找到对应的软链接,指向的路径其实就是跟 link 同级目录的 diff 目录,关于 diff 上面已经介绍过了。
$ cat link
2EZPMEPS63AA6YZVDMSWBOGZRF
$ find /var/lib/docker -name 2EZPMEPS63AA6YZVDMSWBOGZRF
/var/lib/docker/overlay2/l/2EZPMEPS63AA6YZVDMSWBOGZRF
# ls -l 一下,发现文件是一个软链接
# 指向的路径其实是 diff 目录,也就是容器的可读写层
$ ls -l /var/lib/docker/overlay2/l/2EZPMEPS63AA6YZVDMSWBOGZRF
lrwxrwxrwx 1 root root 72 Apr 23 09:14 /var/lib/docker/overlay2/l/2EZPMEPS63AA6YZVDMSWBOGZRF -> ../2e0c64ae7097d3ec659d66d02c7d1bd127d3d87b7d9c0dbf07edebcc0107de6b/diff