大家好!我是大聪明-PLUS!
我们期望项目中使用的任何工具都能稳定运行,Docker 也不例外。为了快速识别潜在问题并避免故障,了解其技术内部机制至关重要。本文是一系列笔记,旨在帮助您理解容器镜像的创建过程。
介绍
让我们先来阐明一下为什么需要容器镜像。在 Docker 公司让容器化技术流行起来之前,虚拟化技术被用于在单个服务器上部署多个应用程序。每个虚拟机都依赖于各自的操作系统和应用程序。
假设一台虚拟机占用约 10 GB 的操作系统空间;在物理系统上运行 100 台虚拟机大约需要 1 TB 的空间。如果所有虚拟机都运行相同的操作系统,则会浪费大量磁盘空间来存储重复文件。即使大多数虚拟机共享相同的依赖项,应用程序的依赖项也必须单独安装在每台虚拟机上。这些限制导致了与更新、分发等相关的诸多运维问题。
一些厂商为应用程序代码及其依赖项提供了独立的磁盘,而操作系统则共享同一个父磁盘。这在一定程度上有所帮助,但导致性能下降,并引发了同样的运维问题。
dotCloud开始尝试使用 Linux 架构来运行应用程序,将其作为隔离系统运行。他们提出了一种机制,将应用程序及其所有依赖项和进程打包到容器中。该解决方案使应用程序与基础设施解耦。容器现在独立于底层基础设施,可以轻松地在云端和本地基础设施之间迁移。开发人员不再需要担心应用程序的运行环境,也不用担心该环境是否具备测试所需的必要选项和依赖项。
镜像是 Docker 容器化的核心元素。它包含了应用程序正常运行所需的进程和依赖项。开发人员通常更倾向于从仓库下载预构建的镜像,而不是从头开始创建,因为仓库中提供了大量适用于各种任务的现成组件。
基础 Docker 镜像上覆盖着只读层,每次镜像发生更改时都会创建新的只读层。每个新层代表镜像的最新版本。最终镜像由所有层合并而成。镜像的每个层都会被保存,以便在必要时快速回滚。这种方案可以节省磁盘空间并缩短容器构建时间。
在深入探讨之前,让我们先来了解一下联合文件系统,这是一个用于处理图像的 Linux 系统。
联合文件系统
联合文件系统(Union File System,简称UnionFS)允许您将一个或多个文件系统的内容合并在一起,同时保持它们在物理上的独立性。UnionFS有多种实现方式,包括AUFS、OverlayFS等等。每种实现方式都有其自身的优缺点。让我们以OverlayFS为例,来探讨一下联合文件系统的概念。
OverlayFS 基于层级结构运行------一个或多个底层层和一个顶层层。底层层为只读,而顶层层为读写。OverlayFS 支持标准层格式。

OverlayFS 会读取上下两层的内容。如果一个文件存在于两个层级(F3),则上层的文件优先于下层的文件。对文件或目录的每个操作都会产生特定的结果。
添加新文件
当添加新文件时,OverlayFS 会将其移动到顶层:

删除现有文件
从顶层删除文件或文件夹时,它会被直接删除。但是,如果删除位于下层的文件,则会在顶层创建一个同名的特殊字符设备。下层是只读的,无法从中删除文件或文件夹。顶层上的字符设备表示该文件或文件夹应在统一视图中隐藏。

对现有文件的更改
如果您从顶层编辑文件,不会发生任何事情。但是,如果您从底层开始修改文件,该文件将被复制到顶层,并且更改将应用到该副本上。

