由浅入深 docker 系列: (6) 镜像分层

这篇文章我们简单聊聊 docker 的镜像分层。

思考题

在第四篇文章容器与虚拟机中,我们说到 docker 其实就是把可执行程序及其所有的依赖打包成镜像文件,然后调用宿主机内核接口运行容器。可以想象,像 ubuntu等基础镜像,体积必然不小。那么,思考以下几个问题:

  • 我们基于同一个镜像(ubuntu 18.4)启动了两个容器,会占用两倍磁盘空间吗?
  • 我们在容器内修改或者新建了某个文件,要修改原镜像吗?
  • 我们基于某镜像(ubuntu 18.04)新建一个镜像(myubuntu),需要将原镜像文件全部拷贝到新镜像中吗?

首先,让我们尝试思考下,如果我们去做,该如何高效的解决这些问题?

  • 问题 1,只要将同一个镜像文件加载到内存不同位置就行了,没必要在磁盘上存储多份,可以节省大量存储空间。
  • 问题 2,我们可以参考 Linux 内核管理内存的 *Copy-On-Write 策略,也即读时大家共用一份文件,如果需要修改再复制一份进行修改,而大部分文件是其实不会修改的,这样可以最大限度节省空间,提升性能。
  • 问题 3,我们可以将镜像文件分为多个独立的层,然后新镜像文件只要引用基础镜像文件就可以了,这样可以节省大量空间。至于修改基础镜像文件的情况,参考问题 2 。

如果你能想到以上思路,那么恭喜你,因为 Docker 就是这么做的,你已经具备为写docker 写一套文件系统的实力了(哈哈哈哈,不要飘,还有大量技术细节需要思考)。

Docker的镜像分层

接下来,我们来看看 Docker的镜像分层机制。

Docker镜像是分层构建的,Dockerfile 中每条指令都会新建一层。例如以下 Dockerfile:

FROM ubuntu:18.04
COPY . /app
RUN make /app
CMD python /app/app.py

以上四条指令会创建四层,分别对应基础镜像、复制文件、编译文件以及入口文件,每层只记录本层所做的更改,而这些层都是只读层。当你启动一个容器,Docker 会在最顶部添加读写层,你在容器内做的所有更改,如写日志、修改、删除文件等,都保存到了读写层内,一般称该层为容器层,如下图所示:

docker layer

事实上,容器(container)和镜像(image)的最主要区别就是容器加上了顶层的读写层。所有对容器的修改都发生在此层,镜像并不会被修改,也即前面说的 COW(copy-on-write)技术。容器需要读取某个文件时,直接从底部只读层去读即可,而如果需要修改某文件,则将该文件拷贝到顶部读写层进行修改,只读层保持不变。

每个容器都有自己的读写层,因此多个容器可以使用同一个镜像,另外容器被删除时,其对应的读写层也会被删除(如果你希望多个容器共享或者持久化数据,可以使用 Docker volume)。

最后,执行命令 docker ps -s,可以看到最后有两列 size 和 virtual size。其中 size就是容器读写层占用的磁盘空间,而 virtual size 就是读写层加上对应只读层所占用的磁盘空间。如果两个容器是从同一个镜像创建,那么只读层就是 100%共享,即使不是从同一镜像创建,其镜像仍然可能共享部分只读层(如一个镜像是基于另一个创建)。因此,docker 实际占用的磁盘空间远远小于 virtual size 的总和。

以上就是 Docker 镜像分层的主要内容,至于这些层的交互、管理就需要存储驱动程序,也即联合文件系统(UnionFS)。Docker 可使用多种驱动,如目前已经合并入 Linux 内核、官方推荐的overlay, 曾在 Ubuntu、Debian等发行版中得到广泛使用的 AUFS,以及devicemapper、zfs等等,需要根据 Docker以及宿主机系统的版本,进行合适的选择。接下来以 AUFS 为例,简单介绍 UnionFS 的使用。


AUFS

AUFS 是一种 UnionFS,所谓 UnionFS 就是把不同物理位置的目录合并到同一个目录中。例如把 CD和硬盘 mount 到一起,就可以对 CD上的文件进行修改,再比如上面所讲 docker 的使用场景。

AUFS是Another UnionFS的首字母缩略字,2006年由冈岛顺治郎开发,是之前的 UnionFS 的完全重写,其稳定性和性能上确实好很多,但从第 2 版开始它代表advanced multi-layered unification filesystem。

这里插播一个悲情的小故事,AUFS被 Linus 拒绝合并到主线 Linux,其代码被批评为“稠密,不可读,无注释”。然后作者不断改进代码,不断提交,不断被 Linus 拒掉,最终放弃。而2014年,OverlayFS被合并到 Linux 内核 3.18版本,冈岛顺治郎再无希望。

言归正传,下面我们简单试验下 AUFS 的使用。(Ubuntu 18.04环境)

首先,创建两个文件夹代表只读层以及读写层。

mkdir readLayer
echo "original read file content" >> readLayer/readFile
mkdir writeLayer
echo "original write file content" >> writeLayer/writeFile

然后,将两个文件夹进行联合挂载。命令中默认dirs后第一个文件夹为读写权限,之后的文件夹为只读权限。

mkdir unionFs
mount -t aufs -o dirs=./writeLayer:./readLayer none ./unionFs

接下来,我们可以查看下 unionFs文件夹下的内容,发现已经有了readFile、writeFile两个文件,说明挂载成功。

然后,我们再来测试下对文件的修改操作,我们分别修改读写层以及只读层的文件。

cd unionFs
echo "edit writeable file" >> writeFile
echo "edit readonly file" >> readFile

根据我们之前的分析,针对读写层writeLayer的 writeFile 的修改,应该是直接在源文件上生效;而针对只读层readLayer的 readFile 的修改,应该是源文件保持不变,将文件拷贝到 writeLayer,然后再修改,我们来检查下。

首先,查看下只读层readLayer,文件内容并没有改变。

然后查看下读写层,可以看到新增的文件 readFile,且文件内容为修改后内容。

可以看到,我们成功模拟了只读层、读写层的联合使用,Docker的镜像分层也是此原理,只是实现更加复杂。

参考资料

以上即是 docker 镜像分层技术的简单介绍,通过这项技术,节省了大量的存储空间,也提升了容器的下载、构建、启动速度,推动了 Docker 技术的广泛使用。

如果你想更进一步了解相关细节,可以参考以下文章:

DOCKER基础技术:AUFS

About storage drivers

闲言

前后拖了 7 个月,终于把六篇文章更完了,把自己学习时的困惑、自己的理解都尽量简洁的写出来,希望能帮大家更快的理解学习。在整理的过程中,我对 Docker 技术的认知也更加清晰完整,收获很大。

每写一篇文章都很痛苦,文采、技术都不好,纠结怎么能以更清晰的方式,说更多的技术点。每写完一篇也很充实,又前近了一步。看着专栏的读者一个个增加,很开心,也很有成就感,接下来还会更新,不保证频繁,但每一篇都会尽量写好。千里之行,始于足下,加油!

本作品采用《CC 协议》,转载必须注明作者和本文链接
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1
draven

大佬牛逼,底层一点也看不懂 :joy:

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!