Docker源码分析(九):Docker镜像

1.前言

回首过去的2014年,大家可以看到Docker在全球刮起了一阵又一阵的“容器风”,工业界对Docker的探索与实践更是一波高过一波。在如今的2015年以及未来,Docker似乎并不会像其他昙花一现的技术一样,在历史的舞台上热潮褪去,反而在工业界实践与评估之后,显现了前所未有的发展潜力。

究其本质,“Docker提供容器服务”这句话,相信很少有人会有异议。那么,既然Docker提供的服务属于“容器”技术,那么反观“容器”技术的本质与历史,我们又可以发现什么呢?正如前文所提到的,Docker使用的“容器”技术,主要是以Linux的cgroup、namespace等内核特性为基础,保障进程或者进程组处于一个隔离、安全的环境。Docker发行第一个版本是在2013年的3月,而cgroup的正式亮相可以追溯到2007年下半年,当时cgroup被合并至Linux内核2.6.24版本。期间6年时间,并不是“容器”技术发展的真空期,2008年LXC(Linux Container)诞生,其简化了容器的创建与管理;之后业界一些PaaS平台也初步尝试采用容器技术作为其云应用的运行环境;而与Docker发布同年,Google也发布了开源容器管理工具lmctfy。除此之外,若抛开Linux操作系统,其他操作系统如FreeBSD、Solaris等,同样诞生了作用相类似的“容器”技术,其发展历史更是需要追溯至千禧年初期。

可见,“容器”技术的发展不可谓短暂,然而论同时代的影响力,却鲜有Docker的媲美者。不论是云计算大潮催生了Docker技术,抑或是Docker技术赶上了云计算的大时代,毋庸置疑的是,Docker作为领域内的新宠儿,必然会继续受到业界的广泛青睐。云计算时代,分布式应用逐渐流行,并对其自身的构建、交付与运行有着与传统不一样的要求。借助Linux内核的cgroup与namespace特性,自然可以做到应用运行环境的资源隔离与应用部署的快速等;然而,cgroup和namespace等内核特性却无法为容器的运行环境做全盘打包。而Docker的设计则很好得考虑到了这一点,除cgroup和namespace之外,另外采用了神奇的“镜像”技术作为Docker管理文件系统以及运行环境的强有力补充。Docker灵活的“镜像”技术,在笔者看来,也是其大红大紫最重要的因素之一。

2.Docker镜像介绍

大家看到这,第一个问题肯定是“什么是Docker镜像”?

据Docker官网的技术文档描述,Image(镜像)是Docker术语的一种,代表一个只读的layer。而layer则具体代表Docker Container文件系统中可叠加的一部分。

笔者如此介绍Docker镜像,相信众多Docker爱好者理解起来依旧是云里雾里。那么理解之前,先让我们来认识一下与Docker镜像相关的4个概念:rootfs、Union mount、image以及layer。

2.1 rootfs

Rootfs:代表一个Docker Container在启动时(而非运行后)其内部进程可见的文件系统视角,或者是Docker Container的根目录。当然,该目录下含有Docker Container所需要的系统文件、工具、容器文件等。

传统来说,Linux操作系统内核启动时,内核首先会挂载一个只读(read-only)的rootfs,当系统检测其完整性之后,决定是否将其切换为读写(read-write)模式,或者最后在rootfs之上另行挂载一种文件系统并忽略rootfs。Docker架构下,依然沿用Linux中rootfs的思想。当Docker Daemon为Docker Container挂载rootfs的时候,与传统Linux内核类似,将其设定为只读(read-only)模式。在rootfs挂载完毕之后,和Linux内核不一样的是,Docker Daemon没有将Docker Container的文件系统设为读写(read-write)模式,而是利用Union mount的技术,在这个只读的rootfs之上再挂载一个读写(read-write)的文件系统,挂载时该读写(read-write)文件系统内空无一物。

