OCI(Open Container Initiative)规范是事实上的容器标准,已经被大部分容器实现以及容器编排系统所采用,包括 Docker 和 Kubernetes。它的出现是一段关于开源商业化的有趣历史:它由 Dokcer 公司作为领头者在 2015 年推出,但如今 Docker 公司在容器行业中已经成了打工仔。
从 OCI 规范开始了解容器镜像,可以让我们对容器技术建立更全面清晰的认知,而不是囿于实现细节。OCI 规范分为 Image spec
和 Runtime spec
两部分,它们分别覆盖了容器生命周期的不同阶段:
镜像规范定义了如何创建一个符合 OCI 规范的镜像,它规定了镜像的构建系统需要输出的内容和格式,输出的容器镜像可以被解包成一个 runtime bundle
,runtime bundle
是由特定文件和目录结构组成的一个文件夹,从中可以根据运行时标准运行容器。
规范要求镜像内容必须包括以下 3 部分:
json
。layer
(与下文提到的镜像层同义),文件格式可以是 tar
,gzip
等存档或压缩格式。json
。rootfs (root file system)即 / 根挂载点所挂载的文件系统,是一个操作系统所包含的文件、配置和目录,但并不包括操作系统内核,同一台机器上的所有容器都共享宿主机操作系统的内核。
接下来我们以 Docker
和 nginx
为例探索一个镜像的实际内容。拉取一个最新版本的 nginx
镜像将其 save
为 tar 包后解压:
$ docker pull nginx
$ docker save nginx -o nginx-img.tar
$ mkdir nginx-img
$ tar -xf nginx-img.tar --directory=nginx-img
得到 nginx-img
目录中的内容如下:
nginx-img
├── 013a6edf61f54428da349193e7a2077a714697991d802a1c5298b07dbe0519c9
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 2bf70c858e6c8243c4713064cf43dea840866afefe52089a3b339f06576b930e
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 490a3e67a61048564048a15d501b8e075d951d0dbba8098d5788bb8453f2371f
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 4cdc5dd7eaadff5080649e8d0014f2f8d36d4ddf2eff2fdf577dd13da85c5d2f.json
├── 761c908ee54e7ccd769e815f38e3040f7b3ff51f1c04f55aac12b9ea3d544cfe
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── 96bfd5bf4ab4c2513fb43534d51e816c4876620767858377d14dcc5a7de5f1fd
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── d18832ef411b346c36b7ba42a6c2e3f77097026fb80651c2d870f19c6fd9ccef
│ ├── json
│ ├── layer.tar
│ └── VERSION
├── manifest.json
└── repositories
首先查看 manifest.json
文件的内容,即该镜像的 Image Manifest:
$ python -m json.tool manifest.json
[
{
"Config": "4cdc5dd7eaadff5080649e8d0014f2f8d36d4ddf2eff2fdf577dd13da85c5d2f.json",
"Layers": [
"490a3e67a61048564048a15d501b8e075d951d0dbba8098d5788bb8453f2371f/layer.tar",
"2bf70c858e6c8243c4713064cf43dea840866afefe52089a3b339f06576b930e/layer.tar",
"013a6edf61f54428da349193e7a2077a714697991d802a1c5298b07dbe0519c9/layer.tar",
"761c908ee54e7ccd769e815f38e3040f7b3ff51f1c04f55aac12b9ea3d544cfe/layer.tar",
"d18832ef411b346c36b7ba42a6c2e3f77097026fb80651c2d870f19c6fd9ccef/layer.tar",
"96bfd5bf4ab4c2513fb43534d51e816c4876620767858377d14dcc5a7de5f1fd/layer.tar"
],
"RepoTags": [
"nginx:latest"
]
}
]
其中记载了 Config
和 Layers
的文件定位信息,也就是标准中所规定的 Image Layer Filesystem Changeset 和 Image Configuration。
Config
存放在另一个 json 文件中,内容较多我们不做展示,具体包含了以下信息:
runtime bundle
后将写入运行时配置文件。layers
之间的 Diff ID。Layers
列表中的 tar 包共同组成了生成容器的 rootfs,容器的镜像是分层构建的, Layers
中的元素顺序还代表了镜像层叠加的顺序,所有 layer
组成一个由下往上叠加的栈式的结构。首先看一下基础层即第一条记录中的内容:
$ mkdir base
$ tar -xf 490a3e67a61048564048a15d501b8e075d951d0dbba8098d5788bb8453f2371f/layer.tar --directory=base
base
目录中解压得到的文件内容如下:
drwxr-xr-x 2 root root 4096 6月 21 08:00 bin
drwxr-xr-x 2 root root 6 6月 13 18:30 boot
drwxr-xr-x 2 root root 6 6月 21 08:00 dev
drwxr-xr-x 28 root root 4096 6月 21 08:00 etc
drwxr-xr-x 2 root root 6 6月 13 18:30 home
drwxr-xr-x 7 root root 85 6月 21 08:00 lib
drwxr-xr-x 2 root root 34 6月 21 08:00 lib64
drwxr-xr-x 2 root root 6 6月 21 08:00 media
drwxr-xr-x 2 root root 6 6月 21 08:00 mnt
drwxr-xr-x 2 root root 6 6月 21 08:00 opt
drwxr-xr-x 2 root root 6 6月 13 18:30 proc
drwx------ 2 root root 37 6月 21 08:00 root
drwxr-xr-x 3 root root 30 6月 21 08:00 run
drwxr-xr-x 2 root root 4096 6月 21 08:00 sbin
drwxr-xr-x 2 root root 6 6月 21 08:00 srv
drwxr-xr-x 2 root root 6 6月 13 18:30 sys
drwxrwxrwt 2 root root 6 6月 21 08:00 tmp
drwxr-xr-x 10 root root 105 6月 21 08:00 usr
drwxr-xr-x 11 root root 139 6月 21 08:00 var
这已经是一个完整的 rootfs,再观察最上面一层 layer
所得到的文件内容:
96bfd5bf4ab4c2513fb43534d51e816c4876620767858377d14dcc5a7de5f1fd/
└── docker-entrypoint.d
└── 30-tune-worker-processes.sh
其中只有一个 shell 脚本文件,这说明镜像的构建过程是增量的,每一层都只包含了和更低一层相比所变更的文件内容,这也是容器镜像得以保持较小体积的原因。
Layers
中的每一层都是文件系统的变更集(ChangeSet),变更集包含新增、修改和删除三种变更,新增或修改(替换)文件的情况较好处理,但如何在应用变更集时删除一个文件呢,答案是用 Whiteouts 表示要删除的文件或文件夹。
Whiteouts 文件是一个具有特殊文件名的空文件,文件名中通过在要删除的路径基本名称添加前缀 .wh.
标志一个(更低一层中的)路径应该被删除。假如在某个 layer
有以下文件:
./etc/my-app.d/
./etc/my-app.d/default.cfg
./bin/my-app-tools
./etc/my-app-config
如果在应用的更高层 layer
中含有 ./etc/.wh.my-app-config
,应用该层变更时原有的 ./etc/my-app-config
路径将被删除。
规范中对于如何将多个镜像层应用成一个文件系统只有原理性的描述,假如我们要在 layer A
的基础上应用 Layer B
:
Layer A
中的文件系统目录以保留文件属性的方式复制到另一个快照目录 A.snapshot
Layer B
所包含的文件变更,所有的更改不会影响原有的变更集。在实践中会采用联合文件系统等更为高效的实现。
联合文件系统(Union File System)也叫 UnionFS,主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。
下面以 Ubuntu 发行版以及 unionfs-fuse
实现为例演示联合挂载的效果:
1. 首先使用包管理器安装 unionfs-fuse,这是 UnionFS 的一个实现:
$ apt install unionfs-fuse
2. 然后创建如下目录结构:
A
├── a
└── x
B
├── b
└── x
3. 创建目录 C 并将 A、B 目录联合挂载到 C 下:
$ unionfs ./B:./A ./C
4. 挂载后 C 目录内容如下:
C
├── a
├── b
└── x
如果我们分别编辑 A、B 目录中的 x 文件,会发现访问目录 C 中 x 文件得到的是 B/x 的内容(因为 B 在挂载时位于更上层)。
Docker 目前在大部分发行版本中使用的联合文件系统实现是 overlay2
,相比其他实现它更加的轻量和高效,下面以实例来简单了解其工作方式。
接着上面 nginx
镜像的例子,拉取镜像后相应的 layer
解压在 /var/lib/docker/overlay2
目录中:
$ ll /var/lib/docker/overlay2 | tee layers.a
drwx-----x 4 root root 72 7月 20 17:20 335aaf02cbde069ddf7aa0077fecac172d4b2f0240975ab0ebecc3f94f1420cc
drwx-----x 3 root root 47 7月 19 10:04 560df35d349e6a750f1139db22d4cb52cba2a1f106616dc1c0c68b3cf11e3df6
drwx-----x 4 root root 72 7月 20 17:20 769a9f5d698522d6e55bd9882520647bd84375a751a67a8ccad1f7bb1ca066dd
drwx-----x 4 root root 72 7月 20 17:20 97aaf293fef495f0f06922d422a6187a952ec6ab29c0aa94cd87024c40e1a7e8
drwx-----x 4 root root 72 7月 20 17:20 a91fb6955249dadfb34a3f5f06d083c192f2774fbec5fbb1db42a04e918432c0
brw------- 1 root root 253, 1 7月 19 10:00 backingFsBlockDev
drwx-----x 4 root root 72 7月 20 17:20 fa29ec8cfe5a6c0b2cd1486f27a20a02867126edf654faad7f3520a220f3705f
drwx-----x 2 root root 278 7月 20 17:25 l
我们将输出结果保存到 layers.a
文件中供之后对比。其中 6 个名称特别长的目录中存放了镜像的 6 个 layer
(目录名称和 manifest.json
中的名称并不对应), l
目录中包含了指向 layers
文件夹的软链接,主要目的是在执行 mount
命令时缩短目录标识符的长度以避免超出页大小限制。
每个 layer
文件夹包含的内容如下:
$ cd /var/lib/docker/overlay2/
$ ll 335aaf02cbde069ddf7aa0077fecac172d4b2f0240975ab0ebecc3f94f1420cc
-rw------- 1 root root 0 7月 15 17:00 committed
drwxr-xr-x 3 root root 33 7月 15 17:00 diff
-rw-r--r-- 1 root root 26 7月 15 17:00 link
-rw-r--r-- 1 root root 86 7月 15 17:00 lower
drwx------ 2 root root 6 7月 15 17:00 work
作者仅向会员开放了此篇文章。开通会员即可立即解锁此文章和其他会员专属权益。