Docker 容器与镜像的储存

VincentAlexia 发布于10月前 阅读84次
0 条评论

Docker 容器与镜像的储存

在 Docker 的生态中,有容器(container)和镜像(image)两个重要的概念,那么容器和镜像是如何在主机(host)上储存的呢?

系统信息

  • 系统: Ubuntu 16.04
  • Docker: 17.10.0-ce
    • Storage Driver: overlay2

镜像

首先来看下什么是容器,引用 Docker 官方的话的就是

容器是一个轻量级(lightweight)、独立的(stand-alone)和包含一系列软件能够执行的程序包

那么镜像和容器有什么关系呢?容器可以认为是一个实例化的镜像的。镜像在系统上,是分层储存的,每一层的文件、配置信息叠加在一起,就成为了镜像。

制作

首先看下制作镜像,一般情况下,是通过编写 Dockerfile 然后使用 Docker 命令来生成一个镜像。

下面来看一个例子,首先新建一个文件,名字为 Dockerfile,内容如下

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"]

然后通过执行 docker build -t repository:tag . 命令,就可以生成一个名为 repository:tag 的镜像。

通过 docker history repository:tag 命令可以看到镜像的每一层的信息,在我的机器上,输出如下

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

可以通过上面的信息看到在 Dockerfile 中的每一个『命令』都被映射到了每一层,其实在制作镜像的过程,在 RUN 命令执行时,docker 会运行一个临时容器,在里面运行 RUN 后面的命令,然后再把容器提交成为镜像,所以,容器可以变成镜像,镜像也可以变成容器。

通过上面的输出的第一列可以看出,在 docker 里面,其实每一层都是一个 image,但是一般情况下,大家都把 `repository:tag` 这个称为一个镜像。

储存

由于 Unix 一切皆文件,所以 Docker 镜像也是以文件的形式储存在系统中,并且是分层储存的。

下面来看另外一个例子,Dockerfile 如下

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

然后通过 docker buil -t repository:layer . 命令,生成一个名为 repository:layer 的镜像,镜像 ID 为 e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8 ,然后执行 docker inspect repository:layer ,可以得到以下信息(省略了部分)

{
  // ...
  "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"
  }
  // ...
}

其中 GraphDriver.Data 下的信息就是镜像在机器上的储存路径了。

将上面信息整理一下,得到下面的结构

  1. /var/lib/docker/overlay2/92820.../diff
  2. /var/lib/docker/overlay2/cc191.../diff
  3. /var/lib/docker/overlay2/5157f.../diff
  4. /var/lib/docker/overlay2/3d008.../diff
  5. /var/lib/docker/overlay2/53f44.../diff
  6. /var/lib/docker/overlay2/3b5e8.../diff
  7. /var/lib/docker/overlay2/4f144.../diff

从上到下,就是镜像当前层的文件与之前所有层的 diff 情况。

与上面镜像的 Dockerfile 对应起来看就是,1 中存的文件就是 echo 'echo "hello"' >> /etc/profile 的改变,因为 /etc/profile 这个文件在之前的层是存在的。

所以在 docker 制作镜像的过程中,docker 会将 /etc/profile 拷贝一份,然后在拷贝的基础上修改储存,diff 的级别是文件本身,而不是文件内容。

7 对应的就是看似是 FROM alpine:3.4 这一行,其实,是因为 alpine:3.4 这个镜像就一层,所以在这里看起来,基础镜像会是一层。

其他层的与 Dockerfile 也是一一对应的。

而 WORKDIR /data/layer 这一条 Dockerfile,是没有文件的改变,所以没有单独的一层来储存,是存在 /var/lib/docker/image/overlay2/imagedb/content/sha256 这里的配置信息中。

上面是在 overlay2 这个 driver 中的储存结构,但是 docker 支持多种 driver,那么 docker 是如何在不同 driver 中相互导入导出的并且保持镜像结构不变的呢?

可以看下 docker image 脱离于 driver 的结构,首先将镜像从 docker 中导出,执行 docker save repository:layer -o image.tar 会在当前目录下生成一个 image.tar 的文件。

解压后就会得到 repository:layer 这个镜像的每一层的文件信息了,解压后的主要文件信息如下

  • e7001f202e365558d9d922010e56775d8d1538d72911c86d8e7b0d9482d9cff8.json 存的镜像的配置信息。
  • repositories 文件存的是镜像顶层的 layer 信息,在我这里是 f8504ccc4a74115c572be9f13925c63b628b1e3c5eb347196f62971aa8e9a335 这个 ID,也就是 layer index。

通过 repositories 里信息,可以看到 ID 的 。除了上面说的两个文件,解压出来的还有以 layer ID 命名的目录。

