<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>oci | 伪架构师</title>
    <link>/tags/oci/</link>
      <atom:link href="/tags/oci/index.xml" rel="self" type="application/rss+xml" />
    <description>oci</description>
    <generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>zh</language><lastBuildDate>Fri, 24 Apr 2020 08:45:50 +0800</lastBuildDate>
    <image>
      <url>/img/logo-wide.png</url>
      <title>oci</title>
      <link>/tags/oci/</link>
    </image>
    
    <item>
      <title>镜像是怎样炼成的</title>
      <link>/post/how-are-docker-images-built/</link>
      <pubDate>Fri, 24 Apr 2020 08:45:50 +0800</pubDate>
      <guid>/post/how-are-docker-images-built/</guid>
      <description>

&lt;p&gt;作者：&lt;a href=&#34;https://twitter.com/napicellatwit&#34; target=&#34;_blank&#34;&gt;Nicola Apicella&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;原文：&lt;a href=&#34;https://dev.to/napicella/how-are-docker-images-built-a-look-into-the-linux-overlay-file-systems-and-the-oci-specification-175n&#34; target=&#34;_blank&#34;&gt;How are docker images built? A look into the Linux overlay file-systems and the OCI specification&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;遵循我的一贯风格，我会尽可能的让本文具备更好的操作性。&lt;/p&gt;

&lt;h2 id=&#34;overlay-文件系统是什么&#34;&gt;Overlay 文件系统是什么&lt;/h2&gt;

&lt;p&gt;Overlay 文件系统（也被称为联合文件系统），能够使用两个或更多的目录创建一个联合：它由低层和高层的目录组成。文件系统中低层的目录是只读的，而高层的文件系统则是可读可写的。我们可以试试加载一个，看看操作效果。&lt;/p&gt;

&lt;h3 id=&#34;创建-overlay-文件系统&#34;&gt;创建 Overlay 文件系统&lt;/h3&gt;

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

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

&lt;pre&gt;&lt;code class=&#34;language-comand&#34;&gt;
$ cd /tmp &amp;amp;&amp;amp; mkdir overlay-example &amp;amp;&amp;amp; cd overlay-example

[2020-04-19 16:02:35] [ubuntu] [/tmp/overlay-example]  
&amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;然后要在除 &amp;ldquo;layer-4&amp;rdquo; 之外的每个目录下创建文件，这个步骤也不是必要的，只是为了更像镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;[2020-04-19 16:02:40] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-1 file&amp;quot; &amp;gt; ./layer-1/some-file-in-layer-1

[2020-04-19 16:03:36] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-2 file&amp;quot; &amp;gt; ./layer-2/some-file-in-layer-2

[2020-04-19 16:03:53] [ubuntu] [/tmp/overlay-example]  
$ echo &amp;quot;Layer-3 file&amp;quot; &amp;gt; ./layer-3/some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;我们来挂载这个文件系统：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;看看挂载目录的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-shell&#34;&gt;[2020-04-19 16:13:28] [ubuntu] [/tmp/overlay-example]  
&amp;gt; cd mount/

[2020-04-19 16:13:31] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;不出所料，前三层的文件都被加载到了挂载根目录。可以看到我们之前写入文件的内容：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ cat some-file-in-layer-3
Layer-3 file
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;试试创建文件&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;$ echo &amp;quot;new content&amp;quot; &amp;gt; new-file

$ ls
new-file  some-file-in-layer-1  some-file-in-layer-2  some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;新文件在哪里呢？自然是在上层，我们的例子里就是 &amp;ldquo;layer-4&amp;rdquo;：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:23:49] [ubuntu] [/tmp/overlay-example]  
 pactvm &amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;试试看删除文件：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;[2020-04-19 16:27:33] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; rm some-file-in-layer-2

[2020-04-19 16:28:58] [ubuntu] [/tmp/overlay-example/mount]  
&amp;gt; ls
new-file  some-file-in-layer-1  some-file-in-layer-3
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;你猜猜，原始文件系统中的 &amp;ldquo;layer-2&amp;rdquo; 目录会怎么样：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:29:57] [ubuntu] [/tmp/overlay-example]  
 pactvm &amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&amp;ldquo;layer-4&amp;rdquo; 中出现了个新文件 &amp;ldquo;some-file-in-layer-2&amp;rdquo;。奇怪的是这个文件的属性（”Character file“），这种文件在 Overlay 文件系统中被称为 ”Whitout“，用于表达被删除的文件。&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt; [2020-04-19 16:31:09] [ubuntu] [/tmp/overlay-example/layer-4]  
 pactvm &amp;gt; 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
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;完成之后，卸载这个文件系统，然后删除目录：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-command&#34;&gt;[2020-04-19 16:37:11] [ubuntu] [/tmp/overlay-example]  
$ sudo umount /tmp/overlay-example/mount &amp;amp;&amp;amp; rm -rf *
&lt;/code&gt;&lt;/pre&gt;

&lt;h3 id=&#34;理顺概念&#34;&gt;理顺概念&lt;/h3&gt;

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

&lt;h2 id=&#34;什么是-docker-镜像&#34;&gt;什么是 Docker 镜像&lt;/h2&gt;

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

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM scratch
ADD my-files /doc
ADD hello /
CMD [&amp;quot;/hello&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;

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

&lt;h3 id=&#34;层缓存&#34;&gt;层缓存&lt;/h3&gt;

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

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

&lt;p&gt;标准化的意义就是让其他人可以自己开发容器的构建工具和运行时。例如 &lt;code&gt;jess/img&lt;/code&gt;、&lt;code&gt;Buildah&lt;/code&gt; 以及 &lt;code&gt;Skopeo&lt;/code&gt; 都是可以脱离 Docker 构建镜像的工具。类似地还有很多容器运行时，例如 runc（Docker 使用） 和 rkt。&lt;/p&gt;

&lt;h2 id=&#34;其他的-overlay-文件系统&#34;&gt;其他的 Overlay 文件系统&lt;/h2&gt;

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

&lt;h2 id=&#34;构建镜像时发生了什么&#34;&gt;构建镜像时发生了什么&lt;/h2&gt;

&lt;p&gt;假设我们要使用下面的 Dockerfile 来构建镜像：&lt;/p&gt;

&lt;pre&gt;&lt;code class=&#34;language-Dockerfile&#34;&gt;FROM ubuntu
RUN apt-get update
...
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;简单描述一下这个过程：&lt;/p&gt;

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

&lt;p&gt;上述过程是个极度简化的过程，其中缺乏一些常见指令，例如 &lt;code&gt;ENTRYPOINT&lt;/code&gt;、&lt;code&gt;ENV&lt;/code&gt; 等。这些内容会被写入元数据，和文件层封装在一起。&lt;/p&gt;

&lt;h2 id=&#34;结论&#34;&gt;结论&lt;/h2&gt;

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