举一个Ubuntu容器启动的例子。假设用户已经通过Docker Registry下拉了Ubuntu:14.04的镜像,并通过命令docker run –it ubuntu:14.04 /bin/bash将其启动运行。则Docker Daemon为其创建的rootfs以及容器可读写的文件系统可参见图2.1:

ubuntu

图2.1 Ubuntu 14.04容器rootfs示意图
正如read-only和read-write的含义那样,该容器中的进程对rootfs中的内容只拥有读权限,对于read-write读写文件系统中的内容既拥有读权限也拥有写权限。通过观察图2.1可以发现:容器虽然只有一个文件系统,但该文件系统由“两层”组成,分别为读写文件系统和只读文件系统。这样的理解已然有些层级(layer)的意味。

简单来讲,可以将Docker Container的文件系统分为两部分,而上文提到是Docker Daemon利用Union Mount的技术,将两者挂载。那么Union mount又是一种怎样的技术?

2.2 Union mount

Union mount:代表一种文件系统挂载的方式,允许同一时刻多种文件系统挂载在一起,并以一种文件系统的形式,呈现多种文件系统内容合并后的目录。

一般情况下,通过某种文件系统挂载内容至挂载点的话,挂载点目录中原先的内容将会被隐藏。而Union mount则不会将挂载点目录中的内容隐藏,反而是将挂载点目录中的内容和被挂载的内容合并,并为合并后的内容提供一个统一独立的文件系统视角。通常来讲,被合并的文件系统中只有一个会以读写(read-write)模式挂载,而其他的文件系统的挂载模式均为只读(read-only)。实现这种Union mount技术的文件系统一般被称为Union Filesystem,较为常见的有UnionFS、AUFS、OverlayFS等。

Docker实现容器文件系统Union mount时,提供多种具体的文件系统解决方案,如Docker早版本沿用至今的的AUFS,还有在docker 1.4.0版本中开始支持的OverlayFS等。

更深入的了解Union mount,可以使用AUFS文件系统来进一步阐述上文中ubuntu:14.04容器文件系统的例子。如图2.2:
ubuntu_mount

图2.2 AUFS挂载Ubuntu 14.04文件系统示意图
使用镜像ubuntu:14.04创建的容器中,可以暂且将该容器整个rootfs当成是一个文件系统。上文也提到,挂载时读写(read-write)文件系统中空无一物。既然如此,从用户视角来看,容器内文件系统和rootfs完全一样,用户完全可以按照往常习惯,无差别的使用自身视角下文件系统中的所有内容;然而,从内核的角度来看,两者在有着非常大的区别。追溯区别存在的根本原因,那就不得不提及AUFS等文件系统的COW(copy-on-write)特性。

COW文件系统和其他文件系统最大的区别就是:从不覆写已有文件系统中已有的内容。由于通过COW文件系统将两个文件系统(rootfs和read-write filesystem)合并,最终用户视角为合并后的含有所有内容的文件系统,然而在Linux内核逻辑上依然可以区别两者,那就是用户对原先rootfs中的内容拥有只读权限,而对read-write filesystem中的内容拥有读写权限。

既然对用户而言,全然不知哪些内容只读,哪些内容可读写,这些信息只有内核在接管,那么假设用户需要更新其视角下的文件/etc/hosts,而该文件又恰巧是rootfs只读文件系统中的内容,内核是否会抛出异常或者驳回用户请求呢?答案是否定的。当此情形发生时,COW文件系统首先不会覆写read-only文件系统中的文件,即不会覆写rootfs中/etc/hosts,其次反而会将该文件拷贝至读写文件系统中,即拷贝至读写文件系统中的/etc/hosts,最后再对后者进行更新操作。如此一来,纵使rootfs与read-write filesystem中均由/etc/ hosts,诸如AUFS类型的COW文件系统也能保证用户视角中只能看到read-write filesystem中的/etc/hosts,即更新后的内容。

