仅限会员阅读

容器技术原理(一):从根本上认识容器镜像

2024年04月27日 03:01  ·  阅读 361

从 OCI 规范说起

OCI(Open Container Initiative)规范是事实上的容器标准,已经被大部分容器实现以及容器编排系统所采用,包括 Docker 和 Kubernetes。它的出现是一段关于开源商业化的有趣历史:它由 Dokcer 公司作为领头者在 2015 年推出,但如今 Docker 公司在容器行业中已经成了打工仔。

从 OCI 规范开始了解容器镜像,可以让我们对容器技术建立更全面清晰的认知,而不是囿于实现细节。OCI 规范分为 Image specRuntime spec 两部分,它们分别覆盖了容器生命周期的不同阶段:

镜像规范

镜像规范定义了如何创建一个符合 OCI 规范的镜像,它规定了镜像的构建系统需要输出的内容和格式,输出的容器镜像可以被解包成一个 runtime bundleruntime bundle 是由特定文件和目录结构组成的一个文件夹,从中可以根据运行时标准运行容器。

镜像里面都有什么

规范要求镜像内容必须包括以下 3 部分:

  • Image Manifest:提供了镜像的配置和文件系统层定位信息,可以看作是镜像的目录,文件格式为 json
  • Image Layer Filesystem Changeset:序列化之后的文件系统和文件系统变更,它们可按顺序一层层应用为一个容器的 rootfs,因此通常也被称为一个 layer(与下文提到的镜像层同义),文件格式可以是 targzip 等存档或压缩格式。
  • Image Configuration:包含了镜像在运行时所使用的执行参数以及有序的 rootfs 变更信息,文件类型为 json

rootfs (root file system)即 / 根挂载点所挂载的文件系统,是一个操作系统所包含的文件、配置和目录,但并不包括操作系统内核,同一台机器上的所有容器都共享宿主机操作系统的内核。

接下来我们以 Dockernginx 为例探索一个镜像的实际内容。拉取一个最新版本的 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"
        ]
    }
]

其中记载了 ConfigLayers 的文件定位信息,也就是标准中所规定的 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 中的 OverlayFS 是如何工作的

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

专栏家上阅读行业精英撰写的最佳文章

作者仅向会员开放了此篇文章。开通会员即可立即解锁此文章和其他会员专属权益。

所有会员专属文章免费阅读
成为你感兴趣领域的专家
获取关于技术的数千个问题的深入解答
发展你的职业生涯或开始新的职业
评论
全部评论