这篇文章主要讲讲 docker 中镜像有关的知识,将涉及到下面几个方面:linux
做者:cizixs
时间:2016-04006
原文连接: https://cizixs.com/2016/04/06...
docker client 提供了各类命令和 daemon 交互,来完成各类任务,其中和镜像有关的命令有:nginx
docker images
:列出 docker host 机器上的镜像,可使用 -f
进行过滤docker build
:从 Dockerfile 中构建出一个镜像docker history
:列出某个镜像的历史docker import
:从 tarball 中建立一个新的文件系统镜像docker pull
:从 docker registry 拉去镜像docker push
:把本地镜像推送到 registrydocker rmi
: 删除镜像docker save
:把镜像保存为 tar 文件docker search
:在 docker hub 上搜索镜像docker tag
:为镜像打上 tag 标记从上面这么多命令中,咱们就能够看出来,docker 镜像在整个体系中的重要性。redis
若是了解 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 找到要下载的镜像呢?具体流程以下:docker
GET /repositories/{repo}/images
GET /repositories/{repo}/tags
根据 tag 找到对应的镜像 uuid,并下载该镜像apache
GET /images/{image_id}/ancestry
GET /images/{image_id}/json
GET /images/{image_id}/layer
在上一个章节提到下载的镜像会保存起来,这一节就讲讲究竟是怎么存的。json
若是对 docker 有所了解的话,会据说过 UnionFS 的概念,这是 docker 实现层级镜像的基础。在 wikipedia 是这么解释的:ubuntu
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 系统管理各个镜像层的内容和关系。bash
docker 负责镜像的模块是 Graph
,对上提供一致和方便的接口,在底层经过调用不一样的 driver 来实现。经常使用的 driver 包括 aufs、devicemapper,这样的好处是:用户能够选择甚至实现本身的 driver。架构
NOTE:
使用 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 命令的文件夹,保存了每一个镜像的信息。先来介绍一下这三个文件夹
好比 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
保存了镜像层的大小。
在使用 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 必定会遍历本地全部镜像,若是发现镜像一致就使用已经构建好的镜像。
若是 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 镜像里面保存的都是静态的东西,而容器里面的东西是动态的,那么这些动态的东西是如何管理的呢?好比说:
这就是上面提到的 json 文件的功能,哪些信息会存放在 json 文件呢?答案就是:除了文件系统的内容外,其余都是,好比:
好了,既然咱们已经知道这些东西是怎么存储的,那么实际运行容器的时候这些内容是怎么被加载到容器里的呢?答案就是 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
参数来强制删除。
另一个要注意的是:若是一个镜像有不少层,而且中间层没有被引用,那么在删除这个镜像的时候,全部没有被引用的镜像都会被删除。