转载

docker images 介绍

这篇文章主要讲讲 docker 中镜像有关的知识,将涉及到下面几个方面:

  • docker images 命令的使用
  • docker 和 registry 交互的过程,pull 命令到底做了什么
  • docker storage driver
  • aufs 的格式和实际的组织结构
  • Dockerfile 原语和 docker 镜像之间的关系

简介

docker images 介绍

  • 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 pulldocker run 都是客户端的命令,最终这些命令会发送到 server 端(docker daemon 启动的时候会启动docker server)进行处理。下载镜像还会和 Registry 打交道,下面我们就说说使用 docker pull 的时候,docker 到底在做些什么!

docker images 介绍

docker client 组织配置和参数,把 pull 指令发送给 docker server,server 端接收到指令之后会交给对应的 handler。handler 会新开一个 CmdPull job 运行,这个 job 在 docker daemon 启动的时候被注册进来,所以控制权就到了 docker daemon 这边。docker daemon 是怎么根据传过来的 registry 地址、repo 名、image 名和tag 找到要下载的镜像呢?具体流程如下:

  1. 获取 repo 下面所有的镜像 id: GET /repositories/{repo}/images
  2. 获取 repo 下面所有 tag 的信息: GET /repositories/{repo}/tags
  3. 根据 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/<image_id>/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/<image_id>/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/<image_id>/ ,比如这里的命令主要会修改 /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 参数来强制删除。

另外一个要注意的是: 如果一个镜像有很多层,并且中间层没有被引用,那么在删除这个镜像的时候,所有没有被引用的镜像都会被删除。

管理镜像:docker registry 的工作原理

docker 1.10 的新变化

docker 镜像的 uuid 是怎么生成的?

在 1.10 之前,docker 镜像的 uuid 是随机生产的;在 1.10 引入了 Content addressable storage 的概念,uuid 是通过 SHA256 hash 算法生产的,主要好处有两点:可以作为镜像内容的验证,不同镜像可以共享镜像层。 需要注意的是:容器的 uuid 还是随机生成的,因为容器不存在共享的情况。

image 的存储

上面讲到的镜像存储方式在 1.10 版本之前是正确的,但是 docker 1.10 引入了新的方式。 所以 docker image id 和 aufs 的文件目录的名字不是对应的!

参考资料

  • allen 谈 docker 系列
  • docker official document on image and container
  • Docker and aufs in practice
  • Docker 1.10 Release Candidate Now Available
原文  http://cizixs.com/2016/04/06/docker-images
正文到此结束
Loading...