当然,这样的特性同样支持rootfs中文件的删除等其他操作。例如:用户通过apt-get软件包管理工具安装Golang,所有与Golang相关的内容都会被安装在读写文件系统中,而不会安装在rootfs。此时用户又希望通过apt-get软件包管理工具删除所有关于MySQL的内容,恰巧这部分内容又都存在于rootfs中时,删除操作执行时同样不会删除rootfs实际存在的MySQL,而是在read-write filesystem中删除该部分内容,导致最终rootfs中的MySQL对容器用户不可见,也不可访。

掌握Docker中rootfs以及Union mount的概念之后,再来理解Docker镜像,就会变得水到渠成。

2.3 image

Docker中rootfs的概念,起到容器文件系统中基石的作用。对于容器而言,其只读的特性,也是不难理解。神奇的是,实际情况下Docker的rootfs设计与实现比上文的描述还要精妙不少。

继续以ubuntu 14.04为例,虽然通过AUFS可以实现rootfs与read-write filesystem的合并,但是考虑到rootfs自身接近200MB的磁盘大小,如果以这个rootfs的粒度来实现容器的创建与迁移等,是否会稍显笨重,同时也会大大降低镜像的灵活性。而且,若用户希望拥有一个ubuntu 14.10的rootfs,那么是否有必要创建一个全新的rootfs,毕竟ubuntu 14.10和ubuntu 14.04的rootfs中有很多一致的内容。

Docker中image的概念,非常巧妙的解决了以上的问题。最为简单的解释image,就是 Docker容器中只读文件系统rootfs的一部分。换言之,实际上Docker容器的rootfs可以由多个image来构成。多个image构成rootfs的方式依然沿用Union mount技术。

多个Image构成rootfs的示意图如图2.3(图中,rootfs中每一层image中的内容划分只为了阐述清楚rootfs由多个image构成,并不代表实际情况中rootfs中的内容划分):

rootfs

图2.3容器rootfs多image构成图
从上图可以看出,举例的容器rootfs包含4个image,其中每个image中都有一些用户视角文件系统中的一部分内容。4个image处于层叠的关系,除了最底层的image,每一层的image都叠加在另一个image之上。另外,每一个image均含有一个image ID,用以唯一的标记该image。

基于以上的概念,Docker Image中又抽象出两种概念:Parent Image以及Base Image。除了容器rootfs最底层的image,其余image都依赖于其底下的一个或多个image,而Docker中将下一层的image称为上一层image的Parent Image。以图2.3为例,imageID_0是imageID_1的Parent Image,imageID_2是imageID_3的Parent Image,而imageID_0没有Parent Image。对于最下层的image,即没有Parent Image的镜像,在Docker中习惯称之为Base Image。

通过image的形式,原先较为臃肿的rootfs被逐渐打散成轻便的多层。Image除了轻便的特性,同时还有上文提到的只读特性,如此一来,在不同的容器、不同的rootfs中image完全可以用来复用。

多image组织关系与复用关系如图2.4(图中镜像名称的举例只为将image之间的关系阐述清楚,并不代表实际情况中相应名称image之间的关系):
image_wood

图2.4 多image组织关系示意图
图2.4中,共罗列了11个image,这11个image之间的关系呈现一副森林图。森林中含有两棵树,左边树中包含5个节点,即含有5个image;右边树中包含6个节点,即含有6个image。图中,有些image标记了红色字段,意味该image代表某一种容器镜像rootfs的最上层image。如图中的ubuntu:14.04,代表imageID_3为该类型容器rootfs的最上层,沿着该节点找到树的根节点,可以发现路径上还有imageID_2,imageID_1和imageID_0。特殊的是,imageID_2作为imageID_3的Parent Image,同时又是容器镜像ubuntu:12.04的rootfs中的最上层,可见镜像ubuntu:14.04只是在镜像ubuntu:12.04之上,再另行叠加了一层。因此,在下载镜像ubuntu:12.04以及ubuntu:14.04时,只会下载一份imageID_2、imageID_1和imageID_0,实现image的复用。同时,右边树中mysql:5.6、mongo:2.2、debian:wheezy和debian:jessie也呈现同样的关系。