OverlayFS 实际应用
在创建合并文件系统之前,请准备以下几个目录:
`$ mkdir lower upper merged work demo
$ cd demo
$ echo "This is a file 1 in lower layer" > lower/f1
$ echo "This is a file 2 in lower layer" > lower/f2
$ echo "This is a file 3 in lower layer" > lower/f3
$ echo "This is a file 3 in upper layer" > upper/f3
$ echo "This is a file 4 in upper layer" > upper/f4
$ cd ..
$ tree demo/
demo/
├── lower
│ ├── f1
│ ├── f2
│ └── f3
├── merged
├── upper
│ ├── f3
│ └── f4
└── work`
要创建联合文件系统,请使用 mount 命令:
`$ sudo mount overlay -t overlay -o lowerdir=demo/lower/,upperdir=demo/upper/,workdir=demo/work/ demo/merged/`
可以将工作目录视为一个临时工作区,文件会从下往上复制到该目录:
`$ ls -l demo/merged/
total 16
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f1
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f2
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f3
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f4
$ cat demo/merged/f3
This is a file 3 in upper layer`
挂载命令成功完成后,您将看到上下两层的内容合并,呈现出标准视图。现在我们可以继续进行图中所示的操作。
添加文件
`$ echo "A new file called f5 in merged view" > demo/merged/f5
$ ls -l demo/merged/
total 20
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f1
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f2
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f3
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f4
-rw-rw-r-- 1 rchaganti rchaganti 36 Oct 18 13:34 f5
$ ls -l demo/upper/
total 12
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f3
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f4
-rw-rw-r-- 1 rchaganti rchaganti 36 Oct 18 13:34 f5`
向联合视图添加文件时,由于顶层是读/写层,因此该文件会被添加到顶层。
删除文件
`$ rm demo/merged/f2
$ ls -l demo/merged/
total 16
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f1
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f3
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f4
-rw-rw-r-- 1 rchaganti rchaganti 36 Oct 18 13:34 f5
$ ls -l demo/upper/
total 12
c--------- 2 root root 0, 0 Oct 18 13:36 f2
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f3
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:31 f4
-rw-rw-r-- 1 rchaganti rchaganti 36 Oct 18 13:34 f5
$ ls -l demo/lower/
total 12
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f1
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f2
-rw-rw-r-- 1 rchaganti rchaganti 32 Oct 18 12:29 f3`
删除 F2 文件会导致字符设备出现在顶层,底层保持不变。
编辑文件
当您更改文件时,系统会根据文件所在位置(顶层或底层)执行复制操作。如果您从底层按 F1 键更改文件,则该文件会先复制到顶层,然后再将更改应用到副本。
`$ cat demo/merged/f1
This is a file 1 in lower layer
$ echo "adding some random content" >> demo/merged/f1
$ cat demo/merged/f1
This is a file 1 in lower layer
adding some random content
$ cat demo/upper/f1
This is a file 1 in lower layer
adding some random content
$ cat demo/lower/f1
This is a file 1 in lower layer`
在上面的示例中,底层的文件 F1 保持不变。F1 的副本显示在顶层,并被修改以添加新行。
现在我们已经了解了 OverlayFS 的工作原理,接下来让我们看看容器化引擎如何使用文件系统来管理容器镜像。
容器图像
容器化引擎支持多种存储驱动程序,这些驱动程序负责将多层容器组合成标准化的形式。由于各层具有不可变性,容器镜像可以重用,并允许从单个镜像启动多个容器。每个容器都有自己的存储层,方便对文件系统进行写入操作,以及修改或删除现有文件。存储驱动程序负责管理写时复制 (CoW) 层。Docker 引擎默认使用 overlay2 作为存储驱动程序。

