1. blkio Cgroup
磁盘除了容量的划分之外,还有一个读写性能的问题。多个容器同时读写节点上的同一块磁盘,这些磁盘读写之间会相互影响,这个时候就希望 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,磁盘读取 IOPS 限制
- blkio.throttle.read_bps_device,磁盘读取吞吐量限制
- blkio.throttle.write_iops_device,磁盘写入 IOPS 限制
- blkio.throttle.write_iops_device,磁盘写入吞吐量限制
每个参数写入值的格式,你可以参考内核 blkio 的文档:https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt。
下面举个例子,如果我们要对一个控制组做限制,限制它对磁盘 /dev/vdb 的写入吞吐量不超过 10MB/s,那么我们对这个控制组的文件 blkio.throttle.write_bps_device 进行如下配置即可。在这个命令中,"252:16"是 /dev/vdb 的主次设备号,你可以通过 ls -l /dev/vdb 看到这两个值,而后面的"10485760"就是 10MB 的每秒钟带宽限制。
$ echo "252:16 10485760" > $CGROUP_CONTAINER_PATH/blkio.throttle.write_bps_device
$ ls -l /dev/vdb -l
brw-rw---- 1 root disk 252, 16 Nov 2 08:02 /dev/vdb
那么之后在使用这个控制的容器中执行,就会发现带宽限制是 10MB。
$ fio -direct=1 -rw=write -ioengine=libaio -bs=4k -size=100MB -numjobs=1 -name=/tmp/fio_test1.log
需要注意的,Cgroup V1 的 blkiio 控制子系统,可以用来限制容器中进程的读写的 IOPS 和吞吐量(Throughput),但是它只能对于 Direct I/O 的读写文件做磁盘限速,对 Buffered I/O 的文件读写,它无法进行磁盘限速。
假如,把 fio 命令中的 “-direct=1” 给去掉,也就是不让 fio 运行在 Direct I/O 模式了,而是用 Buffered I/O 模式再运行一次。同时可以运行 iostat 命令,查看实际的磁盘写入速度,你会发现这个时候 blkio cgroup 已经不能限制磁盘的吞吐量了。
这是主要是因为它被 Cgroups v1 的架构限制了。Cgroup v1 中,它的每一个子系统都是独立的,资源限制只能在子系统中发生。针对上述例子来说,Buffered I/O 会把数据先写入到内存 Page Cache 中,然后由内核线程把数据写入磁盘,但是 Cgroup v1 blkio 的子系统独立于 memory 子系统,无法统计到由 Page Cache 刷入到磁盘的数据量。
就像下图中进程 pid-y 分别属于 memory Cgroup 和 blkio Cgroup,但是在 blkio Cgroup 对进程做磁盘 IO 做限制的时候,blkio 子系统无法知道 pid-y 用了哪些内存,哪些内存不是属于 Page Cache,也就无法统计到由 Page Cache 刷入到磁盘的数据量。所以,这导致了在使用 Buffered IO 的情况下,blkio cgroup V1 无法控制磁盘 IO。
关于 buffered IO 限速的问题,在 Cgroup V2 那里得到了解决,其实这个问题也是促使 Linux 开发者重新设计 Cgroup V2 的原因之一。
1.1. Cgroup V2 可限制 Buffered IO 的读写
要想使用 Cgroup V2,需要在 Linux 系统里打开 Cgroup V2 的功能。因为目前即使最新版本的 Ubuntu Linux 或者 Centos Linux,仍然在使用 Cgroup v1 作为缺省的 Cgroup。
Cgroup V2 相比 Cgroup V1 做的最大的变动就是在 V2 中一个进程属于一个控制组,而每个控制组里可以定义自己需要的多个子系统。比如下面的 Cgroup V2 示意图里,进程 pid_y 属于控制组 group2,而在 group2 里同时打开了 io 和 memory 子系统 (Cgroup V2 里的 io 子系统就等同于 Cgroup v1 里的 blkio 子系统)。
Cgroup v2 从架构上允许一个控制组里有多个子系统协同运行。这样,如果一个控制组里同时设置了 io 和 Memory 子系统,那么就可以对 Buffered I/O 作磁盘读写的限速。为什么这样协作之后可以限制?可以这么理解,协作之后 io cgroup 知道了对应的 page cache 是属于这个进程的,那么内核同步这些 page cache 时所产生的 IO 会被计算到该进程的 IO 中。
虽然 Cgroup v2 解决了 Buffered I/O 磁盘读写限速的问题,但是在现实的容器平台上也不是能够立刻使用的,还需要等待一段时间。目前从 runC、containerd 到 Kubernetes 都是刚刚开始支持 Cgroup v2,而对生产环境中原有运行 Cgroup v1 的节点要迁移转化成 Cgroup v2 需要一个过程。
2. blkio Cgroup 相关问题
2.1. 容器写文件的延时为什么波动很大?
当使用 Buffered I/O 的应用程序从虚拟机迁移到容器,这时我们就会发现多了 Memory Cgroup 的限制之后,write() 写相同大小的数据块花费的时间,延时波动会比较大。
这是因为如果给容器做了内存限制,并且 Cgroup 中 memory.limit_in_bytes 设置得比较小,而容器中的进程又有很大量的 IO 时,那么这些文件写操作会缺省采用 buffered IO 的方式,也就是会往 page cache 中写。由于存在大量 IO,page cache 已经被之前的写操作占用了,所以现在要写的话,又需要不断释放老的内存页面,而这个时间开销是很大的。
因此在对容器做 Memory Cgroup 限制内存大小的时候,不仅要考虑容器中进程实际使用的内存量,还要考虑容器中程序 IO 的量,合理预留足够的内存作为 Buffered IO 的 Page Cache。还有一个解决思路是,我们在程序中自己管理文件的 cache 并且调用 Direct I/O 来读写文件,这样才会对应用程序的性能有一个更好的预期。
巨人的肩膀
- 极客时间.《容器实战》