在 Docker 的生态中,有容器(container)和镜像(image)两个重要的概念,那么容器和镜像是如何在主机(host)上储存的呢?
系统信息
- 系统: Ubuntu 16.04
- Docker: 17.10.0-ce
镜像
首先来看下什么是容器,引用 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
下的信息就是镜像在机器上的储存路径了。
将上面信息整理一下,得到下面的结构
- /var/lib/docker/overlay2/92820.../diff
- /var/lib/docker/overlay2/cc191.../diff
- /var/lib/docker/overlay2/5157f.../diff
- /var/lib/docker/overlay2/3d008.../diff
- /var/lib/docker/overlay2/53f44.../diff
- /var/lib/docker/overlay2/3b5e8.../diff
- /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 中。
根据上面的镜像储存的文件信息,可以看出,镜像是分层储存的。
容器
上面说了,容器就是一个镜像的实例化的表现,所以,容器也是分层的,当运行一个容器时,会在镜像的最上层加一个 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)