写点什么

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

  • 2015-03-06
  • 本文字数:6246 字

    阅读完需:约 20 分钟

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:

图 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:

图 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 中的内容划分):

图 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 之间的关系):

图 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 镜像存储


感谢郭蕾对本文的策划和审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2015-03-06 07:227516

评论

发布
暂无评论
发现更多内容

产品经理训练营笔记-产品思维和产品意识(中)

.nil?

产品经理训练营

架构师训练营第四周学习总结

跳蚤

Mysql 4种方式避免重复插入数据!

xcbeyond

MySQL MySQL使用 28天写作

Spring Boot 中集成 Shiro

武哥聊编程

Java springboot SpringBoot 2 shiro 28天写作

“苹果陷阱”无法避免,但我们已经在路上

JiangX

苹果 供应链 28天写作 制造业

第九周 性能优化(三)课后作业

简简单单

一起进阶一起拿高工资!Java开发进阶-log4j2日志脱敏原理分析

看点代码再上班

Java 程序员 log4j 编程开发

28天瞎写的第二百二十七天:跨年夜的故事

树上

28天写作

机器学习笔记之:监督学习

Nydia

JVM垃圾回收机制是怎样的,何时触发YoungGC或FullGC操作?

Java鱼仔

Java 面试 JVM 垃圾回收

区块链交易所APP开发|区块链交易所系统软件开发

系统开发

【CSS】格仔背景

德育处主任

html css3 大前端 CSS小技巧 28天写作

Elasticsearch 乐观锁并发控制

escray

elastic 七日更 28天写作 死磕Elasticsearch 60天通过Elastic认证考试

如何快速提升自己的能力?高效学习让你更出类拔萃。

一笑

学习方法 28天写作

我是这样使用极客时间APP的

熊斌

极客时间 28天写作

产品经理训练营作业-第二周-产品思维和产品意识

.nil?

产品经理训练营

HASH碰撞问题一直没真正搞懂?这下不用慌了

架构精进之路

算法 hash 七日更 28天写作

新荣耀历险记:带着“神装”踏上英雄试炼路

脑极体

原来Canal也可以做HA!

大数据老哥

网络出口究竟选择防火墙,还是路由器?

项目管理系列(9)- 项目分析与报告

Ian哥

28天写作

第九周 学习总结

简简单单

数字货币交易所软件开发|数字货币交易所系统APP开发

系统开发

架构师训练营第四周作业

跳蚤

四个策略,三个“坑”,读《架构师也不写代码》有感

李忠良

28天写作

一顿午饭的现实思考

石君

28天写作 择业

Mac 电脑安装Etcd和Go的etcd/clientv3

BigYoung

Mac etcd client Go 语言

Spring Boot 中集成Lucence

武哥聊编程

Java springboot SpringBoot 2 28天写作 Lucence

当情绪生病?就嫁接一段新的记忆「幻想短篇 16/28」

道伟

28天写作

2021开启数据结构与算法的学习之旅

Nick

学习 flag 新年计划

碎碎念之「卡马克的反脆弱想法生成系统」

Justin

心理学 创意 28天写作 反脆弱

Docker源码分析(九):Docker镜像_语言 & 开发_孙宏亮_InfoQ精选文章