You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

16 KiB

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/OAsync 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的带宽IOPSI/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命令牵涉到四类目录分别是loweruppermerged和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后端文件系统比如XFSExt4的读写接口。但是它只实现了同步I/Osync I/O并没有实现异步I/O。

而在fio做文件系统性能测试的时候使用的是异步I/O这样才可以得到文件系统的性能最大值。所以在内核5.4上就无法对OverlayFS测出最高的性能指标了。

在Linux内核5.6版本中,这个问题已经通过下面的这个补丁给解决了,有兴趣的同学可以看一下。

commit 2406a307ac7ddfd7effeeaff6947149ec6a95b4e
Author: Jiufei Xue <jiufei.xue@linux.alibaba.com>
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 <jiufei.xue@linux.alibaba.com>
    Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>

重点总结

这一讲,我们最主要的内容是理解容器文件系统。为什么要有容器自己的文件系统?很重要的一点是减少相同镜像文件在同一个节点上的数据冗余,可以节省磁盘空间,也可以减少镜像文件下载占用的网络资源。

作为容器文件系统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看看会发生什么

欢迎在留言区和我分享你的思考和疑问。如果这篇文章让你有所收获,也欢迎分享给你的同事、朋友,一起学习探讨。