镜像是怎样炼成的

作者:Nicola Apicella

原文:How are docker images built? A look into the Linux overlay file-systems and the OCI specification

要使用 Docker,就不可避免地要和 Docker 镜像打交道。本文将会讲述 Docker 镜像的基石: Overlay 文件系统。首先我会简单介绍一下这个文件系统,接下来会看看如何把这个技术用在 Docker 镜像上,以及 Docker 是怎样从 Dockerfile 构建出 Docker 镜像的。最后还会介绍分层缓存以及 OCI 格式的容器镜像。

遵循我的一贯风格,我会尽可能的让本文具备更好的操作性。

Overlay 文件系统是什么

Overlay 文件系统(也被称为联合文件系统),能够使用两个或更多的目录创建一个联合:它由低层和高层的目录组成。文件系统中低层的目录是只读的,而高层的文件系统则是可读可写的。我们可以试试加载一个,看看操作效果。

创建 Overlay 文件系统

我们可以创建几个目录然后把它们联合起来。首先会创建一个叫做 “mount” 的目录,我们将它作为这个联合的父目录。接下来会创建 “layer-1”、“layer-2”、“layer-3”、“layer-4” 着几个目录。最后还要创建一个叫做 “workdir” 的目录, Overlay 文件系统必须有这个目录才能正常工作。

这些目录可以随意命名,不过 “layer-1”、“layer-2” 这样的命名方式,和 Docker 镜像对比起来会比较容易理解。


$ cd /tmp && mkdir overlay-example && cd overlay-example

[2020-04-19 16:02:35] [ubuntu] [/tmp/overlay-example]  
> mkdir mount layer-1 layer-2 layer-3 layer-4 workdir

[2020-04-19 16:02:38] [ubuntu] [/tmp/overlay-example]  
$ ls
layer-1  layer-2  layer-3  layer-4 mount  workdir

然后要在除 “layer-4” 之外的每个目录下创建文件,这个步骤也不是必要的,只是为了更像镜像:

[2020-04-19 16:02:40] [ubuntu] [/tmp/overlay-example]  
$ echo "Layer-1 file" > ./layer-1/some-file-in-layer-1

[2020-04-19 16:03:36] [ubuntu] [/tmp/overlay-example]  
$ echo "Layer-2 file" > ./layer-2/some-file-in-layer-2

[2020-04-19 16:03:53] [ubuntu] [/tmp/overlay-example]  
$ echo "Layer-3 file" > ./layer-3/some-file-in-layer-3

我们来挂载这个文件系统:

sudo mount -t overlay overlay-example \
-o lowerdir=/tmp/overlay-example/layer-1:/tmp/overlay-example/layer-2:/tmp/overlay-example/layer-3,upperdir=/tmp/overlay-example/layer-4,workdir=/tmp/overlay-example/workdir \
/tmp/overlay-example/mount

看看挂载目录的内容:

[2020-04-19 16:13:28] [ubuntu] [/tmp/overlay-example]  
> cd mount/

[2020-04-19 16:13:31] [ubuntu] [/tmp/overlay-example/mount]  
> ls -la
total 20
drwxr-xr-x 1 napicell domain^users 4096 Apr 19 16:07 .
drwxr-xr-x 8 napicell domain^users 4096 Apr 19 16:07 ..
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-1
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-2
-rw-r--r-- 1 napicell domain^users   13 Apr 19 16:03 some-file-in-layer-3

不出所料,前三层的文件都被加载到了挂载根目录。可以看到我们之前写入文件的内容:

$ cat some-file-in-layer-3
Layer-3 file

试试创建文件

$ echo "new content" > new-file

$ ls
new-file  some-file-in-layer-1  some-file-in-layer-2  some-file-in-layer-3

新文件在哪里呢?自然是在上层,我们的例子里就是 “layer-4”:

 [2020-04-19 16:23:49] [ubuntu] [/tmp/overlay-example]  
 pactvm > tree
.
├── layer-1
│   └── some-file-in-layer-1
├── layer-2
│   └── some-file-in-layer-2
├── layer-3
│   └── some-file-in-layer-3
├── layer-4
│   └── new-file
├── mount
│   ├── new-file
│   ├── some-file-in-layer-1
│   ├── some-file-in-layer-2
│   └── some-file-in-layer-3
└── workdir
    └── work [error opening dir]

7 directories, 8 files

试试看删除文件:

[2020-04-19 16:27:33] [ubuntu] [/tmp/overlay-example/mount]  
> rm some-file-in-layer-2

[2020-04-19 16:28:58] [ubuntu] [/tmp/overlay-example/mount]  
> ls
new-file  some-file-in-layer-1  some-file-in-layer-3

你猜猜,原始文件系统中的 “layer-2” 目录会怎么样:

 [2020-04-19 16:29:57] [ubuntu] [/tmp/overlay-example]  
 pactvm > tree
