gitbook/容器实战高手课/docs/320123.md
2022-09-03 22:05:03 +08:00

308 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?
你好,我是程远。今天我们聊一聊磁盘读写不稳定的问题。
上一讲,我给你讲了如何通过XFS Quota来限制容器文件系统的大小,这是静态容量大小的一个限制。
你也许会马上想到,磁盘除了容量的划分,还有一个读写性能的问题。
具体来说,就是如果多个容器同时读写节点上的同一块磁盘,那么它们的磁盘读写相互之间影响吗?如果容器之间读写磁盘相互影响,我们有什么办法解决呢?
接下来,我们就带着问题一起学习今天的内容。
## 场景再现
我们先用这里的[代码](https://github.com/chengyli/training/tree/master/filesystem/blkio),运行一下 `make image` 来做一个带fio的容器镜像,fio在我们之前的课程里提到过,它是用来测试磁盘文件系统读写性能的工具。
有了这个带fio的镜像,我们可以用它启动一个容器,在容器中运行fio,就可以得到只有一个容器读写磁盘时的性能数据。
```shell
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左右。
![](https://static001.geekbang.org/resource/image/a8/54/a8a156d4a543bc02133751a14ba5a354.png)
好了,刚才我们模拟了一个容器写磁盘的性能。那么如果这时候有两个容器,都在往同一个磁盘上写数据又是什么情况呢?我们可以再用下面的这个脚本试一下:
```shell
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了,比之前单独运行的时候性能下降了不少。
![](https://static001.geekbang.org/resource/image/cb/64/cb2f19b2da651b03521804e22f14b864.png)
显然从这个例子中,我们可以看到多个容器同时写一块磁盘的时候,它的性能受到了干扰。那么有什么办法可以保证每个容器的磁盘读写性能呢?
之前,我们讨论过用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参数的配置就是下面这个命令。
```shell
echo "252:16 10485760" > $CGROUP_CONTAINER_PATH/blkio.throttle.write_bps_device
```
在这个命令中,"252:16"是 /dev/vdb的主次设备号,你可以通过 `ls -l /dev/vdb` 看到这两个值,而后面的"10485760"就是10MB的每秒钟带宽限制。
```shell
# ls -l /dev/vdb -l
brw-rw---- 1 root disk 252, 16 Nov 2 08:02 /dev/vdb
```
了解了blkio Cgroup的参数配置,我们再运行下面的这个例子,限制一个容器blkio的读写磁盘吞吐量,然后在这个容器里运行一下fio,看看结果是什么。
```shell
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了。
![](https://static001.geekbang.org/resource/image/e2/b3/e26118e821a4b936521eacac924c7db3.png)
![](https://static001.geekbang.org/resource/image/0a/f5/0ae074c568161d24e57d37d185a47af5.png)
在给每个容器都加了blkio Cgroup限制,限制为10MB/s后,即使两个容器同时在一个磁盘上写入文件,那么每个容器的写入磁盘的最大吞吐量,也不会互相干扰了。
我们可以用下面的这个脚本来验证一下。
```shell
#!/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了。
![](https://static001.geekbang.org/resource/image/67/a9/6719cc30a8e2933dae1ba6f96235e4a9.png)
![](https://static001.geekbang.org/resource/image/de/c8/de4be66c72ff9e4cdc5007fe71f848c8.png)
那么做到了这一步,我们是不是就可以认为,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模式。**
![](https://static001.geekbang.org/resource/image/10/46/1021f5f7ec700f3c7c66cbf8e07b1a46.jpeg)
我们通过前面的测试,发现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。
![](https://static001.geekbang.org/resource/image/32/ba/32c69a6f69c4ce7f11c842450fe7d9ba.jpeg)
这个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的磁盘限速就实现了。
![](https://static001.geekbang.org/resource/image/8a/46/8ae3f3282b9f19720b764c696959bf46.jpeg)
下面我们在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是否可以被限速。
```shell
# 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。
![](https://static001.geekbang.org/resource/image/81/c6/8174902114e01369193945891d054cc6.png)
看到这个结果,我们证实了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限速。
欢迎你在留言区分享你的收获和疑问。如果这篇文章给你带来了启发,也欢迎转发给你的朋友,一起学习和交流。