mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-20 08:03:43 +08:00
del
This commit is contained in:
226
极客时间专栏/geek/容器实战高手课/容器存储/11 | 容器文件系统:我在容器中读写文件怎么变慢了?.md
Normal file
226
极客时间专栏/geek/容器实战高手课/容器存储/11 | 容器文件系统:我在容器中读写文件怎么变慢了?.md
Normal file
@@ -0,0 +1,226 @@
|
||||
<audio id="audio" title="11 | 容器文件系统:我在容器中读写文件怎么变慢了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/76/fd/7645ede689f3f086c97c6cc50c919dfd.mp3"></audio>
|
||||
|
||||
你好,我是程远。从这一讲开始,我们进入容器存储这个模块。
|
||||
|
||||
这一模块我们所讲的内容,都和容器里的文件读写密切相关。因为所有的容器的运行都需要一个容器文件系统,那么我们就从容器文件系统先开始讲起。
|
||||
|
||||
那我们还是和以前一样,先来看看我之前碰到了什么问题。
|
||||
|
||||
这个问题具体是我们在宿主机上,把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左右。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/5d/5e/5df788a1c7fb9f5677557d6cb15c995e.png" alt="">
|
||||
|
||||
同样我们再启动一个ubuntu 20.04,内核5.4的虚拟机,然后在它的上面也启动一个容器。
|
||||
|
||||
我们运行 `docker run -it ubuntu:20.04 bash` ,接着在容器中使用同样的fio命令,可以看到它的I/O性能是70MB带宽,IOPS是18K左右。实践证明,这的确比老版本的ubuntu 18.04差了很多。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/90/f1/90aff20c885286d4b6b5aed7b017a9f1.png" alt="">
|
||||
|
||||
## 知识详解
|
||||
|
||||
### 如何理解容器文件系统?
|
||||
|
||||
刚才我们对比了升级前后的容器读写性能差异,那想要分析刚刚说的这个性能的差异,我们需要先理解容器的文件系统。
|
||||
|
||||
我们在容器里,运行 `df` 命令,你可以看到在容器中根目录(/)的文件系统类型是"overlay",它不是我们在普通Linux节点上看到的Ext4或者XFS之类常见的文件系统。
|
||||
|
||||
那么看到这里你肯定想问,Overlay是一个什么样的文件系统呢,容器为什么要用这种文件系统?别急,我会一步一步带你分析。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2f/b0/2fed851ba2df3232efbdca1d1cce19b0.png" alt="">
|
||||
|
||||
在说容器文件系统前,我们先来想象一下如果没有文件系统管理的话会怎样。假设有这么一个场景,在一个宿主机上需要运行100个容器。
|
||||
|
||||
在我们这个课程的[第一讲](https://time.geekbang.org/column/article/308108)里,我们就说过每个容器都需要一个镜像,这个镜像就把容器中程序需要运行的二进制文件,库文件,配置文件,其他的依赖文件等全部都打包成一个镜像文件。
|
||||
|
||||
如果没有特别的容器文件系统,只是普通的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"镜像的容器都可以共享这一份通用的部分。这样设置的话,不同容器启动的时候,只需要下载自己独特的程序部分就可以。就像下面这张图展示的这样。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/3f/c0119d9d2af9cf7386db13467027003f.jpg" alt="">
|
||||
|
||||
**正是为了有效地减少磁盘上冗余的镜像数据,同时减少冗余的镜像数据在网络上的传输,选择一种针对于容器的文件系统是很有必要的,而这类的文件系统被称为UnionFS。**
|
||||
|
||||
UnionFS这类文件系统实现的主要功能是把多个目录(处于不同的分区)一起挂载(mount)在一个目录下。这种多目录挂载的方式,正好可以解决我们刚才说的容器镜像的问题。
|
||||
|
||||
比如,我们可以把ubuntu18.04这个基础镜像的文件放在一个目录ubuntu18.04/下,容器自己额外的程序文件app_1_bin放在app_1/目录下。
|
||||
|
||||
然后,我们把这两个目录挂载到container_1/这个目录下,作为容器1看到的文件系统;对于容器2,就可以把ubuntu18.04/和app_2/两个目录一起挂载到container_2的目录下。
|
||||
|
||||
这样在节点上我们只要保留一份ubuntu18.04的文件就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/44/27/449669a1aaa8c631d7768369b275ed27.jpg" alt="">
|
||||
|
||||
### OverlayFS
|
||||
|
||||
UnionFS类似的有很多种实现,包括在Docker里最早使用的AUFS,还有目前我们使用的OverlayFS。前面我们在运行`df`的时候,看到的文件系统类型"overlay"指的就是OverlayFS。
|
||||
|
||||
在Linux内核3.18版本中,OverlayFS代码正式合入Linux内核的主分支。在这之后,OverlayFS也就逐渐成为各个主流Linux发行版本里缺省使用的容器文件系统了。
|
||||
|
||||
网上Julia Evans有个[blog](https://jvns.ca/blog/2019/11/18/how-containers-work--overlayfs/),里面有个的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中如果有文件修改,就会在中间过程中临时存放文件到这里。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ca/5d/ca894a91e0171a027ba0ded6cdf2a95d.jpg" alt="">
|
||||
|
||||
从这个例子我们可以看到,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/里了,这就表示它已经被删除了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f3/2a/f3813b984193e3aebebe1b5104f75e2a.png" alt="">
|
||||
|
||||
还有一种操作是修改文件,类似如果修改"in_lower.txt",那么就会在upper/目录中新建一个"in_lower.txt"文件,包含更新的内容,而在lower/中的原来的实际文件"in_lower.txt"不会改变。
|
||||
|
||||
通过这个例子,我们知道了OverlayFS是怎么工作了。那么我们可以再想一想,怎么把它运用到容器的镜像文件上?
|
||||
|
||||
其实也不难,从系统的mounts信息中,我们可以看到Docker是怎么用OverlayFS来挂载镜像文件的。容器镜像文件可以分成多个层(layer),每层可以对应OverlayFS里lowerdir的一个目录,lowerdir支持多个目录,也就可以支持多层的镜像文件。
|
||||
|
||||
在容器启动后,对镜像文件中修改就会被保存在upperdir里了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/55/26/55a7059809afdd3d51e5a6b3f5c83626.png" alt="">
|
||||
|
||||
## 解决问题
|
||||
|
||||
在理解了容器使用的OverlayFS文件系统后,我们再回到开始的问题,为什么在宿主机升级之后,在容器里读写文件的性能降低了?现在我们至少应该知道,在容器中读写文件性能降低了,那么应该是OverlayFS的性能在新的ubuntu20.04中降低了。
|
||||
|
||||
要找到问题的根因,我们还需要进一步的debug。对于性能问题,我们需要使用Linux下的perf工具来查看一下,具体怎么使用perf来解决问题,我们会在后面讲解。
|
||||
|
||||
这里你只要看一下结果就可以了,自下而上是函数的一个调用顺序。通过perf工具,我们可以比较在容器中运行fio的时候,ubuntu 18.04和ubuntu 20.04在内核函数调用上的不同。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/7a/6d970f9cf76bd0875ff3e505900b1b7a.png" alt="" title="ubuntu 18.04 (Linux内核4.15)环境下使用perf输出的函数调用结果">
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/46/1f/466cd0da98f4170111c5ce2436f2ed1f.png" alt="" title="ubuntu 20.04 (Linux内核 5.4)环境下使用perf输出的函数调用结果">
|
||||
|
||||
我们从系统调用框架之后的函数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()的代码。
|
||||
|
||||
查看[代码](https://lwn.net/Articles/755889/)后我们就能明白,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 <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的[例子](https://github.com/chengyli/training/blob/main/filesystem/overlayfs/test_overlayfs.sh)的基础上,建立2个lowerdir的目录,并且在目录中建立相同文件名的文件,然后一起做一个overlay mount,看看会发生什么?
|
||||
|
||||
欢迎在留言区和我分享你的思考和疑问。如果这篇文章让你有所收获,也欢迎分享给你的同事、朋友,一起学习探讨。
|
||||
234
极客时间专栏/geek/容器实战高手课/容器存储/12 | 容器文件Quota:容器为什么把宿主机的磁盘写满了?.md
Normal file
234
极客时间专栏/geek/容器实战高手课/容器存储/12 | 容器文件Quota:容器为什么把宿主机的磁盘写满了?.md
Normal file
@@ -0,0 +1,234 @@
|
||||
<audio id="audio" title="12 | 容器文件Quota:容器为什么把宿主机的磁盘写满了?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fc/c4/fc13b1771af810738b26c6005dc214c4.mp3"></audio>
|
||||
|
||||
你好,我是程远。今天我们聊一聊容器文件Quota。
|
||||
|
||||
上一讲,我们学习了容器文件系统OverlayFS,这个OverlayFS有两层,分别是lowerdir和upperdir。lowerdir里是容器镜像中的文件,对于容器来说是只读的;upperdir存放的是容器对文件系统里的所有改动,它是可读写的。
|
||||
|
||||
从宿主机的角度看,upperdir就是一个目录,如果容器不断往容器文件系统中写入数据,实际上就是往宿主机的磁盘上写数据,这些数据也就存在于宿主机的磁盘目录中。
|
||||
|
||||
当然对于容器来说,如果有大量的写操作是不建议写入容器文件系统的,一般是需要给容器挂载一个volume,用来满足大量的文件读写。
|
||||
|
||||
但是不能避免的是,用户在容器中运行的程序有错误,或者进行了错误的配置。
|
||||
|
||||
比如说,我们把log写在了容器文件系统上,并且没有做log rotation,那么时间一久,就会导致宿主机上的磁盘被写满。这样影响的就不止是容器本身了,而是整个宿主机了。
|
||||
|
||||
那对于这样的问题,我们该怎么解决呢?
|
||||
|
||||
## 问题再现
|
||||
|
||||
我们可以自己先启动一个容器,一起试试不断地往容器文件系统中写入数据,看看是一个什么样的情况。
|
||||
|
||||
用Docker启动一个容器后,我们看到容器的根目录(/)也就是容器文件系统OverlayFS,它的大小是160G,已经使用了100G。其实这个大小也是宿主机上的磁盘空间和使用情况。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/d3/3e/d32c83e404a81b301fbf8bdfd7a9c23e.png" alt="">
|
||||
|
||||
这时候,我们可以回到宿主机上验证一下,就会发现宿主机的根目录(/)的大小也是160G,同样是使用了100G。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b5/0b/b54deb08e46ff1155303581e9d1c8f0b.png" alt="">
|
||||
|
||||
好,那现在我们再往容器的根目录里写入10GB的数据。
|
||||
|
||||
这里我们可以看到容器的根目录使用的大小增加了,从刚才的100G变成现在的110G。而多写入的10G大小的数据,对应的是test.log这个文件。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c0/46/c04dcff3aa4773302495113dd2a8d546.png" alt="">
|
||||
|
||||
接下来,我们再回到宿主机上,可以看到宿主机上的根目录(/)里使用的大小也是110G了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/15/9d/155d30bc20b72c0678d1948f25cbe29d.png" alt="">
|
||||
|
||||
我们还是继续看宿主机,看看OverlayFS里upperdir目录中有什么文件?
|
||||
|
||||
这里我们仍然可以通过/proc/mounts这个路径,找到容器OverlayFS对应的lowerdir和upperdir。因为写入的数据都在upperdir里,我们就只要看upperdir对应的那个目录就行了。果然,里面存放着容器写入的文件test.log,它的大小是10GB。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/7d/4d32334dc0f1ba69881c686037f6577d.png" alt="">
|
||||
|
||||
通过这个例子,我们已经验证了在容器中对于OverlayFS中写入数据,**其实就是往宿主机的一个目录(upperdir)里写数据。**我们现在已经写了10GB的数据,如果继续在容器中写入数据,结果估计你也知道了,就是会写满宿主机的磁盘。
|
||||
|
||||
那遇到这种情况,我们该怎么办呢?
|
||||
|
||||
## 知识详解
|
||||
|
||||
容器写自己的OverlayFS根目录,结果把宿主机的磁盘写满了。发生这个问题,我们首先就会想到需要对容器做限制,限制它写入自己OverlayFS的数据量,比如只允许一个容器写100MB的数据。
|
||||
|
||||
不过我们实际查看OverlayFS文件系统的特性,就会发现没有直接限制文件写入量的特性。别担心,在没有现成工具的情况下,我们只要搞懂了原理,就能想出解决办法。
|
||||
|
||||
所以我们再来分析一下OverlayFS,它是通过lowerdir和upperdir两层目录联合挂载来实现的,lowerdir是只读的,数据只会写在upperdir中。
|
||||
|
||||
那我们是不是可以通过限制upperdir目录容量的方式,来限制一个容器OverlayFS根目录的写入数据量呢?
|
||||
|
||||
沿着这个思路继续往下想,因为upperdir在宿主机上也是一个普通的目录,这样就要看**宿主机上的文件系统是否可以支持对一个目录限制容量了。**
|
||||
|
||||
对于Linux上最常用的两个文件系统XFS和ext4,它们有一个特性Quota,那我们就以XFS文件系统为例,学习一下这个Quota概念,然后看看这个特性能不能限制一个目录的使用量。
|
||||
|
||||
### XFS Quota
|
||||
|
||||
在Linux系统里的XFS文件系统缺省都有Quota的特性,这个特性可以为Linux系统里的一个用户(user),一个用户组(group)或者一个项目(project)来限制它们使用文件系统的额度(quota),也就是限制它们可以写入文件系统的文件总量。
|
||||
|
||||
因为我们的目标是要限制一个目录中总体的写入文件数据量,那么显然给用户和用户组限制文件系统的写入数据量的模式,并不适合我们的这个需求。
|
||||
|
||||
因为同一个用户或者用户组可以操作多个目录,多个用户或者用户组也可以操作同一个目录,这样对一个用户或者用户组的限制,就很难用来限制一个目录。
|
||||
|
||||
那排除了限制用户或用户组的模式,我们再来看看Project模式。Project模式是怎么工作的呢?
|
||||
|
||||
我举一个例子你会更好理解,对Linux熟悉的同学可以一边操作,一边体会一下它的工作方式。不熟悉的同学也没关系,可以重点关注我后面的讲解思路。
|
||||
|
||||
首先我们要使用XFS Quota特性,必须在文件系统挂载的时候加上对应的Quota选项,比如我们目前需要配置Project Quota,那么这个挂载参数就是"pquota"。
|
||||
|
||||
对于根目录来说,**这个参数必须作为一个内核启动的参数"rootflags=pquota",这样设置就可以保证根目录在启动挂载的时候,带上XFS Quota的特性并且支持Project模式。**
|
||||
|
||||
我们可以从/proc/mounts信息里,看看根目录是不是带"prjquota"字段。如果里面有这个字段,就可以确保文件系统已经带上了支持project模式的XFS quota特性。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/72/3d/72d653f67717fe047c98fce37156da3d.png" alt="">
|
||||
|
||||
下一步,我们还需要给一个指定的目录打上一个Project ID。这个步骤我们可以使用XFS文件系统自带的工具 [xfs_quota](https://linux.die.net/man/8/xfs_quota) 来完成,然后执行下面的这个命令就可以了。
|
||||
|
||||
执行命令之前,我先对下面的命令和输出做两点解释,让你理解这个命令的含义。
|
||||
|
||||
第一点,新建的目录/tmp/xfs_prjquota,我们想对它做Quota限制。所以在这里要对它打上一个Project ID。
|
||||
|
||||
第二点,通过xfs_quota这条命令,我们给/tmp/xfs_prjquota打上Project ID值101,这个101是我随便选的一个数字,就是个ID标识,你先有个印象。在后面针对Project进行Quota限制的时候,我们还会用到这个ID。
|
||||
|
||||
```
|
||||
# mkdir -p /tmp/xfs_prjquota
|
||||
# xfs_quota -x -c 'project -s -p /tmp/xfs_prjquota 101' /
|
||||
Setting up project 101 (path /tmp/xfs_prjquota)...
|
||||
Processed 1 (/etc/projects and cmdline) paths for project 101 with recursion depth infinite (-1).
|
||||
|
||||
```
|
||||
|
||||
最后,我们还是使用xfs_quota命令,对101(我们刚才建立的这个Project ID)做Quota限制。
|
||||
|
||||
你可以执行下面这条命令,里面的"-p bhard=10m 101"就代表限制101这个project ID,限制它的数据块写入量不能超过10MB。
|
||||
|
||||
```
|
||||
# xfs_quota -x -c 'limit -p bhard=10m 101' /
|
||||
|
||||
```
|
||||
|
||||
做好限制之后,我们可以尝试往/tmp/xfs_prjquota写数据,看看是否可以超过10MB。比如说,我们尝试写入20MB的数据到/tmp/xfs_prjquota里。
|
||||
|
||||
我们可以看到,执行dd写入命令,就会有个出错返回信息"No space left on device"。这表示已经不能再往这个目录下写入数据了,而最后写入数据的文件test.file大小也停留在了10MB。
|
||||
|
||||
```
|
||||
# dd if=/dev/zero of=/tmp/xfs_prjquota/test.file bs=1024 count=20000
|
||||
dd: error writing '/tmp/xfs_prjquota/test.file': No space left on device
|
||||
10241+0 records in
|
||||
10240+0 records out
|
||||
10485760 bytes (10 MB, 10 MiB) copied, 0.0357122 s, 294 MB/s
|
||||
|
||||
# ls -l /tmp/xfs_prjquota/test.file
|
||||
-rw-r--r-- 1 root root 10485760 Oct 31 10:00 /tmp/xfs_prjquota/test.file
|
||||
|
||||
```
|
||||
|
||||
好了,做到这里,我们发现使用XFS Quota的Project模式,确实可以限制一个目录里的写入数据量,它实现的方式其实也不难,就是下面这两步。
|
||||
|
||||
第一步,给目标目录打上一个Project ID,这个ID最终是写到目录对应的inode上。
|
||||
|
||||
这里我解释一下,inode是文件系统中用来描述一个文件或者一个目录的元数据,里面包含文件大小,数据块的位置,文件所属用户/组,文件读写属性以及其他一些属性。
|
||||
|
||||
那么一旦目录打上这个ID之后,在这个目录下的新建的文件和目录也都会继承这个ID。
|
||||
|
||||
第二步,在XFS文件系统中,我们需要给这个project ID设置一个写入数据块的限制。
|
||||
|
||||
有了ID和限制值之后,文件系统就可以统计所有带这个ID文件的数据块大小总和,并且与限制值进行比较。一旦所有文件大小的总和达到限制值,文件系统就不再允许更多的数据写入了。
|
||||
|
||||
用一句话概括,XFS Quota就是通过前面这两步限制了一个目录里写入的数据量。
|
||||
|
||||
## 解决问题
|
||||
|
||||
我们理解了XFS Quota对目录限流的机制之后,再回到我们最开始的问题,如何确保容器不会写满宿主机上的磁盘。
|
||||
|
||||
你应该已经想到了,方法就是**对OverlayFS的upperdir目录做XFS Quota的限流**,没错,就是这个解决办法!
|
||||
|
||||
其实Docker也已经实现了限流功能,也就是用XFS Quota来限制容器的OverlayFS大小。
|
||||
|
||||
我们在用 `docker run` 启动容器的时候,加上一个参数 `--storage-opt size= <SIZE>` ,就能限制住容器OverlayFS文件系统可写入的最大数据量了。
|
||||
|
||||
我们可以一起试一下,这里我们限制的size是10MB。
|
||||
|
||||
进入容器之后,先运行 `df -h` 命令,这时候你可以看到根目录(/)overlayfs文件系统的大小就10MB,而不是我们之前看到的160GB的大小了。这样容器在它的根目录下,最多只能写10MB数据,就不会把宿主机的磁盘给写满了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a7/8a/a7906f56d9d107f0a290e610b8cd6f8a.png" alt="">
|
||||
|
||||
完成了上面这个小试验之后,我们可以再看一下Docker的代码,看看它的实现是不是和我们想的一样。
|
||||
|
||||
Docker里[SetQuota()](https://github.com/moby/moby/blob/19.03/daemon/graphdriver/quota/projectquota.go#L155)函数就是用来实现XFS Quota 限制的,我们可以看到它里面最重要的两步,分别是 `setProjectID` 和 `setProjectQuota` 。
|
||||
|
||||
其实,这两步做的就是我们在基本概念中提到的那两步:
|
||||
|
||||
第一步,给目标目录打上一个Project ID;第二步,为这个Project ID在XFS文件系统中,设置一个写入数据块的限制。
|
||||
|
||||
```
|
||||
// SetQuota - assign a unique project id to directory and set the quota limits
|
||||
// for that project id
|
||||
|
||||
func (q *Control) SetQuota(targetPath string, quota Quota) error {
|
||||
q.RLock()
|
||||
projectID, ok := q.quotas[targetPath]
|
||||
q.RUnlock()
|
||||
|
||||
if !ok {
|
||||
q.Lock()
|
||||
projectID = q.nextProjectID
|
||||
|
||||
//
|
||||
// assign project id to new container directory
|
||||
//
|
||||
|
||||
err := setProjectID(targetPath, projectID)
|
||||
if err != nil {
|
||||
q.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
q.quotas[targetPath] = projectID
|
||||
q.nextProjectID++
|
||||
q.Unlock()
|
||||
}
|
||||
|
||||
|
||||
|
||||
//
|
||||
// set the quota limit for the container's project id
|
||||
//
|
||||
|
||||
logrus.Debugf("SetQuota(%s, %d): projectID=%d", targetPath, quota.Size, projectID)
|
||||
return setProjectQuota(q.backingFsBlockDev, projectID, quota)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
那 `setProjectID` 和 `setProjectQuota` 是如何实现的呢?
|
||||
|
||||
你可以进入到这两个函数里看一下,**它们分别调用了ioctl()和quotactl()这两个系统调用来修改内核中XFS的数据结构,从而完成project ID的设置和Quota值的设置。**具体的细节,我不在这里展开了,如果你有兴趣,可以继续去查看内核中对应的代码。
|
||||
|
||||
好了,Docker里XFS Quota操作的步骤完全和我们先前设想的一样,那么还有最后一个问题要解决,XFS Quota限制的目录是哪一个?
|
||||
|
||||
这个我们可以根据/proc/mounts中容器的OverlayFS Mount信息,再结合Docker的[代码](https://github.com/moby/moby/blob/19.03/daemon/graphdriver/overlay2/overlay.go#L335),就可以知道限制的目录是"/var/lib/docker/overlay2/<docker_id>"。那这个目录下有什么呢?果然upperdir目录中有对应的"diff"目录,就在里面!
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4d/9f/4d4d995f052c9a8e3ff3e413c0e1199f.png" alt="">
|
||||
|
||||
讲到这里,我想你已经清楚了对于使用OverlayFS的容器,我们应该如何去防止它把宿主机的磁盘给写满了吧?**方法就是对OverlayFS的upperdir目录做XFS Quota的限流。**
|
||||
|
||||
## 重点总结
|
||||
|
||||
我们这一讲的问题是,容器写了大量数据到OverlayFS文件系统的根目录,在这个情况下,就会把宿主机的磁盘写满。
|
||||
|
||||
由于OverlayFS自己没有专门的特性,可以限制文件数据写入量。这时我们通过实际试验找到了解决思路:依靠底层文件系统的Quota特性来限制OverlayFS的upperdir目录的大小,这样就能实现限制容器写磁盘的目的。
|
||||
|
||||
底层文件系统XFS Quota的Project模式,能够限制一个目录的文件写入量,这个功能具体是通过这两个步骤实现:
|
||||
|
||||
第一步,给目标目录打上一个Project ID。
|
||||
|
||||
第二步,给这个Project ID在XFS文件系统中设置一个写入数据块的限制。
|
||||
|
||||
Docker正是使用了这个方法,也就是**用XFS Quota来限制OverlayFS的upperdir目录**,通过这个方式控制容器OverlayFS的根目录大小。
|
||||
|
||||
当我们理解了这个方法后,对于不是用Docker启动的容器,比如直接由containerd启动起来的容器,也可以自己实现XFS Quota限制upperdir目录。这样就能有效控制容器对OverlayFS的写数据操作,避免宿主机的磁盘被写满。
|
||||
|
||||
## 思考题
|
||||
|
||||
在正文知识详解的部分,我们使用"xfs_quota"给目录打了project ID并且限制了文件写入的数据量。那在做完这样的限制之后,我们是否能用xfs_quota命令,查询到被限制目录的project ID和限制的数据量呢?
|
||||
|
||||
欢迎你在留言区分享你的思考或疑问。如果这篇文章让你有所收获,也欢迎转发给你的同事、朋友,一起交流和学习。
|
||||
307
极客时间专栏/geek/容器实战高手课/容器存储/13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?.md
Normal file
307
极客时间专栏/geek/容器实战高手课/容器存储/13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?.md
Normal file
@@ -0,0 +1,307 @@
|
||||
<audio id="audio" title="13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/25/9a/256fbea277961ca436358754e1ae0f9a.mp3"></audio>
|
||||
|
||||
你好,我是程远。今天我们聊一聊磁盘读写不稳定的问题。
|
||||
|
||||
上一讲,我给你讲了如何通过XFS Quota来限制容器文件系统的大小,这是静态容量大小的一个限制。
|
||||
|
||||
你也许会马上想到,磁盘除了容量的划分,还有一个读写性能的问题。
|
||||
|
||||
具体来说,就是如果多个容器同时读写节点上的同一块磁盘,那么它们的磁盘读写相互之间影响吗?如果容器之间读写磁盘相互影响,我们有什么办法解决呢?
|
||||
|
||||
接下来,我们就带着问题一起学习今天的内容。
|
||||
|
||||
## 场景再现
|
||||
|
||||
我们先用这里的[代码](https://github.com/chengyli/training/tree/master/filesystem/blkio),运行一下 `make image` 来做一个带fio的容器镜像,fio在我们之前的课程里提到过,它是用来测试磁盘文件系统读写性能的工具。
|
||||
|
||||
有了这个带fio的镜像,我们可以用它启动一个容器,在容器中运行fio,就可以得到只有一个容器读写磁盘时的性能数据。
|
||||
|
||||
```
|
||||
mkdir -p /tmp/test1
|
||||
docker stop fio_test1;docker rm fio_test1
|
||||
docker run --name fio_test1 --volume /tmp/test1:/tmp registery/fio:v1 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -name=/tmp/fio_test1.log
|
||||
|
||||
```
|
||||
|
||||
上面的这个Docker命令,我给你简单地解释一下:在这里我们第一次用到了"--volume"这个参数。之前我们讲过容器文件系统,比如OverlayFS。
|
||||
|
||||
不过容器文件系统并不适合频繁地读写。对于频繁读写的数据,容器需要把他们到放到"volume"中。这里的volume可以是一个本地的磁盘,也可以是一个网络磁盘。
|
||||
|
||||
在这个例子里我们就使用了宿主机本地磁盘,把磁盘上的/tmp/test1目录作为volume挂载到容器的/tmp目录下。
|
||||
|
||||
然后在启动容器之后,我们直接运行fio的命令,这里的参数和我们[第11讲](https://time.geekbang.org/column/article/318173)最开始的例子差不多,只是这次我们运行的是write,也就是写磁盘的操作,而写的目标盘就是挂载到/tmp目录的volume。
|
||||
|
||||
可以看到,fio的运行结果如下图所示,IOPS是18K,带宽(BW)是70MB/s左右。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/a8/54/a8a156d4a543bc02133751a14ba5a354.png" alt="">
|
||||
|
||||
好了,刚才我们模拟了一个容器写磁盘的性能。那么如果这时候有两个容器,都在往同一个磁盘上写数据又是什么情况呢?我们可以再用下面的这个脚本试一下:
|
||||
|
||||
```
|
||||
mkdir -p /tmp/test1
|
||||
mkdir -p /tmp/test2
|
||||
|
||||
docker stop fio_test1;docker rm fio_test1
|
||||
docker stop fio_test2;docker rm fio_test2
|
||||
|
||||
docker run --name fio_test1 --volume /tmp/test1:/tmp registery/fio:v1 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -name=/tmp/fio_test1.log &
|
||||
|
||||
docker run --name fio_test2 --volume /tmp/test2:/tmp registery/fio:v1 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -name=/tmp/fio_test2.log &
|
||||
|
||||
```
|
||||
|
||||
这时候,我们看到的结果,在容器fio_test1里,IOPS是15K左右,带宽是59MB/s了,比之前单独运行的时候性能下降了不少。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cb/64/cb2f19b2da651b03521804e22f14b864.png" alt="">
|
||||
|
||||
显然从这个例子中,我们可以看到多个容器同时写一块磁盘的时候,它的性能受到了干扰。那么有什么办法可以保证每个容器的磁盘读写性能呢?
|
||||
|
||||
之前,我们讨论过用Cgroups来保证容器的CPU使用率,以及控制Memroy的可用大小。那么你肯定想到了,我们是不是也可以用Cgroups来保证每个容器的磁盘读写性能?
|
||||
|
||||
没错,在Cgroup v1中有blkio子系统,它可以来限制磁盘的I/O。不过blkio子系统对于磁盘I/O的限制,并不像CPU,Memory那么直接,下面我会详细讲解。
|
||||
|
||||
## 知识详解
|
||||
|
||||
### Blkio Cgroup
|
||||
|
||||
在讲解blkio Cgroup 前,我们先简单了解一下衡量磁盘性能的**两个常见的指标IOPS和吞吐量(Throughput)**是什么意思,后面讲Blkio Cgroup的参数配置时会用到。
|
||||
|
||||
IOPS是Input/Output Operations Per Second的简称,也就是每秒钟磁盘读写的次数,这个数值越大,当然也就表示性能越好。
|
||||
|
||||
吞吐量(Throughput)是指每秒钟磁盘中数据的读取量,一般以MB/s为单位。这个读取量可以叫作吞吐量,有时候也被称为带宽(Bandwidth)。刚才我们用到的fio显示结果就体现了带宽。
|
||||
|
||||
IOPS和吞吐量之间是有关联的,在IOPS固定的情况下,如果读写的每一个数据块越大,那么吞吐量也越大,它们的关系大概是这样的:吞吐量=数据块大小*IOPS。
|
||||
|
||||
好,那么我们再回到blkio Cgroup这个概念上,blkio Cgroup也是Cgroups里的一个子系统。 在Cgroups v1里,blkio Cgroup的虚拟文件系统挂载点一般在"/sys/fs/cgroup/blkio/"。
|
||||
|
||||
和我之前讲过的CPU,memory Cgroup一样,我们在这个"/sys/fs/cgroup/blkio/"目录下创建子目录作为控制组,再把需要做I/O限制的进程pid写到控制组的cgroup.procs参数中就可以了。
|
||||
|
||||
在blkio Cgroup中,有四个最主要的参数,它们可以用来限制磁盘I/O性能,我列在了下面。
|
||||
|
||||
```
|
||||
blkio.throttle.read_iops_device
|
||||
blkio.throttle.read_bps_device
|
||||
blkio.throttle.write_iops_device
|
||||
blkio.throttle.write_bps_device
|
||||
|
||||
```
|
||||
|
||||
前面我们刚说了磁盘I/O的两个主要性能指标IOPS和吞吐量,在这里,根据这四个参数的名字,估计你已经大概猜到它们的意思了。
|
||||
|
||||
没错,它们分别表示:磁盘读取IOPS限制,磁盘读取吞吐量限制,磁盘写入IOPS限制,磁盘写入吞吐量限制。
|
||||
|
||||
对于每个参数写入值的格式,你可以参考内核[blkio的文档](https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt)。为了让你更好地理解,在这里我给你举个例子。
|
||||
|
||||
如果我们要对一个控制组做限制,限制它对磁盘/dev/vdb的写入吞吐量不超过10MB/s,那么我们对blkio.throttle.write_bps_device参数的配置就是下面这个命令。
|
||||
|
||||
```
|
||||
echo "252:16 10485760" > $CGROUP_CONTAINER_PATH/blkio.throttle.write_bps_device
|
||||
|
||||
```
|
||||
|
||||
在这个命令中,"252:16"是 /dev/vdb的主次设备号,你可以通过 `ls -l /dev/vdb` 看到这两个值,而后面的"10485760"就是10MB的每秒钟带宽限制。
|
||||
|
||||
```
|
||||
# ls -l /dev/vdb -l
|
||||
brw-rw---- 1 root disk 252, 16 Nov 2 08:02 /dev/vdb
|
||||
|
||||
```
|
||||
|
||||
了解了blkio Cgroup的参数配置,我们再运行下面的这个例子,限制一个容器blkio的读写磁盘吞吐量,然后在这个容器里运行一下fio,看看结果是什么。
|
||||
|
||||
```
|
||||
mkdir -p /tmp/test1
|
||||
rm -f /tmp/test1/*
|
||||
|
||||
docker stop fio_test1;docker rm fio_test1
|
||||
|
||||
docker run -d --name fio_test1 --volume /tmp/test1:/tmp registery/fio:v1 sleep 3600
|
||||
|
||||
sleep 2
|
||||
|
||||
CONTAINER_ID=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i fio_test1 | awk '{print $1}')
|
||||
|
||||
echo $CONTAINER_ID
|
||||
|
||||
CGROUP_CONTAINER_PATH=$(find /sys/fs/cgroup/blkio/ -name "*$CONTAINER_ID*")
|
||||
|
||||
echo $CGROUP_CONTAINER_PATH
|
||||
|
||||
# To get the device major and minor id from /dev for the device that /tmp/test1 is on.
|
||||
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH/blkio.throttle.read_bps_device
|
||||
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH/blkio.throttle.write_bps_device
|
||||
|
||||
docker exec fio_test1 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=100MB -numjobs=1 -name=/tmp/fio_test1.log
|
||||
|
||||
docker exec fio_test1 fio -direct=1 -rw=read -ioengine=libaio -bs=4k -size=100MB -numjobs=1 -name=/tmp/fio_test1.log
|
||||
|
||||
```
|
||||
|
||||
在这里,我的机器上/tmp/test1所在磁盘主次设备号是”253:0”,你在自己运行这组命令的时候,需要把主次设备号改成你自己磁盘的对应值。
|
||||
|
||||
还有一点我要提醒一下,不同数据块大小,在性能测试中可以适用于不同的测试目的。但因为这里不是我们要讲的重点,所以为了方便你理解概念,这里就用固定值。
|
||||
|
||||
在我们后面的例子里,fio读写的数据块都固定在4KB。所以对于磁盘的性能限制,我们在blkio Cgroup里就只设置吞吐量限制了。
|
||||
|
||||
在加了blkio Cgroup限制10MB/s后,从fio运行后的输出结果里,我们可以看到这个容器对磁盘无论是读还是写,它的最大值就不会再超过10MB/s了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e2/b3/e26118e821a4b936521eacac924c7db3.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/0a/f5/0ae074c568161d24e57d37d185a47af5.png" alt="">
|
||||
|
||||
在给每个容器都加了blkio Cgroup限制,限制为10MB/s后,即使两个容器同时在一个磁盘上写入文件,那么每个容器的写入磁盘的最大吞吐量,也不会互相干扰了。
|
||||
|
||||
我们可以用下面的这个脚本来验证一下。
|
||||
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
mkdir -p /tmp/test1
|
||||
rm -f /tmp/test1/*
|
||||
docker stop fio_test1;docker rm fio_test1
|
||||
|
||||
mkdir -p /tmp/test2
|
||||
rm -f /tmp/test2/*
|
||||
docker stop fio_test2;docker rm fio_test2
|
||||
|
||||
docker run -d --name fio_test1 --volume /tmp/test1:/tmp registery/fio:v1 sleep 3600
|
||||
docker run -d --name fio_test2 --volume /tmp/test2:/tmp registery/fio:v1 sleep 3600
|
||||
|
||||
sleep 2
|
||||
|
||||
CONTAINER_ID1=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i fio_test1 | awk '{print $1}')
|
||||
echo $CONTAINER_ID1
|
||||
|
||||
CGROUP_CONTAINER_PATH1=$(find /sys/fs/cgroup/blkio/ -name "*$CONTAINER_ID1*")
|
||||
echo $CGROUP_CONTAINER_PATH1
|
||||
|
||||
# To get the device major and minor id from /dev for the device that /tmp/test1 is on.
|
||||
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH1/blkio.throttle.read_bps_device
|
||||
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH1/blkio.throttle.write_bps_device
|
||||
|
||||
CONTAINER_ID2=$(sudo docker ps --format "{{.ID}}\t{{.Names}}" | grep -i fio_test2 | awk '{print $1}')
|
||||
echo $CONTAINER_ID2
|
||||
|
||||
CGROUP_CONTAINER_PATH2=$(find /sys/fs/cgroup/blkio/ -name "*$CONTAINER_ID2*")
|
||||
echo $CGROUP_CONTAINER_PATH2
|
||||
|
||||
# To get the device major and minor id from /dev for the device that /tmp/test1 is on.
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH2/blkio.throttle.read_bps_device
|
||||
|
||||
echo "253:0 10485760" > $CGROUP_CONTAINER_PATH2/blkio.throttle.write_bps_device
|
||||
|
||||
docker exec fio_test1 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=100MB -numjobs=1 -name=/tmp/fio_test1.log &
|
||||
|
||||
docker exec fio_test2 fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=100MB -numjobs=1 -name=/tmp/fio_test2.log &
|
||||
|
||||
```
|
||||
|
||||
我们还是看看fio运行输出的结果,这时候,fio_test1和fio_test2两个容器里执行的结果都是10MB/s了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/67/a9/6719cc30a8e2933dae1ba6f96235e4a9.png" alt=""><br>
|
||||
<img src="https://static001.geekbang.org/resource/image/de/c8/de4be66c72ff9e4cdc5007fe71f848c8.png" alt="">
|
||||
|
||||
那么做到了这一步,我们是不是就可以认为,blkio Cgroup可以完美地对磁盘I/O做限制了呢?
|
||||
|
||||
你先别急,我们可以再做个试验,把前面脚本里fio命令中的 “-direct=1” 给去掉,也就是不让fio运行在Direct I/O模式了,而是用Buffered I/O模式再运行一次,看看fio执行的输出。
|
||||
|
||||
同时我们也可以运行iostat命令,查看实际的磁盘写入速度。
|
||||
|
||||
这时候你会发现,即使我们设置了blkio Cgroup,也根本不能限制磁盘的吞吐量了。
|
||||
|
||||
### Direct I/O 和 Buffered I/O
|
||||
|
||||
为什么会这样的呢?这就要提到Linux的两种文件I/O模式了:Direct I/O和Buffered I/O。
|
||||
|
||||
Direct I/O 模式,用户进程如果要写磁盘文件,就会通过Linux内核的文件系统层(filesystem) -> 块设备层(block layer) -> 磁盘驱动 -> 磁盘硬件,这样一路下去写入磁盘。
|
||||
|
||||
而如果是Buffered I/O模式,那么用户进程只是把文件数据写到内存中(Page Cache)就返回了,而Linux内核自己有线程会把内存中的数据再写入到磁盘中。**在Linux里,由于考虑到性能问题,绝大多数的应用都会使用Buffered I/O模式。**
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/10/46/1021f5f7ec700f3c7c66cbf8e07b1a46.jpeg" alt="">
|
||||
|
||||
我们通过前面的测试,发现Direct I/O可以通过blkio Cgroup来限制磁盘I/O,但是Buffered I/O不能被限制。
|
||||
|
||||
那通过上面的两种I/O模式的解释,你是不是可以想到原因呢?是的,原因就是被Cgroups v1的架构限制了。
|
||||
|
||||
我们已经学习过了v1 的CPU Cgroup,memory Cgroup和blkio Cgroup,那么Cgroup v1的一个整体结构,你应该已经很熟悉了。它的每一个子系统都是独立的,资源的限制只能在子系统中发生。
|
||||
|
||||
就像下面图里的进程pid_y,它可以分别属于memory Cgroup和blkio Cgroup。但是在blkio Cgroup对进程pid_y做磁盘I/O做限制的时候,blkio子系统是不会去关心pid_y用了哪些内存,哪些内存是不是属于Page Cache,而这些Page Cache的页面在刷入磁盘的时候,产生的I/O也不会被计算到进程pid_y上面。
|
||||
|
||||
就是这个原因,导致了blkio 在Cgroups v1里不能限制Buffered I/O。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/32/ba/32c69a6f69c4ce7f11c842450fe7d9ba.jpeg" alt="">
|
||||
|
||||
这个Buffered I/O限速的问题,在Cgroup V2里得到了解决,其实这个问题也是促使Linux开发者重新设计Cgroup V2的原因之一。
|
||||
|
||||
## Cgroup V2
|
||||
|
||||
Cgroup v2相比Cgroup v1做的最大的变动就是一个进程属于一个控制组,而每个控制组里可以定义自己需要的多个子系统。
|
||||
|
||||
比如下面的Cgroup V2示意图里,进程pid_y属于控制组group2,而在group2里同时打开了io和memory子系统 (Cgroup V2里的io子系统就等同于Cgroup v1里的blkio子系统)。
|
||||
|
||||
那么,Cgroup对进程pid_y的磁盘 I/O做限制的时候,就可以考虑到进程pid_y写入到Page Cache内存的页面了,这样buffered I/O的磁盘限速就实现了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8a/46/8ae3f3282b9f19720b764c696959bf46.jpeg" alt="">
|
||||
|
||||
下面我们在Cgroup v2里,尝试一下设置了blkio Cgroup+Memory Cgroup之后,是否可以对Buffered I/O进行磁盘限速。
|
||||
|
||||
我们要做的第一步,就是在Linux系统里打开Cgroup v2的功能。因为目前即使最新版本的Ubuntu Linux或者Centos Linux,仍然在使用Cgroup v1作为缺省的Cgroup。
|
||||
|
||||
打开方法就是配置一个kernel参数"cgroup_no_v1=blkio,memory",这表示把Cgroup v1的blkio和Memory两个子系统给禁止,这样Cgroup v2的io和Memory这两个子系统就打开了。
|
||||
|
||||
我们可以把这个参数配置到grub中,然后我们重启Linux机器,这时Cgroup v2的 io还有Memory这两个子系统,它们的功能就打开了。
|
||||
|
||||
系统重启后,我们会看到Cgroup v2的虚拟文件系统被挂载到了 /sys/fs/cgroup/unified目录下。
|
||||
|
||||
然后,我们用下面的这个脚本做Cgroup v2 io的限速配置,并且运行fio,看看buffered I/O是否可以被限速。
|
||||
|
||||
```
|
||||
# Create a new control group
|
||||
mkdir -p /sys/fs/cgroup/unified/iotest
|
||||
|
||||
# enable the io and memory controller subsystem
|
||||
echo "+io +memory" > /sys/fs/cgroup/unified/cgroup.subtree_control
|
||||
|
||||
# Add current bash pid in iotest control group.
|
||||
# Then all child processes of the bash will be in iotest group too,
|
||||
# including the fio
|
||||
echo $$ >/sys/fs/cgroup/unified/iotest/cgroup.procs
|
||||
|
||||
# 256:16 are device major and minor ids, /mnt is on the device.
|
||||
echo "252:16 wbps=10485760" > /sys/fs/cgroup/unified/iotest/io.max
|
||||
cd /mnt
|
||||
#Run the fio in non direct I/O mode
|
||||
fio -iodepth=1 -rw=write -ioengine=libaio -bs=4k -size=1G -numjobs=1 -name=./fio.test
|
||||
|
||||
```
|
||||
|
||||
在这个例子里,我们建立了一个名叫iotest的控制组,并且在这个控制组里加入了io和Memory两个控制子系统,对磁盘最大吞吐量的设置为10MB。运行fio的时候不加"-direct=1",也就是让fio运行在buffered I/O模式下。
|
||||
|
||||
运行fio写入1GB的数据后,你会发现fio马上就执行完了,因为系统上有足够的内存,fio把数据写入内存就返回了,不过只要你再运行”iostat -xz 10” 这个命令,你就可以看到磁盘vdb上稳定的写入速率是10240wkB/s,也就是我们在io Cgroup里限制的10MB/s。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/81/c6/8174902114e01369193945891d054cc6.png" alt="">
|
||||
|
||||
看到这个结果,我们证实了Cgoupv2 io+Memory两个子系统一起使用,就可以对buffered I/O控制磁盘写入速率。
|
||||
|
||||
## 重点总结
|
||||
|
||||
这一讲,我们主要想解决的问题是如何保证容器读写磁盘速率的稳定,特别是当多个容器同时读写同一个磁盘的时候,需要减少相互的干扰。
|
||||
|
||||
Cgroup V1的blkiio控制子系统,可以用来限制容器中进程的读写的IOPS和吞吐量(Throughput),但是它只能对于Direct I/O的读写文件做磁盘限速,对Buffered I/O的文件读写,它无法进行磁盘限速。
|
||||
|
||||
**这是因为Buffered I/O会把数据先写入到内存Page Cache中,然后由内核线程把数据写入磁盘,而Cgroup v1 blkio的子系统独立于memory 子系统,无法统计到由Page Cache刷入到磁盘的数据量。**
|
||||
|
||||
这个Buffered I/O无法被限速的问题,在Cgroup v2里被解决了。Cgroup v2从架构上允许一个控制组里有多个子系统协同运行,这样在一个控制组里只要同时有io和Memory子系统,就可以对Buffered I/O 作磁盘读写的限速。
|
||||
|
||||
虽然Cgroup v2 解决了Buffered I/O 磁盘读写限速的问题,但是在现实的容器平台上也不是能够立刻使用的,还需要等待一段时间。目前从runC、containerd到Kubernetes都是刚刚开始支持Cgroup v2,而对生产环境中原有运行Cgroup v1的节点要迁移转化成Cgroup v2需要一个过程。
|
||||
|
||||
## 思考题
|
||||
|
||||
最后呢,我给你留一道思考题。 其实这是一道操作题,通过这个操作你可以再理解一下 blkio Cgroup与 Buffered I/O的关系。
|
||||
|
||||
在Cgroup v1的环境里,我们在blkio Cgroup v1的例子基础上,把 fio 中"direct=1"参数去除之后,再运行fio,同时运行iostat查看实际写入磁盘的速率,确认Cgroup v1 blkio无法对Buffered I/O限速。
|
||||
|
||||
欢迎你在留言区分享你的收获和疑问。如果这篇文章给你带来了启发,也欢迎转发给你的朋友,一起学习和交流。
|
||||
206
极客时间专栏/geek/容器实战高手课/容器存储/14 | 容器中的内存与I|O:容器写文件的延时为什么波动很大?.md
Normal file
206
极客时间专栏/geek/容器实战高手课/容器存储/14 | 容器中的内存与I|O:容器写文件的延时为什么波动很大?.md
Normal file
@@ -0,0 +1,206 @@
|
||||
<audio id="audio" title="14 | 容器中的内存与I/O:容器写文件的延时为什么波动很大?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ab/9b/ab4c658511497d5e94fd9a7bf828b09b.mp3"></audio>
|
||||
|
||||
你好,我是程远。这一讲,我们继续聊一聊容器中写文件性能波动的问题。
|
||||
|
||||
你应该还记得,我们[上一讲](https://time.geekbang.org/column/article/320123)中讲过Linux中的两种I/O模式,Direct I/O和Buffered I/O。
|
||||
|
||||
对于Linux的系统调用write()来说,Buffered I/O是缺省模式,使用起来比较方便,而且从用户角度看,在大多数的应用场景下,用Buffered I/O的write()函数调用返回要快一些。所以,Buffered I/O在程序中使用得更普遍一些。
|
||||
|
||||
当使用Buffered I/O的应用程序从虚拟机迁移到容器,这时我们就会发现多了Memory Cgroup的限制之后,write()写相同大小的数据块花费的时间,延时波动会比较大。
|
||||
|
||||
这是怎么回事呢?接下来我们就带着问题开始今天的学习。
|
||||
|
||||
## 问题再现
|
||||
|
||||
我们可以先动手写一个[小程序](https://github.com/chengyli/training/blob/master/filesystem/writeback/bin/test_iowrite),用来模拟刚刚说的现象。
|
||||
|
||||
这个小程序我们这样来设计:从一个文件中每次读取一个64KB大小的数据块,然后写到一个新文件中,它可以不断读写10GB大小的数据。同时我们在这个小程序中做个记录,记录写每个64KB的数据块需要花费的时间。
|
||||
|
||||
我们可以先在虚拟机里直接运行,虚拟机里内存大小是大于10GB的。接着,我们把这个程序放到容器中运行,因为这个程序本身并不需要很多的内存,我们给它做了一个Memory Cgroup的内存限制,设置为1GB。
|
||||
|
||||
运行结束后,我们比较一下程序写数据块的时间。我把结果画了一张图,图里的纵轴是时间,单位us;横轴是次数,在这里我们记录了96次。图中橘红色的线是在容器里运行的结果,蓝色的线是在虚拟机上运行的结果。
|
||||
|
||||
结果很明显,在容器中写入数据块的时间会时不时地增高到200us;而在虚拟机里的写入数据块时间就比较平稳,一直在30~50us这个范围内。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/7c/c0/7c494f4bc587b618f4b7db3db9ce4ac0.jpg" alt="">
|
||||
|
||||
通过这个小程序,我们再现了问题,那我们就来分析一下,为什么会产生这样的结果。
|
||||
|
||||
## 时间波动是因为Dirty Pages的影响么?
|
||||
|
||||
我们对文件的写入操作是Buffered I/O。在前一讲中,我们其实已经知道了,对于Buffer I/O,用户的数据是先写入到Page Cache里的。而这些写入了数据的内存页面,在它们没有被写入到磁盘文件之前,就被叫作dirty pages。
|
||||
|
||||
Linux内核会有专门的内核线程(每个磁盘设备对应的kworker/flush 线程)把dirty pages写入到磁盘中。那我们自然会这样猜测,也许是Linux内核对dirty pages的操作影响了Buffered I/O的写操作?
|
||||
|
||||
想要验证这个想法,我们需要先来看看dirty pages是在什么时候被写入到磁盘的。这里就要用到**/proc/sys/vm里和dirty page相关的内核参数**了,我们需要知道所有相关参数的含义,才能判断出最后真正导致问题发生的原因。
|
||||
|
||||
现在我们挨个来看一下。为了方便后面的讲述,我们可以设定一个比值A,**A等于dirty pages的内存/节点可用内存*100%**。
|
||||
|
||||
第一个参数,dirty_background_ratio,这个参数里的数值是一个百分比值,缺省是10%。如果比值A大于dirty_background_ratio的话,比如大于默认的10%,内核flush线程就会把dirty pages刷到磁盘里。
|
||||
|
||||
第二个参数,是和dirty_background_ratio相对应一个参数,也就是dirty_background_bytes,它和dirty_background_ratio作用相同。区别只是dirty_background_bytes是具体的字节数,它用来定义的是dirty pages内存的临界值,而不是比例值。
|
||||
|
||||
这里你还要注意,dirty_background_ratio和 dirty_background_bytes只有一个可以起作用,如果你给其中一个赋值之后,另外一个参数就归0了。
|
||||
|
||||
接下来我们看第三个参数,dirty_ratio,这个参数的数值也是一个百分比值,缺省是20%。
|
||||
|
||||
如果比值A,大于参数dirty_ratio的值,比如大于默认设置的20%,这时候正在执行Buffered I/O写文件的进程就会被阻塞住,直到它写的数据页面都写到磁盘为止。
|
||||
|
||||
同样,第四个参数dirty_bytes与dirty_ratio相对应,它们的关系和dirty_background_ratio与dirty_background_bytes一样。我们给其中一个赋值后,另一个就会归零。
|
||||
|
||||
然后我们来看dirty_writeback_centisecs,这个参数的值是个时间值,以百分之一秒为单位,缺省值是500,也就是5秒钟。它表示每5秒钟会唤醒内核的flush线程来处理dirty pages。
|
||||
|
||||
最后还有dirty_expire_centisecs,这个参数的值也是一个时间值,以百分之一秒为单位,缺省值是3000,也就是30秒钟。它定义了dirty page在内存中存放的最长时间,如果一个dirty page超过这里定义的时间,那么内核的flush线程也会把这个页面写入磁盘。
|
||||
|
||||
好了,从这些dirty pages相关的参数定义,你会想到些什么呢?
|
||||
|
||||
进程写操作上的时间波动,只有可能是因为dirty pages的数量很多,已经达到了第三个参数dirty_ratio的值。这时执行写文件功能的进程就会被暂停,直到写文件的操作将数据页面写入磁盘,写文件的进程才能继续运行,所以进程里一次写文件数据块的操作时间会增加。
|
||||
|
||||
刚刚说的是我们的推理,那情况真的会是这样吗?其实我们可以在容器中进程不断写入数据的时候,查看节点上dirty pages的实时数目。具体操作如下:
|
||||
|
||||
```
|
||||
watch -n 1 "cat /proc/vmstat | grep dirty"
|
||||
|
||||
```
|
||||
|
||||
当我们的节点可用内存是12GB的时候,假设dirty_ratio是20%,dirty_background_ratio是10%,那么我们在1GB memory容器中写10GB的数据,就会看到它实时的dirty pages数目,也就是/ proc/vmstat里的nr_dirty的数值,这个数值对应的内存并不能达到dirty_ratio所占的内存值。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/cc/68/ccd0b41e3bd9420c539942b84d88f968.png" alt="">
|
||||
|
||||
其实我们还可以再做个实验,就是在dirty_bytes和dirty_background_bytes里写入一个很小的值。
|
||||
|
||||
```
|
||||
echo 8192 > /proc/sys/vm/dirty_bytes
|
||||
echo 4096 > /proc/sys/vm/dirty_background_bytes
|
||||
|
||||
```
|
||||
|
||||
然后再记录一下容器程序里每写入64KB数据块的时间,这时候,我们就会看到,时不时一次写入的时间就会达到9ms,这已经远远高于我们之前看到的200us了。
|
||||
|
||||
因此,我们知道了这个时间的波动,并不是强制把dirty page写入到磁盘引起的。
|
||||
|
||||
## 调试问题
|
||||
|
||||
那接下来,我们还能怎么分析这个问题呢?
|
||||
|
||||
我们可以用perf和ftrace这两个工具,对容器里写数据块的进程做个profile,看看到底是调用哪个函数花费了比较长的时间。顺便说一下,我们在专题加餐里会专门介绍如何使用perf、ftrace等工具以及它们的工作原理,在这里你只要了解我们的调试思路就行。
|
||||
|
||||
怎么使用这两个工具去定位耗时高的函数呢?我大致思路是这样的:我们发现容器中的进程用到了write()这个函数调用,然后写64KB数据块的时间增加了,而write()是一个系统调用,那我们需要进行下面这两步操作。
|
||||
|
||||
**第一步,我们要找到内核中write()这个系统调用函数下,又调用了哪些子函数。**想找出主要的子函数我们可以查看代码,也可以用perf这个工具来得到。
|
||||
|
||||
然后是**第二步,得到了write()的主要子函数之后,我们可以用ftrace这个工具来trace这些函数的执行时间,这样就可以找到花费时间最长的函数了。**
|
||||
|
||||
好,下面我们就按照刚才梳理的思路来做一下。首先是第一步,我们在容器启动写磁盘的进程后,在宿主机上得到这个进程的pid,然后运行下面的perf命令。
|
||||
|
||||
```
|
||||
perf record -a -g -p <pid>
|
||||
|
||||
```
|
||||
|
||||
等写磁盘的进程退出之后,这个perf record也就停止了。
|
||||
|
||||
这时我们再执行 `perf report` 查看结果。把vfs_write()函数展开之后,我们就可以看到,write()这个系统调用下面的调用到了哪些主要的子函数,到这里第一步就完成了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/91/9d/9191caa5db8c0afe2363540bc31e1d9d.png" alt="">
|
||||
|
||||
下面再来做第二步,我们把主要的函数写入到ftrace的set_ftrace_filter里,然后把ftrace的tracer设置为function_graph,并且打开tracing_on开启追踪。
|
||||
|
||||
```
|
||||
# cd /sys/kernel/debug/tracing
|
||||
# echo vfs_write >> set_ftrace_filter
|
||||
# echo xfs_file_write_iter >> set_ftrace_filter
|
||||
# echo xfs_file_buffered_aio_write >> set_ftrace_filter
|
||||
# echo iomap_file_buffered_write
|
||||
# echo iomap_file_buffered_write >> set_ftrace_filter
|
||||
# echo pagecache_get_page >> set_ftrace_filter
|
||||
# echo try_to_free_mem_cgroup_pages >> set_ftrace_filter
|
||||
# echo try_charge >> set_ftrace_filter
|
||||
# echo mem_cgroup_try_charge >> set_ftrace_filter
|
||||
|
||||
# echo function_graph > current_tracer
|
||||
# echo 1 > tracing_on
|
||||
|
||||
```
|
||||
|
||||
这些设置完成之后,我们再运行一下容器中的写磁盘程序,同时从ftrace的trace_pipe中读取出追踪到的这些函数。
|
||||
|
||||
这时我们可以看到,当需要申请Page Cache页面的时候,write()系统调用会反复地调用mem_cgroup_try_charge(),并且在释放页面的时候,函数do_try_to_free_pages()花费的时间特别长,有50+us(时间单位,micro-seconds)这么多。
|
||||
|
||||
```
|
||||
1) | vfs_write() {
|
||||
1) | xfs_file_write_iter [xfs]() {
|
||||
1) | xfs_file_buffered_aio_write [xfs]() {
|
||||
1) | iomap_file_buffered_write() {
|
||||
1) | pagecache_get_page() {
|
||||
1) | mem_cgroup_try_charge() {
|
||||
1) 0.338 us | try_charge();
|
||||
1) 0.791 us | }
|
||||
1) 4.127 us | }
|
||||
…
|
||||
|
||||
1) | pagecache_get_page() {
|
||||
1) | mem_cgroup_try_charge() {
|
||||
1) | try_charge() {
|
||||
1) | try_to_free_mem_cgroup_pages() {
|
||||
1) + 52.798 us | do_try_to_free_pages();
|
||||
1) + 53.958 us | }
|
||||
1) + 54.751 us | }
|
||||
1) + 55.188 us | }
|
||||
1) + 56.742 us | }
|
||||
…
|
||||
1) ! 109.925 us | }
|
||||
1) ! 110.558 us | }
|
||||
1) ! 110.984 us | }
|
||||
1) ! 111.515 us | }
|
||||
|
||||
```
|
||||
|
||||
看到这个ftrace的结果,你是不是会想到,我们在容器内存[那一讲](https://time.geekbang.org/column/article/316436)中提到的Page Cahe呢?
|
||||
|
||||
是的,这个问题的确和Page Cache有关,Linux会把所有的空闲内存利用起来,一旦有Buffered I/O,这些内存都会被用作Page Cache。
|
||||
|
||||
当容器加了Memory Cgroup限制了内存之后,对于容器里的Buffered I/O,就只能使用容器中允许使用的最大内存来做Page Cache。
|
||||
|
||||
**那么如果容器在做内存限制的时候,Cgroup中memory.limit_in_bytes设置得比较小,而容器中的进程又有很大量的I/O,这样申请新的Page Cache内存的时候,又会不断释放老的内存页面,这些操作就会带来额外的系统开销了。**
|
||||
|
||||
## 重点总结
|
||||
|
||||
我们今天讨论的问题是在容器中用Buffered I/O方式写文件的时候,会出现写入时间波动的问题。
|
||||
|
||||
由于这是Buffered I/O方式,对于写入文件会先写到内存里,这样就产生了dirty pages,所以我们先研究了一下Linux对dirty pages的回收机制是否会影响到容器中写入数据的波动。
|
||||
|
||||
在这里我们最主要的是理解这两个参数,**dirty_background_ratio 和 dirty_ratio**,这两个值都是相对于节点可用内存的百分比值。
|
||||
|
||||
**当dirty pages数量超过dirty_background_ratio对应的内存量的时候,内核flush线程就会开始把dirty pages写入磁盘; 当dirty pages数量超过dirty_ratio对应的内存量,这时候程序写文件的函数调用write()就会被阻塞住,直到这次调用的dirty pages全部写入到磁盘。**
|
||||
|
||||
在节点是大内存容量,并且dirty_ratio为系统缺省值20%,dirty_background_ratio是系统缺省值10%的情况下,我们通过观察 /proc/vmstat中的nr_dirty数值可以发现,dirty pages不会阻塞进程的Buffered I/O写文件操作。
|
||||
|
||||
所以我们做了另一种尝试,使用perf和ftrace工具对容器中的写文件进程进行profile。我们用perf得到了系统调用write()在内核中的一系列子函数调用,再用ftrace来查看这些子函数的调用时间。
|
||||
|
||||
**根据ftrace的结果,我们发现写数据到Page Cache的时候,需要不断地去释放原有的页面,这个时间开销是最大的。造成容器中Buffered I/O write()不稳定的原因,正是容器在限制内存之后,Page Cache的数量较小并且不断申请释放。**
|
||||
|
||||
其实这个问题也提醒了我们:在对容器做Memory Cgroup限制内存大小的时候,不仅要考虑容器中进程实际使用的内存量,还要考虑容器中程序I/O的量,合理预留足够的内存作为Buffered I/O 的Page Cache。
|
||||
|
||||
比如,如果知道需要反复读写文件的大小,并且在内存足够的情况下,那么Memory Cgroup的内存限制可以超过这个文件的大小。
|
||||
|
||||
还有一个解决思路是,我们在程序中自己管理文件的cache并且调用Direct I/O来读写文件,这样才会对应用程序的性能有一个更好的预期。
|
||||
|
||||
## 思考题
|
||||
|
||||
我们对 dirty_bytes 和 dirty_background_bytes做下面的设置:
|
||||
|
||||
```
|
||||
-bash-4.2# echo 8192 > /proc/sys/vm/dirty_bytes
|
||||
-bash-4.2# echo 4096 > /proc/sys/vm/dirty_background_bytes
|
||||
|
||||
```
|
||||
|
||||
然后再运行下面的fio测试,得到的结果和缺省dirty_*配置的时候会有差别吗?
|
||||
|
||||
```
|
||||
# fio -direct=1 -iodepth=64 -rw=write -ioengine=libaio -bs=4k -size=10G -numjobs=1 -name=./fio.test
|
||||
|
||||
```
|
||||
|
||||
欢迎你在留言区提出你的思考或是疑问。如果这篇文章对你有帮助的话,也欢迎你分享给你的朋友、同事,一起学习进步。
|
||||
Reference in New Issue
Block a user