我们来弄清楚它是如何工作的。首先,我们从 Docker Hub 拉取镜像:
`$ docker pull ravikanth/hello-world:v1
v1: Pulling from ravikanth/hello-world
213ec9aee27d: Pull complete
0d351817f207: Pull complete
8b09275c3de5: Pull complete
Digest: sha256:9964e0545f1cbd09fc902dda80664ba4b23b5f4bd32b1a0e7ab135f819c5ed6c
Status: Downloaded newer image for ravikanth/hello-world:v1
docker.io/ravikanth/hello-world:v1`
我们刚刚拉取的镜像由三层组成。Docker 主机上的 overlay 2 驱动程序将镜像层存储在 /var/lib/docker/overlay 2 中。
`$ sudo ls -l /var/lib/docker/overlay2
total 16
drwx--x--- 4 root root 4096 Oct 18 15:32 cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696
drwx--x--- 4 root root 4096 Oct 18 15:32 d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39
drwx--x--- 3 root root 4096 Oct 18 15:32 ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22
drwx------ 2 root root 4096 Oct 18 15:32 l`
在这种情况下,这些目录代表图像的未压缩层。l 目录包含指向每个未压缩层中 diff 目录的符号链接。
如果你的 Docker 主机上已经有多个镜像,可以使用 docker image inspect 命令查找特定镜像的正确未压缩层 ID:
`$ docker image inspect ravikanth/hello-world:v1 | jq -r '.[0] | {Data: .GraphDriver.Data}'
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff:/var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff",
"MergedDir": "/var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/merged",
"UpperDir": "/var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/diff",
"WorkDir": "/var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/work"
}
}`
如果看到这样的输出,则表示 Lower Dir 属性关联了多个图层:
`/var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff
/var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff`
图层按照它们在容器图像中出现的顺序排列。因此,ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22 是底层,d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39 是其上一层。
现在我们知道了各层的顺序,接下来让我们看看每个未压缩的目录中有什么内容。
底层包含三条记录:
`$ sudo ls -l /var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22
total 8
-rw------- 1 root root 0 Oct 18 15:32 committed
drwxr-xr-x 19 root root 4096 Oct 18 15:32 diff
-rw-r--r-- 1 root root 26 Oct 18 15:32 link
$ sudo ls -l /var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff
total 68
drwxr-xr-x 2 root root 4096 Aug 9 14:17 bin
drwxr-xr-x 2 root root 4096 Aug 9 14:17 dev
drwxr-xr-x 16 root root 4096 Aug 9 14:17 etc
drwxr-xr-x 2 root root 4096 Aug 9 14:17 home
drwxr-xr-x 7 root root 4096 Aug 9 14:17 lib
drwxr-xr-x 5 root root 4096 Aug 9 14:17 media
drwxr-xr-x 2 root root 4096 Aug 9 14:17 mnt
drwxr-xr-x 2 root root 4096 Aug 9 14:17 opt
dr-xr-xr-x 2 root root 4096 Aug 9 14:17 proc
drwx------ 2 root root 4096 Aug 9 14:17 root
drwxr-xr-x 2 root root 4096 Aug 9 14:17 run
drwxr-xr-x 2 root root 4096 Aug 9 14:17 sbin
drwxr-xr-x 2 root root 4096 Aug 9 14:17 srv
drwxr-xr-x 2 root root 4096 Aug 9 14:17 sys
drwxrwxrwt 2 root root 4096 Aug 9 14:17 tmp
drwxr-xr-x 7 root root 4096 Aug 9 14:17 usr
drwxr-xr-x 12 root root 4096 Aug 9 14:17 var
$ sudo cat /var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/link
BOOTVTCSKJUJXUJWX765MW7TMJ
$ sudo ls -l /var/lib/docker/overlay2/l/BOOTVTCSKJUJXUJWX765MW7TMJ
lrwxrwxrwx 1 root root 72 Oct 18 15:32 /var/lib/docker/overlay2/l/BOOTVTCSKJUJXUJWX765MW7TMJ -> ../ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff`
该镜像基于 alpine:latest,因此底层 diff 目录包含操作系统文件。我们继续处理下一层。如果您查看最后一个命令,未压缩层中的 filename d 指的是同一层中的 diff 目录。
我们继续来看堆栈中的下一层:
`$ sudo ls -l /var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39
total 16
-rw------- 1 root root 0 Oct 18 15:32 committed
drwxr-xr-x 2 root root 4096 Oct 18 15:32 diff
-rw-r--r-- 1 root root 26 Oct 18 15:32 link
-rw-r--r-- 1 root root 28 Oct 18 15:32 lower
drwx------ 3 root root 4096 Oct 18 15:32 work
$ sudo ls -l /var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff
total 0
-rw-r--r-- 1 root root 0 Oct 16 10:54 hello.txt
$ sudo cat /var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff/hello.txt
$ sudo cat /var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/link
FYTORA5MEBUHE4B4ZY4IPI3P5D
$ sudo cat /var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/lower
l/BOOTVTCSKJUJXUJWX765MW7TMJ`
除底层之外的所有未压缩层都包含五个条目。diff 目录包含该层的内容------即第零个文件 hello.txt。名为 lower 的文件指向堆栈中的底层。在本例中,它指向底层的 diff 目录。您可以忽略此未压缩层中的其余条目。在每个层中,link 文件都包含指向 diff 目录的符号链接。
还有一层:
`$ sudo ls -l /var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696
total 16
drwxr-xr-x 2 root root 4096 Oct 18 15:32 diff
-rw-r--r-- 1 root root 26 Oct 18 15:32 link
-rw-r--r-- 1 root root 57 Oct 18 15:32 lower
drwx------ 3 root root 4096 Oct 18 15:32 work
$ sudo ls -l /var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/diff
total 4
-rw-r--r-- 1 root root 35 Oct 16 10:54 hello.txt
$ sudo cat /var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/diff/hello.txt
this is an update to the hello.txt
$ sudo cat /var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/link
S2YWWPZHW75GE3EIQ7ULNMR5RZ
$ sudo cat /var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/lower
l/FYTORA5MEBUHE4B4ZY4IPI3P5D:l/BOOTVTCSKJUJXUJWX765MW7TMJ`
这一层下面还有两层,所以你会看到名为 lower 的文件中有两个分开的条目。和上一层一样,diff 目录也包含一个名为 hello.txt 的文件。由于这是最顶层,所以当你创建一个容器时,你应该会在容器内看到 hello.txt 文件的内容。
为了更好地理解这一点,让我们将层结构可视化:

