228 lines
14 KiB
Markdown
228 lines
14 KiB
Markdown
|
||
|
||
因收到Google相关通知,网站将会择期关闭。相关通知内容
|
||
|
||
|
||
11 容器文件系统:我在容器中读写文件怎么变慢了?
|
||
你好,我是程远。从这一讲开始,我们进入容器存储这个模块。
|
||
|
||
这一模块我们所讲的内容,都和容器里的文件读写密切相关。因为所有的容器的运行都需要一个容器文件系统,那么我们就从容器文件系统先开始讲起。
|
||
|
||
那我们还是和以前一样,先来看看我之前碰到了什么问题。
|
||
|
||
这个问题具体是我们在宿主机上,把Linux从ubuntu18.04升级到ubuntu20.04之后发现的。
|
||
|
||
在我们做了宿主机的升级后,启动了一个容器,在容器里用fio这个磁盘性能测试工具,想看一下容器里文件的读写性能。结果我们很惊讶地发现,在ubuntu 20.04宿主机上的容器中文件读写的性能只有ubuntu18.04宿主机上的1/8左右了,那这是怎么回事呢?
|
||
|
||
问题再现
|
||
|
||
这里我提醒一下你,因为涉及到两个Linux的虚拟机,问题再现这里我为你列出了关键的结果输出截图,不方便操作的同学可以重点看其中的思路。
|
||
|
||
我们可以先启动一个ubuntu18.04的虚拟机,它的Linux内核版本是4.15的,然后在虚拟机上用命令 docker run -it ubuntu:18.04 bash 启动一个容器,接着在容器里运行fio这条命令,看一下在容器中读取文件的性能。
|
||
|
||
# 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的磁盘文件,用来计算文件的读取性能。
|
||
|
||
那我们看到在ubuntu 18.04,内核4.15上的容器I/O性能是584MB/s的带宽,IOPS(I/O per second)是150K左右。
|
||
|
||
|
||
|
||
同样我们再启动一个ubuntu 20.04,内核5.4的虚拟机,然后在它的上面也启动一个容器。
|
||
|
||
我们运行 docker run -it ubuntu:20.04 bash ,接着在容器中使用同样的fio命令,可以看到它的I/O性能是70MB带宽,IOPS是18K左右。实践证明,这的确比老版本的ubuntu 18.04差了很多。
|
||
|
||
|
||
|
||
知识详解
|
||
|
||
如何理解容器文件系统?
|
||
|
||
刚才我们对比了升级前后的容器读写性能差异,那想要分析刚刚说的这个性能的差异,我们需要先理解容器的文件系统。
|
||
|
||
我们在容器里,运行 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的文件就可以了。
|
||
|
||
|
||
|
||
OverlayFS
|
||
|
||
UnionFS类似的有很多种实现,包括在Docker里最早使用的AUFS,还有目前我们使用的OverlayFS。前面我们在运行df的时候,看到的文件系统类型”overlay”指的就是OverlayFS。
|
||
|
||
在Linux内核3.18版本中,OverlayFS代码正式合入Linux内核的主分支。在这之后,OverlayFS也就逐渐成为各个主流Linux发行版本里缺省使用的容器文件系统了。
|
||
|
||
网上Julia Evans有个blog,里面有个的OverlayFS使用的例子,很简单,我们也拿这个例子来理解一下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中降低了。
|
||
|
||
要找到问题的根因,我们还需要进一步的debug。对于性能问题,我们需要使用Linux下的perf工具来查看一下,具体怎么使用perf来解决问题,我们会在后面讲解。
|
||
|
||
这里你只要看一下结果就可以了,自下而上是函数的一个调用顺序。通过perf工具,我们可以比较在容器中运行fio的时候,ubuntu 18.04和ubuntu 20.04在内核函数调用上的不同。
|
||
|
||
|
||
|
||
|
||
|
||
我们从系统调用框架之后的函数aio_read()开始比较:Linux内核4.15里aio_read()之后调用的是xfs_file_read_iter(),而在Linux 内核5.4里,aio_read()之后调用的是ovl_read_iter()这个函数,之后再调用xfs_file_read_iter()。
|
||
|
||
这样我们就可以去查看一下,在内核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版本中,这个问题已经通过下面的这个补丁给解决了,有兴趣的同学可以看一下。
|
||
|
||
commit 2406a307ac7ddfd7effeeaff6947149ec6a95b4e
|
||
Author: Jiufei Xue <[email protected]>
|
||
Date: Wed Nov 20 17:45:26 2019 +0800
|
||
|
||
ovl: implement async IO routines
|
||
|
||
A performance regression was observed since linux v4.19 with aio test using
|
||
fio with iodepth 128 on overlayfs. The queue depth of the device was
|
||
always 1 which is unexpected.
|
||
|
||
After investigation, it was found that commit 16914e6fc7e1 ("ovl: add
|
||
ovl_read_iter()") and commit 2a92e07edc5e ("ovl: add ovl_write_iter()")
|
||
resulted in vfs_iter_{read,write} being called on underlying filesystem,
|
||
which always results in syncronous IO.
|
||
|
||
Implement async IO for stacked reading and writing. This resolves the
|
||
performance regresion.
|
||
|
||
This is implemented by allocating a new kiocb for submitting the AIO
|
||
request on the underlying filesystem. When the request is completed, the
|
||
new kiocb is freed and the completion callback is called on the original
|
||
iocb.
|
||
|
||
Signed-off-by: Jiufei Xue <[email protected]>
|
||
Signed-off-by: Miklos Szeredi <[email protected]>
|
||
|
||
|
||
重点总结
|
||
|
||
这一讲,我们最主要的内容是理解容器文件系统。为什么要有容器自己的文件系统?很重要的一点是减少相同镜像文件在同一个节点上的数据冗余,可以节省磁盘空间,也可以减少镜像文件下载占用的网络资源。
|
||
|
||
作为容器文件系统,UnionFS通过多个目录挂载的方式工作。OverlayFS就是UnionFS的一种实现,是目前主流Linux发行版本中缺省使用的容器文件系统。
|
||
|
||
OverlayFS也是把多个目录合并挂载,被挂载的目录分为两大类:lowerdir和upperdir。
|
||
|
||
lowerdir允许有多个目录,在被挂载后,这些目录里的文件都是不会被修改或者删除的,也就是只读的;upperdir只有一个,不过这个目录是可读写的,挂载点目录中的所有文件修改都会在upperdir中反映出来。
|
||
|
||
容器的镜像文件中各层正好作为OverlayFS的lowerdir的目录,然后加上一个空的upperdir一起挂载好后,就组成了容器的文件系统。
|
||
|
||
OverlayFS在Linux内核中还在不断的完善,比如我们在这一讲看到的在kenel 5.4中对异步I/O操作的缺失,这也是我们在使用容器文件系统的时候需要注意的。
|
||
|
||
思考题
|
||
|
||
在这一讲OverlayFS的例子的基础上,建立2个lowerdir的目录,并且在目录中建立相同文件名的文件,然后一起做一个overlay mount,看看会发生什么?
|
||
|
||
欢迎在留言区和我分享你的思考和疑问。如果这篇文章让你有所收获,也欢迎分享给你的同事、朋友,一起学习探讨。
|
||
|
||
|
||
|
||
|