详解 OCI 容器镜像的层
在 OCI 容器镜像规范中,最重要的概念是层(layer)。如果需要对容器镜像的存储尺寸进行优化,就离不开对层的理解。
变化集
层表示的是文件系统的变化集(Changeset)。每个层表示的是对文件系统的改动。容器镜像中的层按照自底向上的顺序排列。从最底层的层开始,依次应用每个层所描述的改动,最后得到容器运行时的文件系统。
对文件系统的改动分成三类,分别是添加、修改和删除。
- 对于层中的某个路径,如果当前文件系统中不存在,则视为“添加”。
- 对于层中的某个路径,如果当前文件系统中已经存在,则视为“修改”。
- “删除”当前文件系统中的路径需要一种特殊的表示形式。
在这三种改动中,添加和修改很好理解,只需要在改动集中直接添加文件即可。删除则需要使用特殊的 whiteout 文件来表示。
whiteout 是一个空文件,它的作用是表示需要删除的路径。对于需要删除的路径,在该路径的基础名称(basename)上添加 .wh.
前缀就得到了对应的 whiteout 文件的路径。
举例来说,如果要删除的文件路径是 /etc/app/app.conf
,那么对应的 whiteout 文件路径是 /etc/app/.wh.app.conf
。
这种 whiteout 文件的格式只能删除单个路径。如果要删除某个目录下的全部内容,可以使用不透明 whiteout 文件。不透明 whiteout 文件的名称是 .wh..wh..opq
。当该文件出现在某个目录下时,该目录下的全部内容都会被删除。
举例来说,如果要删除 /etc/app
目录下的全部内容,可以添加路径为 /etc/app/.wh..wh..opq
文件。
实际演示
下面通过具体的容器镜像示例来介绍层。需要用到工具 skopeo 和 dive。
构建镜像
作为示例的容器镜像由下面的 Dockerfile 创建。
FROM busybox
RUN mkdir -p /a/b && echo "Old" > /a/b/a.txt
RUN echo "New" > /a/b/a.txt
RUN rm /a/b/a.txt
RUN rm -rf /a
容器镜像的基础镜像是 busybox
。该镜像一共有 5 个层。基础层来自 busybox
镜像,而 Dockerfile 中的每个 RUN
指令都会创建一个新的层。由 RUN
指令创建的这 4 个层,分别表示不同的对文件系统的改动。
- 创建目录和文件
- 修改文件
- 删除文件
- 删除目录
使用 Docker 来构建镜像,名称为 layers
。
$ docker build . -t layers
再使用 dive
来查看层。
$ dive layers:latest
在 dive 的界面中,左侧列出了全部的层。当选中某个层时,右侧会显示选中层的内容。不同的颜色表示不同类型的改动:
- 绿色表示添加。
- 黄色表示修改。
- 红色表示删除。
层的内容以 tar 格式打包,并可以使用 gzip 或 zstd 格式来压缩。下面使用 skopeo 来查看层的内容。
分析层的内容
首先使用 skopeo copy
命令把 Docker 镜像转换为 OCI 格式。由于容器镜像 layers
只在本地构建,使用 docker-daemon
来引用来镜像。layers_oci
目录包含了镜像的内容。
$ skopeo copy docker-daemon:layers:latest oci:layers_oci
下面是镜像的清单文件的内容。在 layers
属性数组中可以看到 5 个层。第一个层来自 busybox
镜像,不做具体的分析。
{
"schemaVersion": 2,
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:f50a8dd2ccedfa105ff9c32739f096a3a50789944fdaccc969822e87fcf34028",
"size": 1466
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:d962e378abe023ee373a3400e3695e0df87b54c1b1678ecf7cfb855276c54451",
"size": 804596
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:0acd7b33a4ec62d2dbacb6ce14b8d2e2998d4832623d59af27a9b0455ebb4fe1",
"size": 267
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:10071780b50f3ed63ac0b11f70e226bc3d74a05de81c5cf35af13d2197c0dcf6",
"size": 174
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:76567325b61e99efb9c5d86de937ebd3ea32a26a636e63fe253a0824392c1092",
"size": 161
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ae0686350aea7a37ec2f83072fba5d0bae936d7a34dc890244efcabf56310f09",
"size": 129
}
]
}
解压缩第 2 个层的内容到指定目录。
$ tar -xf blobs/sha256/0acd7b33a4ec62d2dbacb6ce14b8d2e2998d4832623d59af27a9b0455ebb4fe1 -C files/02
下面给出了解压缩之后的 tar 文件的内容。路径 /a
下有一个 .wh..wh..opq
文件,表示删除 /a
下的全部内容。之后再添加 /a/b/a.txt
文件。对 /etc
、 /proc
和 /sys
的改动由 RUN
指令中的 Shell 命令所产生。在同一个路径下,不透明 whiteout 文件总是最先被应用的。
$ tree -a files/02
files/02
├── a
│ ├── .wh..wh..opq
│ └── b
│ └── a.txt
├── etc
├── proc
│ └── .wh..wh..opq
└── sys
└── .wh..wh..opq
5 directories, 4 files
解压缩第 3 个层的内容。
$ tar -xf blobs/sha256/10071780b50f3ed63ac0b11f70e226bc3d74a05de81c5cf35af13d2197c0dcf6 -C files/03
查看该层的内容。该层对应的是修改文件的命令,只包含更新之后的文件。
$ tree -a files/03
files/03
├── a
│ └── b
│ └── a.txt
└── etc
3 directories, 1 file
解压缩第 4 个层的内容。
$ tar -xf blobs/sha256/76567325b61e99efb9c5d86de937ebd3ea32a26a636e63fe253a0824392c1092 -C files/04
查看该层的内容。该层仅包含一个 /a/b/.wh.a.txt
文件,表示删除 /a/b/a.txt
。
$ tree -a files/04
files/04
├── a
│ └── b
│ └── .wh.a.txt
└── etc
3 directories, 1 file
解压缩第 5 个层的内容。
$ tar -xf blobs/sha256/ae0686350aea7a37ec2f83072fba5d0bae936d7a34dc890244efcabf56310f09 -C files/05
查看该层的内容。该层只包含一个 /.wh.a
文件,表示删除路径 /a
。
$ tree -a files/05
files/05
├── .wh.a
└── etc
1 directory, 1 file
从上面给出的层的内容,可以很清楚的看到 Dockerfile 中的 RUN
指令中的命令与层中文件的对应关系。