摘要

本文从个人的角度,讲述对于docker镜像和镜像构建的一些实践经验。主要内容包括利用docker hub进行在线编译,下载镜像,dind的实践,对于镜像的一些思考等。本文是对当时微信分享内容的一个整理。同时刊载在dockone社区

前言

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

镜像应该算是docker的核心价值之一。镜像由多层组成。那么对于一个层来说,就有了两个角度来看待。

一个角度是把这层当做一个独立的单位来看,那么这一个层其实主要是包含了文件和配置两个部分。

另一个角度则是把这一层和它的所有父层结合起来看,那么这个整体则是代表了一个完整的镜像。

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

现在已经有了docker hub/daocloud等多家公有容器服务供应商,为我们提供了非常便捷的镜像构建服务。我们不再需要在本地运行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过程中容器使用的网络。该讨论从今年的一月份开始讨论,至今仍未定论结贴。大家可以去强势围观。地址在这里

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

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

玩转docker镜像和镜像构建的更多相关文章

  1. 【Docker】(9)---每天5分钟玩转 Docker 容器技术之镜像

    镜像是 Docker 容器的基石,容器是镜像的运行实例,有了镜像才能启动容器.为什么我们要讨论镜像的内部结构? 如果只是使用镜像,当然不需要了解,直接通过 docker 命令下载和运行就可以了. 但如 ...

  2. 镜像的缓存特性 - 每天5分钟玩转 Docker 容器技术(14)

    上一节我们学习了镜像的分层结构,今天讨论镜像的缓存特性. Docker 会缓存已有镜像的镜像层,构建新镜像时,如果某镜像层已经存在,就直接使用,无需重新创建. 举例说明.在前面的 Dockerfile ...

  3. docker之NGINX镜像构建

    Nginx是一个高性能的Web和反向代理服务器,它具有很多非常优越的特性:1.作为Web服务器.2.作为负载均衡服务器.3.作为邮件代理服务器.4.安装及配置简单.接下来我们介绍在docker构建ng ...

  4. Docker教程:镜像构建和自动镜像构建dockerfile

    http://blog.csdn.net/pipisorry/article/details/50805379 Docker透过Dockerfile来记录建立Container映象文件的每一个步骤,可 ...

  5. Docker学习之4——构建NGINX镜像

    Nginx是一个高性能的Web和反向代理服务器,它具有很多非常优越的特性:1.作为Web服务器.2.作为负载均衡服务器.3.作为邮件代理服务器.4.安装及配置简单.接下来我们介绍在docker构建ng ...

  6. Docker:使用Jenkins构建Docker镜像

    Docker  彭东稳  1年前 (2016-12-27)  10709次浏览  已收录  0个评论 一.介绍Jenkins Jenkins是一个开源项目,提供了一种易于使用的持续集成系统,使开发者从 ...

  7. 在sun jdk 8镜像基础上构建maven 3的docker镜像

    2019独角兽企业重金招聘Python工程师标准>>> 在https://my.oschina.net/ytqvip/blog/1595054文章的sun jdk 8镜像基础上构建m ...

  8. docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像

    docker通过dockerfile构建JDK最小镜像,Docker导出导入镜像 一.docker通过dockerfile构建JDK最小镜像 1.1 下载JRE 1.2 解压JRE,删除相关不需要文件 ...

  9. Docker容器 关于镜像构建的安全问题

    写在前面 确保容器中服务与应用安全是容器化演进的关键点.容器安全涉及到应用开发与维护的整个生命周期,本文主要从镜像构建的视角来看docker容器的一些安全问题及应对措施. 一.权限管理 1.避免以容器 ...

随机推荐

  1. Windows7下搭建Django运行环境

    一直都是在Linux环境下搭建django的运行环境,开学因为需要叫前端的同学帮忙修改模板,所以需要在Windows下搭建起运行环境,想来PHP倒是有不少集成开发环境,Python倒是少的可怜…只在w ...

  2. avalonjs 1.3.7发布

    avalonjs 1.3.7发布 又到每个月的15号了,现在avalon已经固定在每个月的15号发布新版本.这次发布又带来许多新特性,让大家写码更加轻松,借助于“操作数据即操作DOM”的核心理念与双向 ...

  3. jQueryRotate 转盘抽奖代码实现

    代码如下: 例子兼容IE6,7,8 以及高版本浏览器,如有bug请回复! 1.html结构 <!doctype html> <html lang="en"> ...

  4. Python 3语法小记(九) 异常 Exception

    常见异常: Exception                        所有异常的基类 AttributeError                 特性应用或赋值失败时引发 IOError  ...

  5. OWIN产生的背景以及简单介绍

    OWIN产生的背景以及简单介绍 随着VS2013的发布,微软在Asp.Net中引入了很多新的特性,比如使用新的权限验证模块Identity, 使用Async来提高Web服务器的吞吐量和效率等.其中一个 ...

  6. .NET和JAVA的比较- 体系结构

    .NET体系结构   对于.NET Framework体系结构,参考了"你必须知道的.NET"并”借用“别人的经典体系结构图从宏观上说明一下我的理解. 图1 简单的说下几个名词: ...

  7. 经典的SQL语句面试题

    Student(S#,Sname,Sage,Ssex) 学生表 Course(C#,Cname,T#) 课程表 SC(S#,C#,score) 成绩表 Teacher(T#,Tname) 教师表 问题 ...

  8. Memcached在.Net中的基本操作

    Memcached在.Net中的基本操作 一.Memcached ClientLib For .Net 首先,不得不说,许多语言都实现了连接Memcached的客户端,其中以Perl.PHP为主. 仅 ...

  9. JSP技术模型(五)JSP隐含变量

    在JSP页面的转换阶段,容器在_jspService()方法中申明并初始化一些变量,可以在JSP页面小脚本中或表达式中直接使用这些变量. 一.JSP页面中可使用的隐含变量 1.applicationj ...

  10. PLAN :昔日未来

    <昔日未来> 1. C语言: 必须要看的书:<C程序设计语言><C标准库> 像<C和指针>,<C专家编程>,<C陷阱>这种书虽很 ...