容器镜像不会改变。你无法直接向这些层写入任何内容。使用它们的唯一方法是创建容器实例。你可以使用 `docker run` 命令来创建容器实例。
`$ docker run -d --name helloworld ravikanth/hello-world:v1 sleep 1000000
cff3f0322cb6765f06575bba9405d8ffeb0fecac237c5f615b906a9546d2a413`
在上图中,您已经了解了如何从单个镜像创建多个容器,以及每个容器如何拥有自己的读/写层。理论上,该层应该与未压缩镜像的层相关联。可以使用 `docker container inspect` 命令来查找这种关联:
`$ docker container inspect helloworld | jq -r '.[0] | {Data: .GraphDriver.Data}'
{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751-init/diff:/var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/diff:/var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff:/var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff",
"MergedDir": "/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/merged",
"UpperDir": "/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/diff",
"WorkDir": "/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/work"
}
}`
底层目录包含四层,顺序如下:
`/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751-init/diff
/var/lib/docker/overlay2/cf0b92185c9ba3bdf11d19e53252063def37acf80fe3491597fd5a8914e1a696/diff
/var/lib/docker/overlay2/d63b70e8f22063b1eb4296b70c2e231392e28b104475104e14f7346530e0dd39/diff
/var/lib/docker/overlay2/ec861e1affe1b78197b7cc7f6f4381c6de0e490e04a60dc1ff684a9f7ddeab22/diff`
本质上,这包括你之前看到的三个镜像层,外加一个容器层。由于镜像本身不会改变,因此 MergeDir 和 WorkDir 并不重要。/var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/merged 目录包含了所有镜像层的标准表示。
`$ sudo ls -l /var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/merged
total 72
drwxr-xr-x 2 root root 4096 Aug 9 14:17 bin
drwxr-xr-x 1 root root 4096 Oct 18 18:10 dev
drwxr-xr-x 1 root root 4096 Oct 18 18:10 etc
-rw-r--r-- 1 root root 35 Oct 16 10:54 hello.txt
drwxr-xr-x 2 root root 4096 Aug 9 14:17 home
drwxr-xr-x 7 root root 4096 Aug 9 14:17 lib
drwxr-xr-x 5 root root 4096 Aug 9 14:17 media
drwxr-xr-x 2 root root 4096 Aug 9 14:17 mnt
drwxr-xr-x 2 root root 4096 Aug 9 14:17 opt
dr-xr-xr-x 2 root root 4096 Aug 9 14:17 proc
drwx------ 2 root root 4096 Aug 9 14:17 root
drwxr-xr-x 2 root root 4096 Aug 9 14:17 run
drwxr-xr-x 2 root root 4096 Aug 9 14:17 sbin
drwxr-xr-x 2 root root 4096 Aug 9 14:17 srv
drwxr-xr-x 2 root root 4096 Aug 9 14:17 sys
drwxrwxrwt 2 root root 4096 Aug 9 14:17 tmp
drwxr-xr-x 7 root root 4096 Aug 9 14:17 usr
drwxr-xr-x 12 root root 4096 Aug 9 14:17 var
$ sudo cat /var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/merged/hello.txt
this is an update to the hello.txt`
现在我们尝试向正在运行的容器中写入一个新文件:
`$ docker exec helloworld touch /hello-world.txt
rchaganti@ubuntu02:~$ docker exec helloworld ls -l
total 60
drwxr-xr-x 2 root root 4096 Aug 9 08:47 bin
drwxr-xr-x 5 root root 340 Oct 18 12:40 dev
drwxr-xr-x 1 root root 4096 Oct 18 12:40 etc
-rw-r--r-- 1 root root 0 Oct 18 13:11 hello-world.txt
-rw-r--r-- 1 root root 35 Oct 16 05:24 hello.txt
drwxr-xr-x 2 root root 4096 Aug 9 08:47 home
drwxr-xr-x 7 root root 4096 Aug 9 08:47 lib
drwxr-xr-x 5 root root 4096 Aug 9 08:47 media
drwxr-xr-x 2 root root 4096 Aug 9 08:47 mnt
drwxr-xr-x 2 root root 4096 Aug 9 08:47 opt
dr-xr-xr-x 252 root root 0 Oct 18 12:40 proc
drwx------ 2 root root 4096 Aug 9 08:47 root
drwxr-xr-x 2 root root 4096 Aug 9 08:47 run
drwxr-xr-x 2 root root 4096 Aug 9 08:47 sbin
drwxr-xr-x 2 root root 4096 Aug 9 08:47 srv
dr-xr-xr-x 12 root root 0 Oct 18 12:40 sys
drwxrwxrwt 2 root root 4096 Aug 9 08:47 tmp
drwxr-xr-x 7 root root 4096 Aug 9 08:47 usr
drwxr-xr-x 12 root root 4096 Aug 9 08:47 var`
我们来检查一下上层目录是否可见:
`$ sudo ls -l /var/lib/docker/overlay2/38705da4838102c3b9d2ae8aeb49844dac84dae95881df61fcffd1685caf9751/diff
total 0
-rw-r--r-- 1 root root 0 Oct 18 18:41 hello-world.txt`
如果按照定义多个图像图层之间关系的相同步骤进行操作,您将会看到 CoW 图层与图像中较低、不可修改的图层之间的关系。

这张图虽然有点复杂,但足以说明问题。最顶层是读写 CoW 层,其下所有层都是只读的。至此,我们已经介绍了存储驱动程序如何管理镜像。您可能在其他地方读到过,容器镜像可以与任何容器化引擎一起使用。在下一篇文章中,我们将探讨如何实现这一点,并讨论开放容器倡议 (OCI) 和 OCI 镜像规范。