DockOne技术分享(三十七):玩转Docker镜像和镜像构建


【编者的话】本次分享从个人的角度,讲述对于Docker镜像和镜像构建的一些实践经验。主要内容包括利用Docker Hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。

@Container容器技术大会将于2016年1月24日在北京举行,来自爱奇艺、微博、腾讯、去哪儿网、美团云、京东、蘑菇街、惠普、暴走漫画等知名公司的技术负责人将分享他们的容器应用案例。

前言

本次分享主要是从个人实践的角度,讲述本人对于Docker镜像的一些玩法和体会。本文中大部分的内容都还处于实验的阶段,未经过大规模生产的实践。特此说明。思虑不全或者偏颇之处,还请大家指正。

镜像应该算是Docker的核心价值之一。镜像由多层组成。那么对于一个层来说,就有了两个角度来看待。一个角度是把这层当做一个独立的单位来看,那么这一个层其实主要是包含了文件和配置两个部分。另一个角度则是把这一层和它的所有父层结合起来看,那么这个整体则是代表了一个完整的镜像。

本文所述的Docker镜像,主要是指的从Dockerfile构建出来的镜像。

现在已经有了Docker Hub等多家公有容器服务供应商,为我们提供了非常便捷的镜像构建服务。我们不再需要在本地运行docker build而是可以借用他们的服务实现方便的镜像构建。下文中以Docker Hub为例,介绍一些非常规的用法。各位在实践中可以使用国内的多家容器服务提供商,如DaoCloud等。

Docker Hub之在线编译

众所周知,Docker镜像可以用来描述一个APP的runtime。比如我们构建一个Tomcat的镜像,镜像里包含了运行Tomcat的环境以及依赖。但是我们再细看,其实Docker镜像不仅仅是一个runtime,而是提供了一个环境,一个软件栈。从这个角度上来说,镜像不仅仅可以用来提供APP进行运行,还可以提供诸如编译的环境。

用Docker来进行编译,这个应该来说不是什么新奇玩法。因为Docker源码的编译就是通过这种方式来获得的。Docker有对应的Dockerfile。可以利用这个来完成代码的编译。

这里我举个例子。这里有一个写的Dockerfile。test.c是一个输出hello world的c语言源文件。
FROM centos:centos6
RUN yum install -y gcc
ADD test.c /
RUN gcc /test.c

构建这个镜像,由于最后一步是编译命令gcc/test.c,所以编译过程会在Docker Hub上进行执行。

我们可以通过编写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

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过程中容器使用的网络。该讨论从今年的一月份开始讨论,至今仍未定论结贴。大家可以去强势围观。地址在这里

这里特别说一下,在CentOS 6下,dind不能使用网桥(centos7可以支持),所以在CentOS 6下使用dind,进行docker build,需要指定网络--net=host的方式。

所以很多功能并不能等待Docker自己去完善,只好自己动手开发。其实熟悉了Docker源码后,关于docker build这方面的开发难度并不是很大。可以自己去实现。读一下孙宏亮同学的《Docker源码分析》,会很快上手。

Q&A

Q:京东私有云是基于OpenStack+Docker吗,网络和存储的解决方案是什么?

A:是的。私有云网络使用的是VLAN。并没有使用租户隔离,主要保证效率。存储使用的是京东自己的存储。
Q:那个镜像压缩,有什么好处?

A: 镜像压缩或者说合并,主要是减少层数,减少担忧。其实目前看,好处并不明显。因为层数过多带来的更多的是担忧,但没有确凿证据表明会影响稳定。
Q:在线编译应用广泛吗?我们一般可能更关注最后的结果。有很多代码都是先在本地编译,成功后,再发布到镜像中的。

A:这个玩法应该说并不广泛。主要是我自己玩的时候,不想自己去拉镜像的全部层,只关注编译结果。所以这样玩
Q:对于Docker镜像的存储京东是使用什么方式实现的分布式文件系统京东Docker上有使用吗能否介绍下?

A:镜像存储使用的是官方的registry。v1版本。registry后端是京东自研的JFS存储。
Q:你之前提到了“镜像的合并缩减了层数,但是弊端在于将生成镜像的Dockerfile信息也消除了(使用Dockerfile生成的镜像,可以通过docker history进行回溯)。”那如果使用了Compress之后,应该如何进行回溯?还是说需要舍弃这部分功能?

A:是的,确实没办法回溯了,所以要舍弃了。不过反过来想,其实如果Dockerfile的ADD和COPY之类的功能,就算能回溯,其实意义也不大。所以我认为保存Dockerfile更有意义。
Q:为什么不采用将要执行的命令做成脚本,直接add进去执行这种,也能减少层数?

