简介
一图看尽 docker 镜像
docker 镜像代表了容器的文件系统里的内容,是容器的基础,镜像一般是通过 Dockerfile 生成的
docker 的镜像是分层的,所有的镜像(除了基础镜像)都是在之前镜像的基础上加上自己这层的内容生成的
每一层镜像的元数据都是存在 json 文件中的,除了静态的文件系统之外,还会包含动态的数据
使用镜像:docker image 命令
docker client 提供了各种命令和 daemon 交互,来完成各种任务,其中和镜像有关的命令有:
docker images :列出 docker host 机器上的镜像,可以使用 -f 进行过滤
docker build:从 Dockerfile 中构建出一个镜像
docker history:列出某个镜像的历史
docker import:从 tarball 中创建一个新的文件系统镜像
docker pull:从 docker registry 拉去镜像
docker push:把本地镜像推送到 registry
docker rmi: 删除镜像
docker save:把镜像保存为 tar 文件
docker search:在 docker hub 上搜索镜像
docker tag:为镜像打上 tag 标记
从上面这么多命令中,我们就可以看出来,docker 镜像在整个体系中的重要性。
下载镜像:pull 和 push 镜像到底在做什么?
如果了解 docker 结构的话,你会知道 docker 是典型的 C/S 架构。平时经常使用的 docker pull, docker run 都是客户端的命令,最终这些命令会发送到 server 端(docker daemon 启动的时候会启动docker server)进行处理。下载镜像还会和 Registry 打交道,下面我们就说说使用 docker pull 的时候,docker 到底在做些什么!
docker client 组织配置和参数,把 pull 指令发送给 docker server,server 端接收到指令之后会交给对应的 handler。handler 会新开一个 CmdPull job 运行,这个 job 在 docker daemon 启动的时候被注册进来,所以控制权就到了 docker daemon 这边。docker daemon 是怎么根据传过来的 registry 地址、repo 名、image 名和tag 找到要下载的镜像呢?具体流程如下:
获取 repo 下面所有的镜像 id:GET /repositories/{repo}/images
获取 repo 下面所有 tag 的信息: GET /repositories/{repo}/tags
根据 tag 找到对应的镜像 uuid,并下载该镜像
获取该镜像的 history 信息,并依次下载这些镜像层: GET /images/{image_id}/ancestry
如果这些镜像层已经存在,就 skip,不存在的话就继续
获取镜像层的 json 信息:GET /images/{image_id}/json
下载镜像内容: GET /images/{image_id}/layer
下载完成后,把下载的内容存放到本地的 UnionFS 系统
在 TagStore 添加刚下载的镜像信息
存储镜像:docker storage 介绍
在上一个章节提到下载的镜像会保存起来,这一节就讲讲到底是怎么存的。
UnionFS 和 aufs
如果对 docker 有所了解的话,会听说过 UnionFS 的概念,这是 docker 实现层级镜像的基础。在 wikipedia 是这么解释的:
Unionfs is a filesystem service for Linux, FreeBSD and NetBSD which
implements a union mount for other file systems. It allows files and
directories of separate file systems, known as branches, to be
transparently overlaid, forming a single coherent file system.
Contents of directories which have the same path within the merged
branches will be seen together in a single merged directory, within
the new, virtual filesystem.
简单来说,就是用多个文件夹和文件(这些是系统文件系统的概念)存放内容,对上(应用层)提供虚拟的文件访问。
比如 docker 中有镜像的概念,应用层看来只是一个文件,可以读取、删除,在底层却是通过 UnionFS 系统管理各个镜像层的内容和关系。
docker 负责镜像的模块是 Graph,对上提供一致和方便的接口,在底层通过调用不同的 driver 来实现。常用的 driver 包括 aufs、devicemapper,这样的好处是:用户可以选择甚至实现自己的 driver。
aufs 镜像在机器上的存储结构
NOTE:
只下载了 ubuntu:14.04 镜像
docker version:1.6.3
image driver:aufs
使用 docker history 查看镜像历史:
root@cizixs-ThinkPad-T450:~# docker images
REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
172.16.1.41:5000/ubuntu 14.04 2d24f826cb16 13 months ago 188.3 MB
root@cizixs-ThinkPad-T450:~# docker history 2d24
IMAGE CREATED CREATED BY SIZE
2d24f826cb16 13 months ago /bin/sh -c #(nop) CMD [/bin/bash] 0 B
117ee323aaa9 13 months ago /bin/sh -c sed -i ‘s/^#\s(deb.universe)$/ 1.895 kB
1c8294cc5160 13 months ago /bin/sh -c echo ‘#!/bin/sh’ > /usr/sbin/polic 194.5 kB
fa4fd76b09ce 13 months ago /bin/sh -c #(nop) ADD file:0018ff77d038472f52 188.1 MB
511136ea3c5a 2.811686 years ago 0 B
可以看到,ubuntu:14.04 一共有五层镜像。aufs 数据存放在 /var/lib/docker/aufs 目录下:
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# tree -L 1
.
├── diff
├── layers
└── mnt
一共有三个文件夹,每个文件夹下面都是以镜像 id 命令的文件夹,保存了每个镜像的信息。先来介绍一下这三个文件夹
layers:显示了每个镜像有哪些层构成
diff:每个镜像的和之前镜像的区别,就是这一层的内容
mnt:UnionFS 对外提供的 mount point,因为 UnionFS 底层是多个文件夹和文件,对上层要提供统一的文件服务,是通过 mount 的形式实现的。每个运行的容器都会在这个目录下有一个文件夹
比如 diff 文件夹是这样的:
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7/
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c/
etc
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/1c8294cc516082dfbb731f062806b76b82679ce38864dd87635f08869c993e45/
etc sbin usr var
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/fa4fd76b09ce9b87bfdc96515f9a5dd5121c01cc996cf5379050d8e13d4a864b/
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@cizixs-ThinkPad-T450:/var/lib/docker/aufs# ls diff/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/
除了这些实际的数据之外,docker 还为每个镜像层保存了 json 格式的元数据,存储在 /var/lib/docker/graph//json,比如:
root@cizixs-ThinkPad-T450:/var/lib/docker# cat graph/2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7/json | jq ‘.’
{
“id”: “2d24f826cb16146e2016ff349a8a33ed5830f3b938d45c0f82943f4ab8c097e7”,
“parent”: “117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c”,
“created”: “2015-02-21T02:11:06.735146646Z”,
“container”: “c9a3eda5951d28aa8dbe5933be94c523790721e4f80886d0a8e7a710132a38ec”,
“container_config”: {
“Hostname”: “43bd710ec89a”,
“Domainname”: “”,
“User”: “”,
“Memory”: 0,
“MemorySwap”: 0,
“CpuShares”: 0,
“Cpuset”: “”,
“AttachStdin”: false,
“AttachStdout”: false,
“AttachStderr”: false,
“PortSpecs”: null,
“ExposedPorts”: null,
“Tty”: false,
“OpenStdin”: false,
“StdinOnce”: false,
“Env”: [
“PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”
],
“Cmd”: [
“/bin/sh”,
“-c”,
“#(nop) CMD [/bin/bash]”
],
“Image”: “117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c”,
“Volumes”: null,
“WorkingDir”: “”,
“Entrypoint”: null,
“NetworkDisabled”: false,
“MacAddress”: “”,
“OnBuild”: [],
“Labels”: null
},
“docker_version”: “1.4.1”,
“config”: {
“Hostname”: “43bd710ec89a”,
“Domainname”: “”,
“User”: “”,
“Memory”: 0,
“MemorySwap”: 0,
“CpuShares”: 0,
“Cpuset”: “”,
“AttachStdin”: false,
“AttachStdout”: false,
“AttachStderr”: false,
“PortSpecs”: null,
“ExposedPorts”: null,
“Tty”: false,
“OpenStdin”: false,
“StdinOnce”: false,
“Env”: [
“PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin”
],
“Cmd”: [
“/bin/bash”
],
“Image”: “117ee323aaa9d1b136ea55e4421f4ce413dfc6c0cc6b2186dea6c88d93e1ad7c”,
“Volumes”: null,
“WorkingDir”: “”,
“Entrypoint”: null,
“NetworkDisabled”: false,
“MacAddress”: “”,
“OnBuild”: [],
“Labels”: null
},
“architecture”: “amd64”,
“os”: “linux”,
“Size”: 0
}
除了 json 之外,还有一个文件 /var/lib/docker/graph//layersize 保存了镜像层的大小。
创建镜像:镜像的 cache 机制
在使用 docker build 创建新的镜像的时候,docker 会使用到 cache 机制,来提高执行的效率。为了理解这个问题,我们先看一下 build 命令都做了哪些东西吧。
我们来看一个简单的 Dockerfile:
FROM ubuntu:14.04
RUN apt-get update
ADD run.sh /
VOLUME /data
CMD [”./run.sh”]
这个文件虽然简单,却包含了很多命令:RUN、ADD、VOLUME、CMD 涉及到很多概念。
一般情况下,对于每条命令,docker 都会生成一层镜像。 cache 的作用也很容易猜测,如果在构建某个镜像层的时候,发现这个镜像层已经存在了,就直接使用,而不是重新构建。这里最重要的问题在于:怎么知道要构建的镜像层已经存在了? 下面就重点解释这个问题。
docker daemon 读到 FROM 命令的时候,会在本地查找对应的镜像,如果没有找到,会从 registry 去取,当然也会取到包含 metadata 的 json 文件。然后到了 RUN 命令,如果没有 cache 的话,这个命令会做什么呢?
我们已经知道,每层镜像都是由文件系统内容和 metadata 构成的。
文件系统的内容,就是执行 apt-get update 命令导致的文件变动,会保存到 /var/lib/docker/aufs/diff//,比如这里的命令主要会修改 /var/lib 和 /var/cache 下面和 apt 有关的内容:
root@cizixs-ThinkPad-T450:/var/lib/docker# tree -L 2 aufs/diff/e7ae26691ff649c55296adf7c0e51b746e22abefa6b30310b94bbb9cfa6fce63/
aufs/diff/e7ae26691ff649c55296adf7c0e51b746e22abefa6b30310b94bbb9cfa6fce63/
├── tmp
└── var
├── cache
└── lib
我们来看一下 json 文件的内容,最重要的改变就是 container_config.Cmd 变成了:
“Cmd”: [
“/bin/sh”,
“-c”,
“apt-get update”
],
也就是说,如果下次再构建镜像的时候,我们发现新的镜像层 parent 还是 ubuntu:14.04,并且 json 文件中 cmd 要更改的内容也一致,那么就认为这两层镜像是相同的,不需要重新构建。好了,那么构建的时候,daemon 一定会遍历本地所有镜像,如果发现镜像一致就使用已经构建好的镜像。
ADD 和 COPY 文件
如果 Dockerfile 中有 ADD 或者 COPY 命令,那么怎么判断镜像是否相同呢?第一个想法肯定是文件名,但即使文件名不变,那么文件也是可以变的;那就再加上文件大小,不过两个同名并且大小相同的文件也不一定内容完全一样啊!最保险的办法就是用 hash 了,嗯!docker 就是这个干的,我们来看一下 ADD 这层镜像的 json 文件变化:
“Cmd”: [
“/bin/sh”,
“-c”,
“#(nop) ADD file:9fb96e5dd9ce3e03665523c164bbe775d64cc5d8cc8623fbcf5a01a63e9223ab in /”
],
看到没,ADD 的时候只有一串 hash 字符串,hash 算法的实现,如果感兴趣可以自己研究一下。
喂!这样真的就万无一失了吗?
看完上面的内容,大多数同学会觉得 cache 机制真好, 很节省时间,也能节省空间。但是这里还有一个问题,有些命令是依赖外部的,比如 apt-get update 或者 curl http://some.url.com/,如果外部内容发生了改变,docker 就没有办法侦测到,去做相应的处理了。所以它提供了 –no-cache 参数来强制不要使用 cache 机制,所以说这部分内容是要用户自己维护的。
除此之外,还需要在编写 Dockerfile 的时候考虑到 cache,这一点在官方提供的 dockerfile best practice 也有提及。
运行镜像:docker 镜像和 docker 容器
我们都知道 docker 容器就是运行态的docker 镜像,但是有一个问题:docker 镜像里面保存的都是静态的东西,而容器里面的东西是动态的,那么这些动态的东西是如何管理的呢?比如说:
docker 容器里该运行那些进程?
怎么把 docker 镜像转换成docker 容器?
docker 容器里面 ip、hostname 这些东西使如何动态生成的?
这就是上面提到的 json 文件的功能,哪些信息会存放在 json 文件呢?答案就是:除了文件系统的内容外,其他都是,比如:
ENV FOO=BAR: 环境变量,
VOLUME /some/path:容器使用的 volume,乍看上去这是文件系统的一部分,其实这部分内容不是确定的,在构建镜像的时候数据卷可以是不存在的,会在容器运行的时候动态地添加。所以这部分内容不能放到镜像层文件中
EXPOSE 80:expose 命令记录了容器运行的时候要暴露给外部的端口,这也是运行时状态,不是文件系统的一部分
CMD [“./myscript.sh”]:CMD 命令记录了 docker 容器的执行入口,这不是文件系统的一部分
好了,既然我们已经知道这些东西是怎么存储的,那么实际运行容器的时候这些内容是怎么被加载到容器里的呢?答案就是 docker daemon,这个实际管理容器实现的家伙。
我们知道,在容器实际运行过程中,每个容器就是 docker daemon 的子进程:
root 3249 0.1 6.6 985212 33288 ? Ssl 04:53 0:19 /usr/bin/docker daemon –insecure-registry 172.16.1.41:5000 –exec-opt native.cgroupdriver=cgroupfs –bip=10.12.240.1/20 –mtu=1500 –ip-masq=false
root 3597 0.0 0.1 3816 632 ? Ssl 04:55 0:00 _ /pause
root 3633 0.0 0.1 3816 504 ? Ssl 04:55 0:00 _ /pause
root 3695 0.0 0.1 3816 516 ? Ssl 04:55 0:00 _ /pause
root 3710 0.0 0.1 3816 528 ? Ssl 04:55 0:00 _ /pause
root 3745 0.0 0.1 3816 504 ? Ssl 04:55 0:00 _ /pause
polkitd 3793 0.0 0.2 36524 1280 ? Ssl 04:55 0:07 _ redis-server *:6379
root 3847 0.0 0.0 4184 184 ? Ss 04:55 0:00 _ /bin/sh -c /run.sh
root 3872 0.0 0.0 17668 360 ? S 04:55 0:00 | _ /bin/bash /run.sh
root 3873 0.0 0.3 42824 1752 ? Sl 04:55 0:01 | _ redis-server *:6379
root 3865 0.0 1.5 166256 8024 ? Ss 04:55 0:00 _ apache2 -DFOREGROUND
33 3881 0.0 1.0 166280 5140 ? S 04:55 0:00 | _ apache2 -DFOREGROUND
33 3882 0.0 1.0 166280 5140 ? S 04:55 0:00 | _ apache2 -DFOREGROUND
33 3883 0.0 1.0 166280 5140 ? S 04:55 0:00 | _ apache2 -DFOREGROUND
33 3884 0.0 1.0 166280 5140 ? S 04:55 0:00 | _ apache2 -DFOREGROUND
33 3885 0.0 1.0 166280 5140 ? S 04:55 0:00 | _ apache2 -DFOREGROUND
root 3939 0.0 0.7 90264 4016 ? Ss 04:55 0:00 _ nginx: master process nginx
33 3947 0.0 0.3 90632 1660 ? S 04:55 0:00 _ nginx: worker process
33 3948 0.0 0.3 90632 1660 ? S 04:55 0:00 _ nginx: worker process
33 3949 0.0 0.3 90632 1660 ? S 04:55 0:00 _ nginx: worker process
33 3950 0.0 0.3 90632 1660 ? S 04:55 0:00 _ nginx: worker process
也是说,docker daemon 会读取镜像的信息,作为容器的 rootfs,然后读取 json 文件中的动态信息作为运行时状态。
删除镜像:清理镜像之道
镜像是按照 UnionFS 的格式存放在本地的,删除也很容易理解,就是把对应镜像层的本地文件(夹)删除。docker 也提供了 docker rmi 这个命令来处理。
不过需要注意一点:镜像也是有“引用”这个概念的,只有当该镜像层没有被引用的时候,才能删除。“引用”就是被打上 tag,同一个 uuid 的镜像是可以被打上不同的 tag 的。我们来看一个官方提供的例子:
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test1 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
test2 latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
$ docker rmi fd484f19954f
Error: Conflict, cannot delete image fd484f19954f because it is tagged in multiple repositories, use -f to force
2013/12/11 05:47:16 Error: failed to remove one or more images
$ docker rmi test1
Untagged: test1:latest
$ docker rmi test2
Untagged: test2:latest
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest fd484f19954f 23 seconds ago 7 B (virtual 4.964 MB)
$ docker rmi test
Untagged: test:latest
Deleted: fd484f19954f4920da7ff372b5067f5b7ddb2fd3830cecd17b96ea9e286ba5b8
删除有 tag 的镜像时,会先有 untag 的操作。如果删除的镜像还有其他 tag,必须先把所有的 tag 删除后才能继续,当然你也可以使用 -f 参数来强制删除。
另外一个要注意的是:如果一个镜像有很多层,并且中间层没有被引用,那么在删除这个镜像的时候,所有没有被引用的镜像都会被删除。
https://segmentfault.com/a/1190000021809269