.
├── layer-1
│   └── some-file-in-layer-1
├── layer-2
│   └── some-file-in-layer-2
├── layer-3
│   └── some-file-in-layer-3
├── layer-4
│   ├── new-file
│   └── some-file-in-layer-2
├── mount
│   ├── new-file
│   ├── some-file-in-layer-1
│   └── some-file-in-layer-3
└── workdir
    └── work [error opening dir]

7 directories, 8 files

“layer-4” 中出现了个新文件 “some-file-in-layer-2”。奇怪的是这个文件的属性(”Character file“),这种文件在 Overlay 文件系统中被称为 ”Whitout“,用于表达被删除的文件。

 [2020-04-19 16:31:09] [ubuntu] [/tmp/overlay-example/layer-4]  
 pactvm > ls -la
total 12
drwxr-xr-x 2 napicell domain^users 4096 Apr 19 16:28 .
drwxr-xr-x 8 napicell domain^users 4096 Apr 19 16:07 ..
-rw-r--r-- 1 napicell domain^users   12 Apr 19 16:23 new-file
c--------- 1 root     root         0, 0 Apr 19 16:28 some-file-in-layer-2

完成之后,卸载这个文件系统,然后删除目录:

[2020-04-19 16:37:11] [ubuntu] [/tmp/overlay-example]  
$ sudo umount /tmp/overlay-example/mount && rm -rf *

理顺概念

正如开篇所说, Overlay 文件系统上可以把多个目录联合在一起。在前边的例子里,这个联合过程由 “layer-{1,2,3,4}” 在 “mount” 目录里组成。对文件的修改、创建和删除都在上层发生——也就是这里的 “layer-4”,因此这一层也被称为差异层。上层的文件会对下层文件造成遮盖。假设 “layer-2” 和 “layer-1” 中,在相同的相对目录下有同名的文件,那么在 “mount” 目录中就会以 “layer-2” 为准。下一节将会看看这一技术在 Docker 镜像中的应用。

什么是 Docker 镜像

简单总结,Docker 镜像就是一个 Tar 文件,其中包含一个根文件系统和一些愿数据。你可能听说过,Dockerfile 中的每一行都会生成一个层。例如下面的代码就会生成一个三层的镜像:

FROM scratch
ADD my-files /doc
ADD hello /
CMD ["/hello"]

“docker run” 的过程很复杂,但是本文中只会关注和镜像有关的一点点内容。概括的说,Docker 会下载这个文件包,把每个层解压到单独的目录中,然后用 Overlay 文件系统将这些目录以及用于进行写入的一个上层空目录联合起来。当你在容器中进行修改、创建或者删除操作时,这些变更都会保存到这个空目录中。容器退出时,Docker 会清理这个目录——这就是在容器中的变更无法保持的原因。

层缓存

要运行容器,就要构建镜像,Docker 将这两个步骤分离开来独立运作,是它得以流行的重要原因。OCI 就是业界公认的规范。

OCI 当前包括两个规范:运行规范和镜像规范。运行规范描述了如何运行一个解压到磁盘上的 “复合文件系统” 。简单说来,OCI 实现会把 OCI 镜像下载回来,然后解压到一个 OCI 运行时复合文件系统之中。这一操作完成后就可以让 OCI 运行时运行了。

标准化的意义就是让其他人可以自己开发容器的构建工具和运行时。例如 jess/imgBuildah 以及 Skopeo 都是可以脱离 Docker 构建镜像的工具。类似地还有很多容器运行时,例如 runc(Docker 使用) 和 rkt。

其他的 Overlay 文件系统

Docker 能够使用的联合文件系统不止这一种。任何有差异层和联合特性的文件系统都是可能的候选者。例如 Docker 还能运行在 aufs、btrfs、zfs 和 devicemapper 系统上。

构建镜像时发生了什么

假设我们要使用下面的 Dockerfile 来构建镜像:

FROM ubuntu
RUN apt-get update
...

简单描述一下这个过程:

  1. Docker 下载 FROM 语句中指定的 tar 文件,这是目标镜像的第一层。
  2. 加载一个联合文件系统,其底层就是刚下载的部分,在上面创建一个空目录。
  3. 在 chroot 中启动一个 bash,运行 RUN 语句中的命令:RUN: chroot . /bin/bash -c "apt get update"
  4. 命令结束后,会把上层目录压缩,形成新镜像中的新的一层。
  5. 如果 Dockerfile 中包含其它命令,就以之前构建的层次为基础,从第二步开始重复创建新层,直到完成所有语句后退出。

上述过程是个极度简化的过程,其中缺乏一些常见指令,例如 ENTRYPOINTENV 等。这些内容会被写入元数据,和文件层封装在一起。

结论

这种将根文件系统和每个差异层都进行打包的思路非常强大。它不仅是 Docker 的基础,我想还能用在其它一些领域里,以后可能会诞生更多这类工具。

Avatar
崔秀龙

简单,是大师的责任;我们凡夫俗子,能做到清楚就很不容易了。

comments powered by Disqus
下一页
上一页

相关