Docker 容器与镜像的储存
derek_zhan
7年前
<p><img src="https://simg.open-open.com/show/d666b35857fe14f56af7385a10c6a8c4.jpg" alt="Docker 容器与镜像的储存" width="550" height="413"></p> <p>在 Docker 的生态中,有容器(container)和镜像(image)两个重要的概念,那么容器和镜像是如何在主机(host)上储存的呢?</p> <h2>系统信息</h2> <ul> <li>系统: Ubuntu 16.04</li> <li>Docker: 17.10.0-ce <ul> <li>Storage Driver: overlay2</li> </ul> </li> </ul> <h2>镜像</h2> <p>首先来看下什么是容器,引用 Docker 官方的话的就是</p> <p>容器是一个轻量级(lightweight)、独立的(stand-alone)和包含一系列软件能够执行的程序包</p> <p>那么镜像和容器有什么关系呢?容器可以认为是一个实例化的镜像的。镜像在系统上,是分层储存的,每一层的文件、配置信息叠加在一起,就成为了镜像。</p> <h2>制作</h2> <p>首先看下制作镜像,一般情况下,是通过编写 Dockerfile 然后使用 Docker 命令来生成一个镜像。</p> <p>下面来看一个例子,首先新建一个文件,名字为 Dockerfile,内容如下</p> <pre> FROM debian:8 MAINTAINER @cloverstd <cloverstd@gmail.com> RUN apt-get update -y && \ apt-get install -y emacs RUN apt-get install -y apache2 CMD ["/bin/bash"]</pre> <p>然后通过执行 <code>docker build -t repository:tag .</code> 命令,就可以生成一个名为 <code>repository:tag</code> 的镜像。</p> <p>通过 <code>docker history repository:tag</code> 命令可以看到镜像的每一层的信息,在我的机器上,输出如下</p> <pre> IMAGE CREATED CREATED BY SIZE COMMENT d951e6ed5b00 34 minutes ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B 4ea03e7b0db6 34 minutes ago /bin/sh -c apt-get install -y apache2 13.5MB 9ea713f268c9 36 minutes ago /bin/sh -c apt-get update -y && apt-ge... 364MB 0f8e9812e8b8 42 minutes ago /bin/sh -c #(nop) MAINTAINER @cloverstd <... 0B 25fc9eb3417f 4 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B <missing> 4 weeks ago /bin/sh -c #(nop) ADD file:55b071e2cfc3ea2... 123MB</pre> <p>可以通过上面的信息看到在 Dockerfile 中的每一个『命令』都被映射到了每一层,其实在制作镜像的过程,在 <code>RUN</code> 命令执行时,docker 会运行一个临时容器,在里面运行 <code>RUN</code> 后面的命令,然后再把容器提交成为镜像,所以,容器可以变成镜像,镜像也可以变成容器。</p> <p>通过上面的输出的第一列可以看出,在 docker 里面,其实每一层都是一个 image,但是一般情况下,大家都把 `repository:tag` 这个称为一个镜像。</p> <h2>储存</h2> <p>由于 Unix 一切皆文件,所以 Docker 镜像也是以文件的形式储存在系统中,并且是分层储存的。</p> <p>下面来看另外一个例子,Dockerfile 如下</p> <pre> FROM alpine:3.4 RUN mkdir -p /data/layer WORKDIR /data/layer COPY layer1 /data/layer COPY layer2 /data/layer RUN touch /data/layer/layer1 COPY layer3 /data/layer RUN echo 'echo "hello"' >> /etc/profile</pre> <p>然后通过 <code>docker buil -t repository:layer .</code> 命令,生成一个名为 <code>repository:layer</code> 的镜像,镜像 ID 为 <code>e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8</code> ,然后执行 <code>docker inspect repository:layer</code> ,可以得到以下信息(省略了部分)</p> <pre> { // ... "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/cc191abf48cfa6ba96e1f4eae0133743c6cdcc6eb9942624bd0ad4df015d1f85/diff:/var/lib/docker/overlay2/5157fc9701ca747754ad8f3a18622ae1d38aab8302324c34cb5614ee30b7abdb/diff:/var/lib/docker/overlay2/3d008a0d62a6ce66adba7401a6a887a87cc0ee3fba306e7d06fcbd4d76f35207/diff:/var/lib/docker/overlay2/53f442e9e9c78238eb98fc3a9d418b66218ab34cfeb5618adb3c40558b8f5b59/diff:/var/lib/docker/overlay2/3b5e8ca8ad4b0b4605a7e27f272e5ad85a9198ac6ae730c4de3a6ee27ab558bb/diff:/var/lib/docker/overlay2/4f144dd9d686cc3c6f1dae44e921e20969ea4b977f7beef16d6f8a258f1cb894/diff", "MergedDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/merged", "UpperDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/diff", "WorkDir": "/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/work" }, "Name": "overlay2" } // ... }</pre> <p>其中 <code>GraphDriver.Data</code> 下的信息就是镜像在机器上的储存路径了。</p> <p>将上面信息整理一下,得到下面的结构</p> <ol> <li>/var/lib/docker/overlay2/92820.../diff</li> <li>/var/lib/docker/overlay2/cc191.../diff</li> <li>/var/lib/docker/overlay2/5157f.../diff</li> <li>/var/lib/docker/overlay2/3d008.../diff</li> <li>/var/lib/docker/overlay2/53f44.../diff</li> <li>/var/lib/docker/overlay2/3b5e8.../diff</li> <li>/var/lib/docker/overlay2/4f144.../diff</li> </ol> <p>从上到下,就是镜像当前层的文件与之前所有层的 diff 情况。</p> <p>与上面镜像的 Dockerfile 对应起来看就是,1 中存的文件就是 <code>echo 'echo "hello"' >> /etc/profile</code> 的改变,因为 <code>/etc/profile</code> 这个文件在之前的层是存在的。</p> <p>所以在 docker 制作镜像的过程中,docker 会将 <code>/etc/profile</code> 拷贝一份,然后在拷贝的基础上修改储存,diff 的级别是文件本身,而不是文件内容。</p> <p>7 对应的就是看似是 <code>FROM alpine:3.4</code> 这一行,其实,是因为 <code>alpine:3.4</code> 这个镜像就一层,所以在这里看起来,基础镜像会是一层。</p> <p>其他层的与 Dockerfile 也是一一对应的。</p> <p>而 <code>WORKDIR /data/layer</code> 这一条 Dockerfile,是没有文件的改变,所以没有单独的一层来储存,是存在 <code>/var/lib/docker/image/overlay2/imagedb/content/sha256</code> 这里的配置信息中。</p> <p>上面是在 overlay2 这个 driver 中的储存结构,但是 docker 支持多种 driver,那么 docker 是如何在不同 driver 中相互导入导出的并且保持镜像结构不变的呢?</p> <p>可以看下 docker image 脱离于 driver 的结构,首先将镜像从 docker 中导出,执行 <code>docker save repository:layer -o image.tar</code> 会在当前目录下生成一个 <code>image.tar</code> 的文件。</p> <p>解压后就会得到 <code>repository:layer</code> 这个镜像的每一层的文件信息了,解压后的主要文件信息如下</p> <ul> <li><code>e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8.json</code> 存的镜像的配置信息。</li> <li><code>repositories</code> 文件存的是镜像顶层的 layer 信息,在我这里是 <code>f8504ccc4a74115c572be9f13925c63b628b1e3c5eb347196f62971aa8e9a335</code> 这个 ID,也就是 layer index。</li> </ul> <p>通过 <code>repositories</code> 里信息,可以看到 ID 的 。除了上面说的两个文件,解压出来的还有以 layer ID 命名的目录。</p> <p>根据 <code>repositories</code> 中的 layer ID 进入到对应的目录里。</p> <p>里面有三个文件,其中 <code>layer.tar</code> 里存的就是这一层与之前所以层的 diff 文件,也就是上面 1 中的文件, <code>/etc/profile</code> 。</p> <p>然后还有一个 <code>json</code> 文件,里面存的是这一层在镜像制作过程中的临时容器信息,还有一个最重要的 <code>parent</code> 项,里面存的信息就是这一层的下面一层的 ID,根据这个 ID 就可以依次找到每一层的信息。</p> <p>这里面存的就是镜像的信息,把这个 <code>image.tar</code> 拿到其他装有 docker 的机器上,通过 <code>docker load -i image.tar</code> 就可以将镜像导入到 docker 中。</p> <p>根据上面的镜像储存的文件信息,可以看出,镜像是分层储存的。</p> <h2>容器</h2> <p><img src="https://simg.open-open.com/show/4bdc9f3d427a74fa21fa5d015661a02f.jpg" alt="Docker 容器与镜像的储存" width="550" height="413"></p> <p>图片来源网络</p> <p>上面说了,容器就是一个镜像的实例化的表现,所以,容器也是分层的,当运行一个容器时,会在镜像的最上层加一个 writable layer(如上图所属),在容器运行时对于容器的读写文件操作,都是作用在 writable layer 的。</p> <p>将上面的 <code>repository:layer</code> 镜像通过命令 <code>docker run -it --name layer --rm repository:layer sh</code> 运行起来,然后再次通过 <code>docker inspect layer</code> 这个命令,还是看 <code>GraphDriver.Data</code> 信息</p> <pre> { "GraphDriver": { "Data": { "LowerDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b-init/diff:/var/lib/docker/overlay2/92820aa50dce9750006c7afcb53c110f7f254818e42d4a641a21ef397652a687/diff:/var/lib/docker/overlay2/cc191abf48cfa6ba96e1f4eae0133743c6cdcc6eb9942624bd0ad4df015d1f85/diff:/var/lib/docker/overlay2/5157fc9701ca747754ad8f3a18622ae1d38aab8302324c34cb5614ee30b7abdb/diff:/var/lib/docker/overlay2/3d008a0d62a6ce66adba7401a6a887a87cc0ee3fba306e7d06fcbd4d76f35207/diff:/var/lib/docker/overlay2/53f442e9e9c78238eb98fc3a9d418b66218ab34cfeb5618adb3c40558b8f5b59/diff:/var/lib/docker/overlay2/3b5e8ca8ad4b0b4605a7e27f272e5ad85a9198ac6ae730c4de3a6ee27ab558bb/diff:/var/lib/docker/overlay2/4f144dd9d686cc3c6f1dae44e921e20969ea4b977f7beef16d6f8a258f1cb894/diff", "MergedDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/merged", "UpperDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/diff", "WorkDir": "/var/lib/docker/overlay2/9b949f2ddb766c5fe0e66aa4e81b66c2367a6a3d1f6658ab6ac863a66b63dd4b/work" }, "Name": "overlay2" } }</pre> <p>从上面可以看到,从 <code>/var/lib/docker/overlay2/92820.../diff</code> 开始,都是和上面镜像一模一样的文件夹。</p> <p>唯一的区别就是 <code>/var/lib/docker/overlay2/9b949f...-init/diff</code> ,这个是容器在运行时的 init layer,里面存的是容器的 host 和 dns 信息,这一层也是 readonly layer 。</p> <p>真正的 writable layer 是 <code>/var/lib/docker/overlay2/9b949...</code> 。</p> <p>如果在上面运行的容器中去修改一下 <code>/data/layer/layer3</code> 文件的值为 4,</p> <p>对应的在系统中的 <code>/var/lib/docker/overlay2/9b949.../diff</code> 目录下,</p> <p>就会多出一个 <code>data/layer/layer3</code> 的文件,并且文件内容为 <code>4</code> 。</p> <p>而 <code>/var/lib/docker/overlay2/9b949.../merged</code> 目录中就是容器中的用户视角的所以文件了,包含这个容器的每一层文件,所以在这个目录下的 <code>data/layer/layer3</code> 文件的内容也会变成 <code>4</code> 。</p> <p>以上就是容器在系统中的储存结构了。</p> <h2>registry</h2> <p>registry 是镜像在服务端的储存仓库, <a href="/misc/goto?guid=4959755860220664397" rel="nofollow,noindex">docker hub </a>就是 docker 官方提供的 docker registry。</p> <p>我们也可以通过官方提供的 <a href="/misc/goto?guid=4959755860318023331" rel="nofollow,noindex">distribution </a>来自己搭建私有的镜像仓库。</p> <p>在 registry 中,镜像也是以分层的形式储存的,registry 也是支持多种储存方式( driver )的,默认就是 <code>filesystem</code> 本地文件存储,关于自定义 driver 可以看 <a href="/misc/goto?guid=4959755860398540466" rel="nofollow,noindex">这里 </a>。</p> <p>通过 <code>docker run -d -v /var/lib/registry:/var/lib/registry -p 5000:5000 registry:2</code> 来在本地运行一个镜像仓库。</p> <p>然后将我们前面制作的 <code>repository:layer</code> 推送到这个镜像仓库中。</p> <p>其实镜像的名字,实际上是应该要包含镜像仓库的地址的,如果不写,默认就是官方的 docker hub 了。</p> <p>所以推送之前,先需要将我们的镜像通过 <code>docker tag repository:layer 127.0.0.1:5000/repository:layer</code> 命令重新命名一下。</p> <p>然后执行 <code>docker push 127.0.0.1:5000/repository:layer</code> 就可以将镜像推送都刚刚运行的镜像仓库中了。</p> <p>在推送的过程中,也是可以看到,镜像是分层推送的。</p> <p>当推送完毕之后,可以在主机上的 <code>/var/lib/registry/docker/registry/v2</code> 这个目录下看到刚刚推送的镜像了,当然,也是分层储存的,并且镜像的每一层的文件、配置信息与连接每一层的 index 是分开储存的,这样就可以在镜像仓库中复用同一层,当推送的镜像的某一层在 registry 中时,docker 就不会再次推送这一层了,可以加速镜像的推送,也可以节省储存空间。</p> <p>其中 <code>repositories/repository</code> 这个目录,表示的是镜像 <code>127.0.0.1:5000/repository:layer</code> 的 <code>repository</code> 这个 namespace。</p> <p>在这个目录下的 <code>_manifests/tags</code> 目录下,则存的是这个 namespace 下所以的 tag 了,比如我们刚刚推送的 tag 是 <code>layer</code> ,所以会有一个 <code>layer</code> 的目录,里面包含了 <code>layer</code> 这个 tag 的 index 信息。</p> <p>通过 index 信息,就可以在 <code>repositories/repository/layer/_layers/sha256</code> 里面找到每一层的 index,根据 index 可以在 <code>repositories/blobs</code> 下面找到对应的每一层的文件和配置信息。</p> <p>相同的层的只会存一份。</p> <h2>编写 Dockerfile</h2> <p>通过上面的镜像的储存分析,所以在编写 Dockerfile 的时候,可以遵循下面的几点规则</p> <ul> <li>合理分层,重复利用镜像缓存</li> <li>只删除当前层中创建的文件</li> <li>选择较小体积的基础镜像(比如 alpine)</li> </ul> <p> </p> <p>来自:<a href="https://zhuanlan.zhihu.com/p/31744232?utm_source=tuicool&utm_medium=referral">https://zhuanlan.zhihu.com/p/31744232</a></p> <p> </p>