深入理解 Docker 容器镜像分层的原理

作者: 王炳明 分类: Docker 发布时间: 2021-04-24 17:37 热度:620

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 文件
深入理解 Docker 容器镜像分层的原理插图

第一个文件夹distribution,建立了 diffID 和 v2metadata 的连接,可以根据其中一个id查得另一个id,目前我还不知道这样做的意义,以后再来补充

深入理解 Docker 容器镜像分层的原理插图(1)

第二个文件夹imagedb,记录的是镜像的元数据,包括了镜像架构(如 amd64)、操作系统(如 linux)、镜像默认配置、构建该镜像的容器 ID 和配置、创建时间、创建该镜像的 docker 版本、构建镜像的历史信息以及 rootfs 组成,在 imagedb/content/sha256 目录下的每个文件对应一个镜像

深入理解 Docker 容器镜像分层的原理插图(2)

每个文件的名字,都可以一一和镜像ID对应上

深入理解 Docker 容器镜像分层的原理插图(3)

其内容就是一些包含镜像元数据的 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 镜像的信息,非常的直观,由于数据太多,这里只截取一部分

深入理解 Docker 容器镜像分层的原理插图(4)

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 和 diffID
  • parent:记录父镜像层的 chainID
  • size:记录该层因数据变更而增加的镜像大小
  • 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,逐渐推导出整个镜像链。

深入理解 Docker 容器镜像分层的原理插图(5)

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 这个目录就会被删除掉。

深入理解 Docker 容器镜像分层的原理插图(6)

第三个目录: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

7. 参考文档

Docker学习:Image的本地存储结构

docker中镜像存储中各个ID的详细介绍

明哥公众号

文章有帮助,请作者喝杯咖啡?

发表评论