什么是容器系统
这一模块我们所讲的内容,都和容器里的文件读写密切相关。因为所有的容器的运行都需要一个容器文件系统,那么我们就从容器文件系统先开始讲起。
文件读写性能测试
我们可以先启动一个的虚拟机,它的 Linux 内核版本是 4.15 的,然后在虚拟机上用命令 docker run -it ubuntu:18.04 bash 启动一个容器,接着在容器里运行 fio 这条命令,看一下在容器中读取文件的性能。
1 | fio -direct=1 -iodepth=64 -rw=read -ioengine=libaio -bs=4k -size=10G -numjobs=1 -name=./fio.test |
这里我给你解释一下 fio 命令中的几个主要参数:
第一个参数是”-direct=1”,代表采用非 buffered I/O 文件读写的方式,避免文件读写过程中内存缓冲对性能的影响。
接着我们来看这”-iodepth=64”和”-ioengine=libaio”这两个参数,这里指文件读写采用异步 I/O(Async I/O)的方式,也就是进程可以发起多个 I/O 请求,并且不用阻塞地等待 I/O 的完成。稍后等 I/O 完成之后,进程会收到通知。
这种异步 I/O 很重要,因为它可以极大地提高文件读写的性能。在这里我们设置了同时发出 64 个 I/O 请求。
然后是”-rw=read,-bs=4k,-size=10G”,这几个参数指这个测试是个读文件测试,每次读 4KB 大小数块,总共读 10GB 的数据。
最后一个参数是”-numjobs=1”,指只有一个进程 / 线程在运行。
所以,这条 fio 命令表示我们通过异步方式读取了 10GB 的磁盘文件,用来计算文件的读取性能。
理解容器文件系统
我们在容器里,运行 df 命令,你可以看到在容器中根目录 (/) 的文件系统类型是”overlay”,它不是我们在普通 Linux 节点上看到的 Ext4 或者 XFS 之类常见的文件系统。
每个容器都需要一个镜像,这个镜像就把容器中程序需要运行的二进制文件,库文件,配置文件,其他的依赖文件等全部都打包成一个镜像文件。
如果没有特别的容器文件系统,只是普通的 Ext4 或者 XFS 文件系统,那么每次启动一个容器,就需要把一个镜像文件下载并且存储在宿主机上。
正是为了有效地减少磁盘上冗余的镜像数据,同时减少冗余的镜像数据在网络上的传输,选择一种针对于容器的文件系统是很有必要的,而这类的文件系统被称为 UnionFS。
UnionFS 这类文件系统实现的主要功能是把多个目录(处于不同的分区)一起挂载(mount)在一个目录下。这种多目录挂载的方式,正好可以解决我们刚才说的容器镜像的问题。
OverlayFSUnionFS 类似的有很多种实现,包括在 Docker 里最早使用的 AUFS,还有目前我们使用的 OverlayFS。
比如我们在运行df的时候,看到的文件系统类型”overlay”指的就是 OverlayFS。
在 Linux 内核 3.18 版本中,OverlayFS 代码正式合入 Linux 内核的主分支。在这之后,OverlayFS 也就逐渐成为各个主流 Linux 发行版本里缺省使用的容器文件系统了。
blog
举个OverlayFS 使用的例子
1 | !/bin/bash |
OverlayFS介绍
OverlayFS 的一个 mount 命令牵涉到四类目录,分别是 lower,upper,merged 和 work
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”不会改变。
从系统的 mounts 信息中,我们可以看到 Docker 是怎么用 OverlayFS 来挂载镜像文件的。容器镜像文件可以分成多个层(layer),每层可以对应 OverlayFS 里 lowerdir 的一个目录,lowerdir 支持多个目录,也就可以支持多层的镜像文件。
性能优化与发展
在内核 4.15 之后新加入的这个函数 ovl_read_iter() 的代码。
代码
查看代码后我们就能明白,Linux 为了完善 OverlayFS,增加了 OverlayFS 自己的 read/write 函数接口,从而不再直接调用 OverlayFS 后端文件系统(比如 XFS,Ext4)的读写接口。
但是它只实现了同步 I/O(sync I/O),并没有实现异步 I/O。
而在 fio 做文件系统性能测试的时候使用的是异步 I/O,这样才可以得到文件系统的性能最大值。
所以,在内核 5.4 上就无法对 OverlayFS 测出最高的性能指标了。
在 Linux 内核 5.6 版本中,这个问题已经通过下面的这个补丁给解决了,有兴趣的同学可以看一下。
1 | commit 2406a307ac7ddfd7effeeaff6947149ec6a95b4e |
重点总结
很重要的一点是减少相同镜像文件在同一个节点上的数据冗余,可以节省磁盘空间,也可以减少镜像文件下载占用的网络资源。
作为容器文件系统,UnionFS 通过多个目录挂载的方式工作。OverlayFS 就是 UnionFS 的一种实现,是目前主流 Linux 发行版本中缺省使用的容器文件系统。
OverlayFS 也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir 和 upperdir。
lowerdir 允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除的,也就是只读的;upperdir 只有一个,不过这个目录是可读写的,挂载点目录中的所有文件修改都会在 upperdir 中反映出来。
容器的镜像文件中各层正好作为 OverlayFS 的 lowerdir 的目录,然后加上一个空的 upperdir 一起挂载好后,就组成了容器的文件系统。