本次分享主要是从我的实践的角度,讲述本人对于Docker镜像的一些玩法和体会。本文中大部分的内容都还处于实验的阶段,未通过大规模生产的实践。特此说明。思虑不全或者偏颇之处,还请你们指正。git
镜像应该算是Docker的核心价值之一。镜像由多层组成。那么对于一个层来讲,就有了两个角度来看待。一个角度是把这层当作一个独立的单位来看,那么这一个层其实主要是包含了文件和配置两个部分。另外一个角度则是把这一层和它的全部父层结合起来看,那么这个总体则是表明了一个完整的镜像。github
本文所述的Docker镜像,主要是指的从Dockerfile构建出来的镜像。docker
如今已经有了Docker Hub等多家公有容器服务供应商,为咱们提供了很是便捷的镜像构建服务。咱们再也不须要在本地运行docker build而是能够借用他们的服务实现方便的镜像构建。下文中以Docker Hub为例,介绍一些很是规的用法。各位在实践中可使用国内的多家容器服务提供商,如DaoCloud等。json
众所周知,Docker镜像能够用来描述一个APP的runtime。好比咱们构建一个Tomcat的镜像,镜像里包含了运行Tomcat的环境以及依赖。可是咱们再细看,其实Docker镜像不只仅是一个runtime,而是提供了一个环境,一个软件栈。从这个角度上来讲,镜像不只仅能够用来提供APP进行运行,还能够提供诸如编译的环境。ubuntu
用Docker来进行编译,这个应该来讲不是什么新奇玩法。由于Docker源码的编译就是经过这种方式来得到的。Docker有对应的Dockerfile。能够利用这个来完成代码的编译。小程序
这里我举个例子。这里有一个写的Dockerfile。test.c是一个输出hello world的c语言源文件。centos
FROM centos:centos6 RUN yum install -y gcc ADD test.c / RUN gcc /test.c
构建这个镜像,因为最后一步是编译命令gcc/test.c,因此编译过程会在Docker Hub上进行执行。bash
咱们能够经过编写Dockerfile,使得整个编译过程都托管在Docker Hub上。若是咱们提交了新的代码,须要从新编译,那么只须要从新构建镜像便可。服务器
在v1版本中,Docker Client是串行下载镜像的各层。对于docker pull的过程进行分析,能够看到Docker Client总共有这样几个步骤:网络
/v1/repositories/{repository}/tags/{tag} 获取tag的id
/v1/images/{tag_id}/ancestry 获取tag的各层的id
/v1/images/{layer_id}/json 依次获取各层对应的配置文件json
/v1/images/{layer_id}/layer 依次获取各层对应的镜像数据layer
Docker Hub的镜像数据,并非在本身的服务器中存储,而是使用的亚马逊的s3服务。所以在调用/v1/images/{layer_id}/layer接口,拉取镜像的layer数据时,会返回302,将请求重定向到亚马逊的s3服务上进行下载。
为了方便下载,我本身写了个小程序,使用HTTP协议便可彻底模拟Docker Client的整个过程。本身写的好处在于你能够依次获取tag的ID,各层的ID,以及全部层的配置,进而一次性将全部层对应的镜像数据存储在亚马逊的s3地址获取到,而后能够进行并行下载。若是单层下载失败,只须要从新下载这一层便可。当全部的层在本地下载完毕后。而后打成tar包,再使用Docker Client进行load便可。
对于上文中所说的在线编译,那么咱们其实只关心编译出来的相关文件。如刚刚的举例,咱们其实只须要获取镜像的最后一层就能够了。那么使用本身写的工具,能够仅仅把最后一层下载下来。下载下来的tar包进行解包,就能够直接获取出编译结果,即编译过程生成的相关文件了。Docker Hub就成为了咱们的一个强大的在线编译器。
注:这里说的镜像下载过程是针对的Registry v1版本。Docker Hub在不久以后即将全面结束v1的服务。目前国内的几家容器服务提供商还能够支持v1。该方法一样有效。v2的协议和代码我还没学习,后面研究以后再同你们分享。
镜像层合并这个话题一直是一个有争议的话题。过长的Dockefile会致使一个冗长的镜像层数。而由于镜像层数过多(好比十几层,几十层),可能会带来的性能和稳定性上的担心也不无道理,可是彷佛Docker社区一直不认为这是一个重要的问题。因此基本上对于镜像层合并的PR最后都被拒了。可是这不影响咱们在这里讨论他的实现。
我为Dockerfile增长了两个指令。TAG和COMPRESS。
TAG功能相似于docker build -t
的参数。不过build -t
只能给Dockerfile中的最后一层镜像打上tag。新增长的TAG指令能够在build生成的中间层也用标签记录下来。好比:
FROM centos:centos6 RUN yum install -y sshd TAG sshd:latest ADD test / CMD /bin/bash
这个TAG功能至关于使用下面的Dockerfile生成了这样的一个镜像,并打上了sshd:latest的标签。
FROM centos:centos6
RUN yum install -y sshd
COMPRESS功能实现了一个镜像多层合并的功能。好比下面这个Dockerfile:
FROM centos:centos6 RUN yum install -y sshd ADD test / CMD /bin/bash COMPRESS centos:centos6
咱们知道这里假设RUN yum install -y sshd
,ADD test /, CMD /bin/bash生成的镜像层为a、b、c。那么COMPRESS的功能目标就是将新增的a、b、c的文件和配置合并为一个新的层d,并设置层d的父亲为镜像centos:centos6。层d的配置文件能够直接使用层c的配置文件。合并的难点在于如何计算层d的文件。
这里有两种作法,一种是把层a、b、c中的文件按照合并的规则合并起来。合并的规则包括子层和父层共有的文件则使用子层的,没有交叉的文件则所有作为新添加的。这种方法效率较低,在须要合并的层数过多的时候,会极为耗时。
另一种思路则较为简单,不须要考虑中间总共有多少层。直接比较centos:centos6镜像和c镜像(c镜像是指由c和其全部父层组成的镜像),将二者的全部文件作比较,二者的diff结果即为新层d。
最终,我采用了后者做为COMPRESS的实现。镜像的合并缩减了层数,可是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,能够经过docker history进行回溯)。
dind(Docker in Docker),顾名思义就是在容器里面启动一个Docker Daemon。而后使用后者再启动容器。dind是一种比较高级的玩法,从另外一个角度来讲也是一种有必定风险的玩法。dind巧妙的利用了Docker的嵌套的能力,可是使人颇为担忧的是底层graph driver在嵌套后的性能和稳定性。因此dind我并不推荐做为容器的运行环境来使用(RancherOS实际上是使用了这种方式的),可是使用其做为构建镜像的环境,能够进行实践。毕竟构建失败的后果没有运行时崩溃的后果那么严重。
之因此会用到dind,是由于若是用于镜像构建,那么直接使用多个物理机,未免比较浪费。由于构建并非随时都会发生的。而使用dind的方式,只需在须要的时候申请多个容器,而后再在其上进行构建操做。在不须要时候就能够及时释放容器资源,更加灵活。
制做dind的镜像须要一个CentOS的镜像(其余暂未实践过,fedora/ubuntu也均可以作),和一个wrapdocker的文件。wrapdocker的主要做用是容器启动后为Docker Daemon运行时准备所需的环境。
由于容器启动后,Docker还须要一些环境才能启动daemon。好比在CentOS下,须要wrapdocker把cgroup等准备好。使用CentOS的镜像建立一个容器后,安装Docker等Docker须要的组件后,而后把wrapdocker ADD进去。并把wrapdocker添加为ENTRYPOINT或者CMD。而后将容器commit成为镜像,就得到了一个dind的镜像。使用dind的镜像时须要使用privileged赋予权限,就可使用了。
熟悉Docker源码的同窗应该知道,dind其实并不陌生。在Docker项目里,就有这样一个dind的文件。这个dind文件其实就是一个wrapdocker文件。在Docker进行集成测试时,须要使用该文件,协助准备环境以便在容器内部启动一个Daemon来完成集成测试。
若是对于dind有兴趣,能够参考jpetazzo中的Dockerfile和wrapdocker,构建本身的dind镜像。
dind中Docker的使用跟普通Docker同样。再也不赘述。
Docker镜像由若干层组成。而其中的每一层是由文件和配置组成的。若是把层与层之间的父子关系,看作一种时间上的前后关系,那么Docker镜像其实与Git十分的相像。那么从理论上来讲,Git的若干功能,好比merge、reset、rebase功能其实咱们均可以在Docker的构建过程当中予以实现。好比上文中的COMPRESS功能,就相似于Git的merge。理论上,Docker镜像其实也能够拥有Git般强大的功能。从这点上来讲,Docker镜像的灵活性就远高于KVM之类的镜像。
在这里,不得不抱怨几句。Docker的维护者们对于dockerfile或者说Docker的构建过程并无给予很是积极的态度,予以改善。固然这也多是因为他们的更多的关注点集中在了runC、libnetwork、Orchestration上。因此没有更多的人力来完善Docker构建的工具,而是寄但愿于社区能本身增长其余的tool来丰富Docker的构建过程。
因此不少时候,docker build的功能并不尽如人意。好比一直呼声很高的Docker镜像压缩功能,几经讨论,终于无果而终。又好比在build过程当中,使用–net参数来使得能够控制build过程当中容器使用的网络。该讨论从今年的一月份开始讨论,至今仍未定论结贴。你们能够去强势围观。地址在这里(https://github.com/docker/docker/issues/10324)。
这里特别说一下,在CentOS 6下,dind不能使用网桥(centos7能够支持),因此在CentOS 6下使用dind,进行docker build,须要指定网络–net=host的方式。
因此不少功能并不能等待Docker本身去完善,只好本身动手开发。其实熟悉了Docker源码后,关于docker build这方面的开发难度并非很大。能够本身去实现。读一下孙宏亮同窗的《Docker源码分析》,会很快上手。