Docker 镜像简介

作者: cizixs | 来源:发表于2016-05-13 14:59 被阅读0次

    原文发布在:http://cizixs.com/2016/04/06/docker-images,转载请注明出处。

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

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

    简介

    一图看尽 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 pulldocker 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 找到要下载的镜像呢?具体流程如下:

    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 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 的文件目录的名字不是对应的!

    参考资料

    相关文章

      网友评论

        本文标题:Docker 镜像简介

        本文链接:https://www.haomeiwen.com/subject/mlgftttx.html