A:这种方法也是可行的。只是Dockerfile更显式一些。同理,其实只要你做好镜像,直接export出去,就可以得到所有文件了。再配上配置文件。这样整个就只有一层了。
Q:我平时在,测试的时候并没-有压缩过,也不知道,压缩会带来什么风险,但是,看你刚才说有可能会带来一定的风险。 你们遇到过么?

A:因为我们的镜像都做过合并层,所以层数并不多。不合并会带来什么风险,其实更多的是出于性能和稳定性上的担忧。这种担忧可能是多余的。但是我们宁愿选择谨慎一些。
Q:镜像的合并方面怎么样能方便的减小镜像的大小,我做的镜像有些都在1G以上?

A:减少镜像大小主要还是靠去除不必要的文件。合并只能减少冗余文件,如果每层的文件都不相同,合并并不会缩小镜像的大小。
Q:网络这个使用VLAN能说详细一些吗,是每个容器都有一个和宿主机同网段的真实的物理IP吗?

A:是的。每个容器都有一个真实的IP。跟宿主机网段不同。是单独的容器网络。这个可以参考neutron中的Vlan实现。
Q:还有,把镜像压缩我也觉,但是像你那样把父镜像整个合并成新镜像这点我觉得有点问题,毕竟大家玩容器时都是在基础镜像上添加东西,你把常用的镜像为了压缩生成一个一次性的镜像,以后再使用基础镜像做其他业务时那不还得重新下载基础镜像?

A:镜像合并其实主要还是为了获得一个基础镜像。然后大家在基础镜像上添加东西。基础镜像相对来说,不会轻易改变。
Q:在你们的实践中,大规模部署容器时,每个节点都会从Registry节点下载镜像,给网络带来的压力大吗?

A:我们做了一些优化。首先,大部分业务使用的镜像会提前推送到每个Docker节点上。即使节点没有,Registry后端接的是京东的JFS,通过优化,临时去下载的时候可以直接从JFS去拿镜像数据。所以网络压力并不大。
Q:镜像压缩或者合并之后,镜像的层次减少了,但每层镜像不是变大了吗,这对于发布不是会占用带宽降低效率吗?

A:这个问题跟上个差不多。合并主要是为基础镜像使用的。
Q:你们怎么看待OpenStack和Docker的关系?在京东未来会长期两个并存吗?现在两个架构的发展速度和研发力量对比如何?

A:OpenStack和Docker并不矛盾。私有云采用nova docker的结合更多的是迎合用户习于使用VM的习惯。Magnum也在快速发展中。所以我相信二者都有存在的价值和发展的必要。
Q:关于dockfile的优化,你们有没有什么好的建议或者经验?

A:似乎也没多少新的建议。参考DockOne的相关文章。Dockerfile之优化经验浅谈大家在写 dockerfile 时有啥最佳实践?希望得到大家的建议
Q:比如创建一个rabbitmq镜像,需要安装很多依赖包,最后编译,最后生成的镜像1.3G,像这种情况,在创建镜像的时候能否减少镜像的大小呢?

A:并没有什么好的办法来减少。可能需要一定的人工或者工具去分析不需要的文件,来减少镜像的大小。
Q:Docker是如何进行自动更新的,自己搭建的镜像仓库,如何更新新版本的镜像?

A:Docker我们固定了一个版本。如果没出大面积的严重问题,几乎不会更新。目前来看,运行稳定。所以也没有更新必要。新版本的Docker提供的如网络等,暂时我们还不会大面积跟进使用。自己的镜像仓库,如果要更新新版本镜像,push进去就可以了。
Q:一个困扰我比较久的问题,如果镜像间存在依赖关系,基础镜像发生改变后其他镜像你们是跟着更新的呢?

A:在内部私有云中,一般大家使用的都是一个做好的base镜像。这里面就有一个问题,一旦这个base镜像需要打补丁,影响面比较大。首先很多base的子镜像会受到影响。另一方面,就是要考虑已经在使用基于base或者base子镜像的节点。前者我的方案是直接在base镜像中的layer,把需要打补丁的文件加入进去,重新打包放回。对于后者,目前还没想到很好的方法解决。
Q:在运行容器的时候,1、应用里面的日志或者配置文件,使用本地映射是不是好点,我是考虑到方便查看日志或者修改配置;2、创建的数据库镜像,在运行容器的时候把数据文件是不是映射到本地更好些呢?

