原文: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/img
、Buildah
以及 Skopeo
都是可以脱离 Docker 构建镜像的工具。类似地还有很多容器运行时,例如 runc(Docker 使用) 和 rkt。
其他的 Overlay 文件系统
Docker 能够使用的联合文件系统不止这一种。任何有差异层和联合特性的文件系统都是可能的候选者。例如 Docker 还能运行在 aufs、btrfs、zfs 和 devicemapper 系统上。
构建镜像时发生了什么
假设我们要使用下面的 Dockerfile 来构建镜像:
FROM ubuntu
RUN apt-get update
...
简单描述一下这个过程:
- Docker 下载 FROM 语句中指定的 tar 文件,这是目标镜像的第一层。
- 加载一个联合文件系统,其底层就是刚下载的部分,在上面创建一个空目录。
- 在 chroot 中启动一个 bash,运行 RUN 语句中的命令:
RUN: chroot . /bin/bash -c "apt get update"
。 - 命令结束后,会把上层目录压缩,形成新镜像中的新的一层。
- 如果 Dockerfile 中包含其它命令,就以之前构建的层次为基础,从第二步开始重复创建新层,直到完成所有语句后退出。
上述过程是个极度简化的过程,其中缺乏一些常见指令,例如 ENTRYPOINT
、ENV
等。这些内容会被写入元数据,和文件层封装在一起。
结论
这种将根文件系统和每个差异层都进行打包的思路非常强大。它不仅是 Docker 的基础,我想还能用在其它一些领域里,以后可能会诞生更多这类工具。
文章来源于互联网:镜像是怎样炼成的