2.4 layer

Docker术语中,layer是一个与image含义较为相近的词。容器镜像的rootfs是容器只读的文件系统,rootfs又是由多个只读的image构成。于是,rootfs中每个只读的image都可以称为一层layer。

除了只读的image之外,Docker Daemon在创建容器时会在容器的rootfs之上,再mount一层read-write filesystem,而这一层文件系统,也称为容器的一层layer,常被称为top layer。

因此,总结而言,Docker容器中的每一层只读的image,以及最上层可读写的文件系统,均被称为layer。如此一来,layer的范畴比image多了一层,即多包含了最上层的read-write filesystem。

有了layer的概念,大家可以思考这样一个问题:容器文件系统分为只读的rootfs,以及可读写的top layer,那么容器运行时若在top layer中写入了内容,那这些内容是否可以持久化,并且也被其它容器复用?

上文对于image的分析中,提到了image有复用的特性,既然如此,再提一个更为大胆的假设:容器的top layer是否可以转变为image?

答案是肯定的。Docker的设计理念中,top layer转变为image的行为(Docker中称为commit操作),大大释放了容器rootfs的灵活性。Docker的开发者完全可以基于某个镜像创建容器做开发工作,并且无论在开发周期的哪个时间点,都可以对容器进行commit,将所有top layer中的内容打包为一个image,构成一个新的镜像。Commit完毕之后,用户完全可以基于新的镜像,进行开发、分发、测试、部署等。不仅docker commit的原理如此,基于Dockerfile的docker build,其追核心的思想,也是不断将容器的top layer转化为image。

3.总结

Docker风暴席卷全球,并非偶然。如今的云计算时代下,轻量级容器技术与灵活的镜像技术相结合,似乎颠覆了以往的软件交付模式,为持续集成(Continuous Integration, CI)与持续交付(Continuous Delivery, CD)的发展带来了全新的契机。

理解Docker的“镜像”技术,有助于Docker爱好者更好的使用、创建以及交付Docker镜像。基于此,本文从Docker镜像的4个重要概念入手,介绍了Docker镜像中包含的内容,涉及到的技术,还有重要的特性。Docker引入优秀的“镜像”技术时,着实使容器的使用变得更为便利,也拓宽了Docker的使用范畴。然而,于此同时,我们也应该理性地看待镜像技术引入时,是否会带来其它的副作用。关于镜像技术的其它思考,《Docker源码分析系列》将在后续另文分析。

4.作者介绍

孙宏亮,DaoCloud初创团队成员,软件工程师,浙江大学VLIS实验室应届研究生。读研期间活跃在PaaS和Docker开源社区,对Cloud Foundry有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014年末以合伙人身份加入DaoCloud团队,致力于传播以Docker为主的容器的技术,推动互联网应用的容器化步伐。邮箱:allen.sun@daocloud.io

5.参考文献

http://www.csdn.net/article/2014-09-24/2821832
http://en.wikipedia.org/wiki/Cgroups
http://www.infoq.com/cn/articles/docker-future
https://docs.docker.com/terms/image/
https://docs.docker.com/terms/layer/#layer
http://en.wikipedia.org/wiki/Union_mount
https://www.usenix.org/legacy/publications/library/proceedings/neworl/full_papers/mckusick.a
http://www.qnx.com/developers/docs/660/index.jsp?topic=%2Fcom.qnx.doc.neutrino.sys_arch%2Ftopic%2Ffsys_COW_filesystem.html

6.下期预告

Docker源码分析(十):Docker镜像下载
Docker源码分析(十一):Docker镜像存储