容器化之路:谁偷走了我的构建时间

随着全面云时代到来,很多公司都走上了容器化道路,老刘所在的公司也不例外。作为一家初创型的互联网公司,容器化的确带来了很多便捷,也降低了公司成本,不过老刘却有一个苦恼,以前每天和他一起下班的小王自从公司上云以后每天都比他早下班一个小时,大家手头上的活都差不多,讲道理不应该呀,经过多番试探、跟踪、调查,终于让老刘发现了秘密的所在。

容器化之路:谁偷走了我的构建时间

作为一个开发,每天总少不了要出N个测试版本进行调试,容器化以后每次出版本都需要打成镜像,老刘发现每次他做一个镜像都要20分钟,而小王只要10分钟,对比来对比去只有这个东西不一样!

容器化之路:谁偷走了我的构建时间

Storage-Dirver到底是何方神圣?为什么能够导致构建时间上的差异?现在让我们来一窥究竟。

在回答这个问题之前我们需要先回答三个问题——什么是镜像?什么是镜像构建?什么是storage-driver?

什么是镜像?

说到镜像就绕不开容器,我们先看一张来自官方对镜像和容器解释的图片:

容器化之路:谁偷走了我的构建时间

看完以后是不是更疑惑了,我们可以这样简单粗暴的去理解,镜像就是一堆只读层的堆叠。那只读层里到底是什么呢,另外一个简单粗暴的解释:里边就是放了一堆被改动的文件。这个解释在不同的storage-driver下不一定准确但是我们可以先这样简单去理解。

那不对呀,执行容器的时候明明是可以去修改删除容器里的文件的,都是只读的话怎么去修改呢?实际上我们运行容器的时候是在那一堆只读层的顶上再增加了一个读写层,所有的操作都是在这个读写层里进行的,当需要修改一个文件的时候我们会将需要修改的文件从底层拷贝到读写层再进行修改。那如果是删除呢,我们不是没有办法删除底层的文件么?没错,确实没有办法删除,但只需要在上层把这个文件隐藏起来,就可以达到删除的效果。按照官方说法,这就是Docker的写时复制策略。

为了加深大家对镜像层的理解我们来举个栗子,用下面的Dockerfile构建一个etcd镜像:

容器化之路:谁偷走了我的构建时间

构建完成以后生成了如下的层文件:

容器化之路:谁偷走了我的构建时间

每次进入容器的时候都感觉仿佛进入了一台虚机,里面包含linux的各个系统目录。那是不是有一层目录里包含了所有的linux系统目录呢?

bingo答对!在最底层的层目录的确包含了linux的所有的系统目录文件。

容器化之路:谁偷走了我的构建时间

上述Dockerfile中有这样一步操作

ADD . /go/src/github.com/coreos/etcd 

将外面目录的文件拷到了镜像中,那这一层镜像里究竟保存了什么呢?

容器化之路:谁偷走了我的构建时间

打开发现里面就只有

/go/src/github.com/coreos/etcd这个目录,目录下存放了拷贝进来的文件。 

到这里是不是有种管中窥豹的感觉,接下来我们再来了解什么是镜像构建,这样基本上能够窥其全貌了。

什么是镜像构建?

通过第一节的内容我们知道了镜像是由一堆层目录组成的,每个层目录里放着这一层修改的文件,镜像构建简单的说就是制作和生成镜像层的过程,那这一过程是如何实现的呢?以下图流程为例:

容器化之路:谁偷走了我的构建时间

Docker Daemon首先利用基础镜像ubuntu:14.04创建了一个容器环境,通过第一节的内容我们知道容器的最上层是一个读写层,在这一层我们是可以写入修改的,Docker Daemon首先执行了RUN apt-update get命令,执行完成以后,通过Docker的commit操作将这个读写层的内容保存成一个只读的镜像层文件。接下来再在这一层的基础上继续执行 ADD run.sh命令,执行完成后继续commit成一个镜像层文件,如此反复直到将所有的Dockerfile都命令都被提交后,镜像也就做好了。

这里我们就能解释为什么etcd的某个层目录里只有一个go目录了,因为构建的过程是逐层提交的,每一层里只会保存这一层操作所涉及改动的文件。

这样看来镜像构建就是一个反复按照Dockerfile启动容器执行命令并保存成只读文件的过程,那为什么速度会不一样呢?接下来就得说到storage-driver了。

什么是storage-driver?

再来回顾一下这张图:

容器化之路:谁偷走了我的构建时间

之前我们已经知道了,镜像是由一个个的层目录叠加起来的,容器运行时只是在上面再增加一个读写层,同时还有写时复制策略保证在最顶层能够修改底层的文件内容,那这些原理是怎么实现的呢?就是靠storage-driver!

简单介绍三种常用的storage-driver:

AUFS

AUFS通过联合挂载的方式将多个层文件堆叠起来,形成一个统一的整体提供统一视图,当在读写层进行读写的时,先在本层查找文件是否存在,如果没有则一层一层的往下找。aufs的操作都是基于文件的,需要修改一个文件时无论大小都会将整个文件从只读层拷贝到读写层,因此如果需要修改的文件过大,会导致容器执行速度变慢,docker官方给出的建议是通过挂载的方式将大文件挂载进来而不是放在镜像层中。

容器化之路:谁偷走了我的构建时间

OverlayFS

OverlayFS可以认为是AUFS的升级版本,容器运行时镜像层的文件是通过硬链接的方式组成一个下层目录,而容器层则是工作在上层目录,上层目录是可读写的,下层目录是只读的,由于大量的采用了硬链接的方式,导致OverlayFS会可能会出现inode耗尽的情况,后续Overlay2对这一问题进行了优化,且性能上得到了很大的提升,不过Overlay2也有和AUFS有同样的弊端——对大文件的操作速度比较慢。

容器化之路:谁偷走了我的构建时间

DeviceMapper

DeviceMapper和前两种Storage-driver在实现上存在很大的差异。首先DeviceMapper的每一层保存的是上一层的快照,其次DeviceMapper对数据的操作不再是基于文件的而是基于数据块的。

下图是devicemapper在容器层读取文件的过程:

容器化之路:谁偷走了我的构建时间
  1. 首先在容器层的快照中找到该文件指向下层文件的指针。
  2. 再从下层0xf33位置指针指向的数据块中读取的数据到容器的存储区
  3. 最后将数据返回app。

在写入数据时还需要根据数据的大小先申请1~N个64K的容器快照,用于保存拷贝的块数据。

DeviceMapper的块操作看上去很美,实际上存在很多问题,比如频繁操作较小文件时需要不停地从资源池中分配数据库并映射到容器中,这样效率会变得很低,且DeviceMapper每次镜像运行时都需要拷贝所有的镜像层信息到内存中,当启动多个镜像时会占用很大的内存空间。

针对不同的storage-driver我们用上述etcd的dockerfile进行了一组构建测试

容器化之路:谁偷走了我的构建时间

:该数据因dockerfile以及操作系统、文件系统、网络环境的不同测试结果可能会存在较大差异

相关推荐