我们在容器里,运行 df 命令,你可以看到在容器中根目录 (/) 的文件系统类型是"overlay",它不是我们在普通 Linux 节点上看到的 ext4 或者 xfs 之类常见的文件系统。
那么看到这里你肯定想问,Overlay 是一个什么样的文件系统呢,容器为什么要用这种文件系统?
在说容器文件系统前,我们先来想象一下如果没有文件系统管理的话会怎样。假设有这么一个场景,在一个宿主机上需要运行 100 个容器。
每个容器都需要一个镜像,这个镜像就把容器中程序需要运行的二进制文件,库文件,配置文件,其他的依赖文件等全部都打包成一个镜像文件。
如果没有特别的容器文件系统,只是普通的 ext4 或者 xfs 文件系统,那么每次启动一个容器,就需要把一个镜像文件下载并且存储在宿主机上。
我举个例子帮你理解,比如说,假设一个镜像文件的大小是 500MB,那么 100 个容器的话,就需要下载 500MB*100= 50GB 的文件,并且占用 50GB 的磁盘空间。
如果你再分析一下这 50GB 里的内容,你会发现,在绝大部分的操作系统里,库文件都是差不多的。而且,在容器运行的时候,这类文件也不会被改动,基本上都是只读的。特别是这样的情况:假如这 100 个容器镜像都是基于"ubuntu:18.04"的,每个容器镜像只是额外复制了 50MB 左右自己的应用程序到"ubuntu: 18.04"里,那么就是说在总共 50GB 的数据里,有 90% 的数据是冗余的。
讲到这里,你不难推测出理想的情况应该是什么样的?
没错,当然是在一个宿主机上只要下载并且存储存一份"ubuntu:18.04",所有基于"ubuntu:18.04"镜像的容器都可以共享这一份通用的部分。这样设置的话,不同容器启动的时候,只需要下载自己独特的程序部分就可以。
正是为了有效地减少磁盘上冗余的镜像数据,同时减少冗余的镜像数据在网络上的传输,选择一种针对于容器的文件系统是很有必要的,而这类的文件系统被称为 UnionFS。
UnionFS 这类文件系统实现的主要功能是把多个目录(处于不同的分区)一起挂载(mount)在一个目录下。
这种多目录挂载的方式,正好可以解决我们刚才说的容器镜像的问题。比如,我们可以把 ubuntu18.04 这个基础镜像的文件放在一个目录 ubuntu18.04/ 下,容器自己额外的程序文件 app_1_bin 放在 app_1/ 目录下。然后,我们把这两个目录挂载到 container_1/ 这个目录下,作为容器 1 看到的文件系统;对于容器 2,就可以把 ubuntu18.04/ 和 app_2/ 两个目录一起挂载到 container_2 的目录下。这样在节点上我们只要保留一份 ubuntu18.04 的文件就可以了。
UnionFS 类似的有很多种实现,包括在 Docker 里最早使用的 AUFS,还有目前我们使用的 OverlayFS。
前面我们在运行df的时候,看到的文件系统类型"overlay"指的就是 OverlayFS。
在 Linux 内核 3.18 版本中,OverlayFS 代码正式合入 Linux 内核的主分支。在这之后,OverlayFS 也就逐渐成为各个主流 Linux 发行版本里缺省使用的容器文件系统了。
网上 Julia Evans 有个blog,里面有个的 OverlayFS 使用的例子,很简单,我们也拿这个例子来理解一下 OverlayFS 的一些基本概念。
How containers work: overlayfs
https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs
你可以先执行一下这一组命令。
###############################################
#!/bin/bash
umount ./merged
rm upper lower merged work -r
mkdir upper lower merged work
echo "I'm from lower!" > lower/in_lower.txt
echo "I'm from upper!" > upper/in_upper.txt
# `in_both` is in both directories
echo "I'm from lower!" > lower/in_both.txt
echo "I'm from upper!" > upper/in_both.txt
sudo mount -t overlay overlay \
-o lowerdir=./lower,upperdir=./upper,workdir=./work \
./merged
################################################
我们可以看到,OverlayFS 的一个 mount 命令牵涉到四类目录,分别是 lower,upper,merged 和 work,那它们是什么关系呢?
我们看下面这张图,这和前面 UnionFS 的工作示意图很像,也不奇怪,OverlayFS 就是 UnionFS 的一种实现。接下来,我们从下往上依次看看每一层的功能。
首先,最下面的"lower/",也就是被 mount 两层目录中底下的这层(lowerdir)。
在 OverlayFS 中,最底下这一层里的文件是不会被修改的,你可以认为它是只读的。我还想提醒你一点,在这个例子里我们只有一个 lower/ 目录,不过 OverlayFS 是支持多个 lowerdir 的。然后我们看"uppder/",它是被 mount 两层目录中上面的这层 (upperdir)。
在 OverlayFS 中,如果有文件的创建,修改,删除操作,那么都会在这一层反映出来,它是可读写的。接着是最上面的"merged" ,它是挂载点(mount point)目录,也是用户看到的目录,用户的实际文件操作在这里进行。
其实还有一个"work/",这个目录没有在这个图里,它只是一个存放临时文件的目录,OverlayFS 中如果有文件修改,就会在中间过程中临时存放文件到这里。
从这个例子我们可以看到,OverlayFS 会 mount 两层目录,分别是 lower 层和 upper 层,这两层目录中的文件都会映射到挂载点上。
从挂载点的视角看,upper 层的文件会覆盖 lower 层的文件,比如"in_both.txt"这个文件,在 lower 层和 upper 层都有,但是挂载点 merged/ 里看到的只是 upper 层里的 in_both.txt.
如果我们在 merged/ 目录里做文件操作,具体包括这三种:
第一种,新建文件,这个文件会出现在 upper/ 目录中。
第二种是删除文件,如果我们删除"in_upper.txt",那么这个文件会在 upper/ 目录中消失。
如果删除"in_lower.txt", 在 lower/ 目录里的"in_lower.txt"文件不会有变化,只是在 upper/ 目录中增加了一个特殊文件来告诉 OverlayFS,"in_lower.txt'这个文件不能出现在 merged/ 里了,这就表示它已经被删除了。
第三种是修改文件,类似如果修改"in_lower.txt",那么就会在 upper/ 目录中新建一个"in_lower.txt"文件,包含更新的内容,而在 lower/ 中的原来的实际文件"in_lower.txt"不会改变。
通过这个例子,我们知道了 OverlayFS 是怎么工作了。
那么我们可以再想一想,怎么把它运用到容器的镜像文件上?
其实也不难,从系统的 mounts 信息中,我们可以看到 Docker 是怎么用 OverlayFS 来挂载镜像文件的:
容器镜像文件可以分成多个层(layer),每层可以对应 OverlayFS 里 lowerdir 的一个目录,lowerdir 支持多个目录,也就可以支持多层的镜像文件。
在容器启动后,对镜像文件中修改就会被保存在 upperdir 里了。
在理解了容器使用的 OverlayFS 文件系统后,我们再回到开始的问题,为什么在宿主机升级之后,在容器里读写文件的性能降低了?
现在我们至少应该知道,在容器中读写文件性能降低了,那么应该是 OverlayFS 的性能在新的 ubuntu20.04 中降低了。
Linux 为了完善 OverlayFS,增加了 OverlayFS 自己的 read/write 函数接口,从而不再直接调用 OverlayFS 后端文件系统(比如 xfs,ext4)的读写接口。但是它只实现了同步 I/O(sync I/O),并没有实现异步 I/O。
在 fio 做文件系统性能测试的时候使用的是异步 I/O,这样才可以得到文件系统的性能最大值。所以,在内核 5.4 上就无法对 OverlayFS 测出最高的性能指标了。
在 Linux 内核 5.6 版本中,这个问题已经通过下面的这个补丁给解决了,有兴趣的同学可以看一下。
overlayfs: stack file operations
https://lwn.net/Articles/755889/
为什么要有容器自己的文件系统?
很重要的一点是减少相同镜像文件在同一个节点上的数据冗余,可以节省磁盘空间,也可以减少镜像文件下载占用的网络资源。
作为容器文件系统,UnionFS 通过多个目录挂载的方式工作。
OverlayFS 就是 UnionFS 的一种实现,是目前主流 Linux 发行版本中缺省使用的容器文件系统。
OverlayFS 也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir 和 upperdir。
lowerdir 允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除的,也就是只读的;upperdir 只有一个,不过这个目录是可读写的,挂载点目录中的所有文件修改都会在 upperdir 中反映出来。
容器的镜像文件中各层正好作为 OverlayFS 的 lowerdir 的目录,然后加上一个空的 upperdir 一起挂载好后,就组成了容器的文件系统。
OverlayFS 在 Linux 内核中还在不断的完善,比如我们在这一讲看到的在 kenel 5.4 中对异步 I/O 操作的缺失,这也是我们在使用容器文件系统的时候需要注意的。
1. 在上面 OverlayFS 的例子的基础上,建立 2 个 lowerdir 的目录,并且在目录中建立相同文件名的文件,然后一起做一个 overlay mount,看看会发生什么?
经过实验确认,只会在merge即联合挂载点里生成一个文件名,也就是说overlay文件系统为了省存储空间是做了同名文件合并优化。
“merge层”相当于提供给用户进行交互的视图层;
“upper层”相当于存储实际发生变动的地方;
“lower层”是不变的,用户通过merge层视图对lower层文件的所有操作,都被重定向到“upper层”了,特殊的是删除操作,会在upper层生成一个特殊的c类型的文件来代表该文件被删。
用户交互时只能对merge层的视图来操作。
在merge层的视图中,上层覆盖下层同名文件,上下关系不只是upper和lower之间,即便在多个lower层中,也是上层覆盖下层。
那么,merged/in_lower.txt ”里的值有可能是"I'm from lower2!"吗?
2.aufs,是完全被废弃吗?aufs的废弃是指在内核层的废弃吗?之前安装docker时,时可以配置使用aufs 还是overlay2,也就是说内核层还未完全去除对aufs的支持吗?
aufs的代码从来就没有进入Linux内核的主干。
3.本文中描述的现象,一个重要的原因是容器镜像里只有rootfs,没有Linux内核,宿主机上的所有容器是共用宿主机内核的。所以,当宿主机内核版本升级后,容器镜像并没有相应的升级,也会产生这个问题,文中并没有对这个知识要点说明。
容器镜像中只有rootfs没有Linux内核是对的。
在文章里,宿主机内核升级后,无论容器的镜像是否升级,都会有这个问题。文中的问题是overlayfs引起的,和镜像中的文件没有关系。
4. CentOS7.x 容器节点 tmpfs文件类型与 Overlayfs文件类型的区别是什么?
tmpfs只是用来存放一些临时文件的内存文件系统,比如/tmp目录可以使用tmpfs。
5.假如我将一个卷(宿主机上的某个目录)挂在到容器文件系统中的某个目录,我在容器中对这个卷中的数据做读写操作,因为这个挂载路径也是容器文件系统的一部分,性能是不是也是会有影响的?
如果以volume的方式挂载到容器中,那么它就不是以overlayfs的文件系统。
性能是否影响要看volume目录的位置在哪个物理磁盘上,和它共享物理磁盘的有哪些读写进程。
6.对当前k8s和docker的相爱相杀有什么看法?未来docker市场会被podman取代吗?如果会的话,这个过程大概要多久?
容器云平台里,k8s肯定是主流,用了k8s, 基本就不需要要docker了,启动容器的程序肯定是越简单越好。
参考
如何理解容器文件系统?
https://time.geekbang.org/column/article/318173
容器实战高手课/在实战中深入理解容器技术的本质
https://time.geekbang.org/column/intro/365
How containers work: overlayfs
https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs
网友评论