A:日志我们的确是使用的本地映射。而且有的业务方狂写日志不加约束。所以我们给本地映射做了个LVM,挂给容器。做了容量上的限制。配置的话,现在是有一个内部的部署系统会帮他们部署配置。数据库的话是一个道理,也是映射到本地。也有一部分接入了云硬盘。
Q:Docker中,每层镜像打标签那我觉的很奇怪,当pull一个镜像或生成一个容器时,它如何找到你所命名的镜像层?

A:并不是给每层都打标签,而是你根据你的需要来给某一层打标签。至于标签内容,需要自己来进行控制。
Q:关于Compress的实现有些疑问,是不是在实现的过程中,只考虑最后的镜像和前一层的diff,还是说要逐层做diff?

A:是只考虑最后的镜像和你要合并到的父层镜像做diff。这样只要做一次diff,就可以获得中间的所有文件变化了。
Q:wrapdocker文件的工作原理是什么?

A:这个工作原理主要是准备一些Docker启动必要的环境。比如在CentOS下,需要wrapdocker把cgroups等准备好等。你可以参考下wrapdocker里面的代码。
Q:容器运行在物理机上,与OpenStack平台虚拟机是同一套管理系统?如何与容器的集群系统整合?

A:是同一套系统,都是用nova。虚拟机KVM和容器主要是镜像类型不同。在nova调度的时候,会根据镜像类型调度到KVM或者Docker节点进行创建。
Q:在一台物理机上运行Docker的数量是否有限定 还是看运行的应用来决定?

A:没有特别做限定。主要还是业务方去申请的。业务方习惯用大内存,多CPU的。那这个物理机上创建的容器数就少些。大致这样。
Q:想了解一下,你们对镜像的tag是怎么管理的?根据什么来打的?对于旧的镜像你们是丢弃还是像Git保存代码一样一直保留在仓库呢?

A:tag由各个用户来定。不同的用户在不同的Repository里。镜像tag自己管理。不过我们更希望他们能够更加规范一些,比如用git的版本号来打tag。
旧的镜像如果失去了tag(新的镜像抢夺了该tag),则旧镜像会被删除。不过不是立即,也是定期清理,主要减少存储量。因为毕竟不需要存储那么多的版本。
===========================
以上内容根据2015年12月8日晚微信群分享内容整理。分享人:徐新坤,京东商城云平台南京研发中心JDOS团队研发工程师,从2014年初开始从事Docker的研发,主要负责Docker在京东落地的相关开发和维护工作。 DockOne每周都会组织定向的技术分享,欢迎感兴趣的同学加微信:liyingjiesx,进群参与,您有想听的话题可以给我们留言。

4 个评论

引用: ”为了方便下载,我自己写了个小程序“

希望 @xiaolunsanguo 能够分享这个小程序,先谢谢了
代码我有时间整理下,到时候分享给大家
我想我已经找到我想要的信息了。

$ curl https://cdn-registry-1.docker.io/v1/images/511136ea3c5a64f264b78b5433614aec563103b4d4702f3ba7d4d2698e22c158/layer -H 'Authorization: Token signature=eb36430ddfc1a2cdc99f3b5d01347181a3e0cbc7,repository="library/ubuntu",access=read'
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<title>Redirecting...</title>
<h1>Redirecting...</h1>
<p>You should be redirected automatically to target URL: <a href="https://dseasb33srnrn.cloudfront.net/registry-v2/docker/registry/v2/blobs/sha256/a3/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4/data?Expires=1449716813&amp;Signature=Jo1MXGFrJdibLKc1IbUIf~KN6jQgEmLqEztlX9FY-y~WuN7612l5M7NSseQi7u69ggQNjdbsEp~ODDZOgOmVe2yCUk9mDA2y7kFdP~TwkvZRoJ9pn9sCXsjYuzpQdf6ZhOLqIA7rhY3cFPz4acQeNu2KnrIXZAbNLKVXBykigfs_&amp;Key-Pair-Id=APKAJECH5M7VWIS5YZ6Q">https://dseasb33srnrn.cloudfront.net/registry-v2/docker/registry/v2/blobs/sha256/a3/a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4/data?Expires=1449716813&amp;Signature=Jo1MXGFrJdibLKc1IbUIf~KN6jQgEmLqEztlX9FY-y~WuN7612l5M7NSseQi7u69ggQNjdbsEp~ODDZOgOmVe2yCUk9mDA2y7kFdP~TwkvZRoJ9pn9sCXsjYuzpQdf6ZhOLqIA7rhY3cFPz4acQeNu2KnrIXZAbNLKVXBykigfs_&amp;Key-Pair-Id=APKAJECH5M7VWIS5YZ6Q</a>. If not click the link.
学习了

要回复文章请先登录注册