根据 repositories 中的 layer ID 进入到对应的目录里。

里面有三个文件,其中 layer.tar 里存的就是这一层与之前所以层的 diff 文件,也就是上面 1 中的文件, /etc/profile 。

然后还有一个 json 文件,里面存的是这一层在镜像制作过程中的临时容器信息,还有一个最重要的 parent 项,里面存的信息就是这一层的下面一层的 ID,根据这个 ID 就可以依次找到每一层的信息。

这里面存的就是镜像的信息,把这个 image.tar 拿到其他装有 docker 的机器上,通过 docker load -i image.tar 就可以将镜像导入到 docker 中。

根据上面的镜像储存的文件信息,可以看出,镜像是分层储存的。

容器

Docker 容器与镜像的储存

图片来源网络

上面说了,容器就是一个镜像的实例化的表现,所以,容器也是分层的,当运行一个容器时,会在镜像的最上层加一个 writable layer(如上图所属),在容器运行时对于容器的读写文件操作,都是作用在 writable layer 的。

将上面的 repository:layer 镜像通过命令 docker run -it --name layer --rm repository:layer sh 运行起来,然后再次通过 docker inspect layer 这个命令,还是看 GraphDriver.Data 信息

{
  "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"
  }
}

从上面可以看到,从 /var/lib/docker/overlay2/92820.../diff 开始,都是和上面镜像一模一样的文件夹。

唯一的区别就是 /var/lib/docker/overlay2/9b949f...-init/diff ,这个是容器在运行时的 init layer,里面存的是容器的 host 和 dns 信息,这一层也是 readonly layer 。

真正的 writable layer 是 /var/lib/docker/overlay2/9b949... 。

如果在上面运行的容器中去修改一下 /data/layer/layer3 文件的值为 4,

对应的在系统中的 /var/lib/docker/overlay2/9b949.../diff 目录下,

就会多出一个 data/layer/layer3 的文件,并且文件内容为 4 。

而 /var/lib/docker/overlay2/9b949.../merged 目录中就是容器中的用户视角的所以文件了,包含这个容器的每一层文件,所以在这个目录下的 data/layer/layer3 文件的内容也会变成 4 。

以上就是容器在系统中的储存结构了。

registry

registry 是镜像在服务端的储存仓库, docker hub 就是 docker 官方提供的 docker registry。

我们也可以通过官方提供的 distribution 来自己搭建私有的镜像仓库。

在 registry 中,镜像也是以分层的形式储存的,registry 也是支持多种储存方式( driver )的,默认就是 filesystem 本地文件存储,关于自定义 driver 可以看 这里

通过 docker run -d -v /var/lib/registry:/var/lib/registry -p 5000:5000 registry:2 来在本地运行一个镜像仓库。

然后将我们前面制作的 repository:layer 推送到这个镜像仓库中。

其实镜像的名字,实际上是应该要包含镜像仓库的地址的,如果不写,默认就是官方的 docker hub 了。

所以推送之前,先需要将我们的镜像通过 docker tag repository:layer 127.0.0.1:5000/repository:layer 命令重新命名一下。

然后执行 docker push 127.0.0.1:5000/repository:layer 就可以将镜像推送都刚刚运行的镜像仓库中了。

在推送的过程中,也是可以看到,镜像是分层推送的。

当推送完毕之后,可以在主机上的 /var/lib/registry/docker/registry/v2 这个目录下看到刚刚推送的镜像了,当然,也是分层储存的,并且镜像的每一层的文件、配置信息与连接每一层的 index 是分开储存的,这样就可以在镜像仓库中复用同一层,当推送的镜像的某一层在 registry 中时,docker 就不会再次推送这一层了,可以加速镜像的推送,也可以节省储存空间。

其中 repositories/repository 这个目录,表示的是镜像 127.0.0.1:5000/repository:layer 的 repository 这个 namespace。

在这个目录下的 _manifests/tags 目录下,则存的是这个 namespace 下所以的 tag 了,比如我们刚刚推送的 tag 是 layer ,所以会有一个 layer 的目录,里面包含了 layer 这个 tag 的 index 信息。

通过 index 信息,就可以在 repositories/repository/layer/_layers/sha256 里面找到每一层的 index,根据 index 可以在 repositories/blobs 下面找到对应的每一层的文件和配置信息。

相同的层的只会存一份。

编写 Dockerfile

通过上面的镜像的储存分析,所以在编写 Dockerfile 的时候,可以遵循下面的几点规则

  • 合理分层,重复利用镜像缓存
  • 只删除当前层中创建的文件
  • 选择较小体积的基础镜像(比如 alpine)

查看原文: Docker 容器与镜像的储存

  • whitebear
  • bigbird
  • yellowtiger
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。