详解 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 文件。

实际演示

下面通过具体的容器镜像示例来介绍层。需要用到工具 skopeodive

构建镜像

作为示例的容器镜像由下面的 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 个层,分别表示不同的对文件系统的改动。

  1. 创建目录和文件
  2. 修改文件
  3. 删除文件
  4. 删除目录

使用 Docker 来构建镜像,名称为 layers

$ docker build . -t layers

再使用 dive 来查看层。

$ dive layers:latest

在 dive 的界面中,左侧列出了全部的层。当选中某个层时,右侧会显示选中层的内容。不同的颜色表示不同类型的改动:

  • 绿色表示添加。
  • 黄色表示修改。
  • 红色表示删除。

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 指令中的命令与层中文件的对应关系。

版权所有 © 2024 灵动代码