该文完稿于 2020-03-13 凌晨
Background
最近在做一个与 Docker 相关的实验,其中需要限制 Docker 容器中应用程序的 IO,比如 NginX 的 IO。这听起来很简单,毕竟远在 Feb 4th, 2016 release 的 Docker v1.10 就在其功能中加入了限制容器 IO 的参数
Constraints on disk I/O: Various options for setting constraints on disk I/O have been added to
docker run
:--device-read-bps
,--device-write-bps
,--device-read-iops
,--device-write-iops
, and--blkio-weight-device
.
就在一切都顺利进行,我写完包含了 NginX
的 Dockerfile
,准备满心欢喜地开始我的 1MB/s
实验的时候,一道晴天霹雳打在我心上——
1 | root@53ace8551c27:/#$ dd if=500M.file bs=1M count=500 of=/dev/null |
当然问题现在已经解决了。为了重现当时的情况,我们从头开始。
Toolbox
我们简单地使用 Debian 作为测试的 Docker Image。
1 | $ docker pull debian |
并且使用 dd
命令生成一个 500M 的 000 文件,测试磁盘读写速度。
1 | $ dd if=/dev/zero of=500M.file bs=1M count=500 |
Yesterday Once More *
初次碰壁
拉镜像
1 | $ docker pull debian |
找到宿主机的设备路径
1 | $ sudo fdisk -l |
起一个 Docker,限制对应设备的读写速度
1 | $ docker run -it --rm --device-read-bps /dev/vda:1MB --device-write-bps /dev/vda:1MB debian |
1 | $ dd if=/dev/zero of=500M.file bs=1M count=500 |
?
这是一个非常可怕的事情。我限制的 1MB/s
并不工作。这个实验是基于这个假设进行的,如果没有办法限制设备的 IO,实验也没有办法继续进行了。
另辟蹊径
为了完成实验,我找了大量的资料。首先我把焦点放在使用 systemd
控制资源限制上。
根据 systemd
的文档( https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Options ),我们可以在对应服务的 systemd 配置文件中增加一些参数来实现自动化的资源控制,包括 CPU 资源限制,Memory 资源限制,进程数资源限制和 IO 限制。
IOAccounting =
Turn on Block I/O accounting for this unit, if the unified control group hierarchy is used on the system. Takes a boolean argument. Note that turning on block I/O accounting for one unit will also implicitly turn it on for all units contained in the same slice and all for its parent slices and the units contained therein. The system default for this setting may be controlled with
DefaultIOAccounting=
in systemd-system.conf(5).This setting replaces
BlockIOAccounting=
and disables settings prefixed withBlockIO
orStartupBlockIO
.IOReadBandwidthMax=
device
bytes
,
IOWriteBandwidthMax=device
bytes
Set the per-device overall block I/O bandwidth maximum limit for the executed processes, if the unified control group hierarchy is used on the system. This limit is not work-conserving and the executed processes are not allowed to use more even if the device has idle capacity. Takes a space-separated pair of a file path and a bandwidth value (in bytes per second) to specify the device specific bandwidth. The file path may be a path to a block device node, or as any other file in which case the backing block device of the file system of the file is used. If the bandwidth is suffixed with K, M, G, or T, the specified bandwidth is parsed as Kilobytes, Megabytes, Gigabytes, or Terabytes, respectively, to the base of 1000. (Example: “/dev/disk/by-path/pci-0000:00:1f.2-scsi-0:0:0:0 5M”). This controls the “
io.max
“ control group attributes. Use this option multiple times to set bandwidth limits for multiple devices. For details about this control group attribute, see IO Interface Files.These settings replace
BlockIOReadBandwidth=
andBlockIOWriteBandwidth=
and disable settings prefixed withBlockIO
orStartupBlockIO
.Similar restrictions on block device discovery as for
IODeviceWeight=
apply, see above.
这个方法一听就非常靠谱。 systemd
是一个让人又爱又恨的工具,我曾经为了从 service
切换到 systemctl
不知道背了多久这个命令的单词拼写。由于我们需要限制 NginX 的 IO 资源,首先要找到 NginX 的 systemd
配置文件。
1 | $ sudo systemctl status nginx |
然后进入 /lib/systemd/system/nginx.service
,修改 [Service]
块,增加三行。
1 | IOAccounting=true |
Reload daemon and nginx。
1 | $ sudo systemctl daemon-reload |
由于我在 NginX 的网页目录下放了一个测试下载速度的文件,我换了内网其他机器去拉。
1 | $ wget http://10.10.194.18/dash/test.file |
这个结果无疑告诉我这次尝试又失败了。
曙光初现
这个问题真的很奇怪,为什么我对资源的限制会不起作用。我后来在 NginX 的 systemd
配置文件中增加了 Memory 的 limit 是 work 的,但是 IO 相关的就不行。在本地虚拟机测试中,不管是 Docker 的 IO 限制还是 NginX 的 systemd
资源控制都是生效的,甚至一度让我怀疑是远程服务器的镜像问题。因为根据我咨询运维人员的情况来看,远程机器的镜像都经过特殊定制,有可能是这个原因。
不过,后来我的 Teammate 给了我一个 很重要的提示 。通过给 dd
命令增加参数 oflag=direct
,在 Docker 中可以得到限速后的效果。
1 | root@9f79e6469b67:/# dd if=500M.file bs=1M count=500 of=500.out oflag=direct |
这个参数的作用是什么呢?GNU https://www.gnu.org/software/coreutils/manual/html_node/dd-invocation.html#dd-invocation 介绍如下
‘oflag=flag[,flag]…’
Access the output file using the flags specified by the flag argument(s). (No spaces around any comma(s).)
Here are the flags. Not every flag is supported on every operating system.
‘direct’
Use direct I/O for data, avoiding the buffer cache. Note that the kernel may impose restrictions on read or write buffer sizes. For example, with an ext4 destination file system and a Linux-based kernel, using ‘oflag=direct’ will cause writes to fail with
EINVAL
if the output buffer size is not a multiple of 512.
这说明远程服务器的读写缓存对硬盘 IO 产生了巨大的影响。虽然我记得之前不知道在哪里看到过说,Cgroups 的磁盘 IO 限制模块 blkio 会对读写 buffer 进行限制,但是由于这是远程服务器,并且镜像经过定制,可能在系统底层绕开了这一限制,用于提升服务器 IO。
我也曾经考虑过读写 buffer 的问题,但是当时执行命令清空读写缓存时,遇上了这样的问题
1 | $ sudo echo 1 > /proc/sys/vm/drop_caches |
我便没有继续。
问题解决
最终,我们把问题锁定在服务器的读写缓存上。既然已经知道了问题所在,解决起来也就相对容易。虽然没有办法直接将清除缓存命令写进特定位置,但是可以用这条命令解决。
1 | $ sudo sh -c "/bin/echo 1 > /proc/sys/vm/drop_caches" |
这是工作的。至于为啥,我没研究。
还有另一套方案,将磁盘缓存的超时时间设置极低,也可以解决。
1 | $ sudo echo 100 > /proc/sys/vm/dirty_expire_centisecs |
当然,这些命令都要在宿主机进行操作,因为 Docker 本质只是一个在宿主机上虚拟化的线程。/proc/sys/vm/
文件夹对 Docker 容器来说,只是一个 Read-Only 的文件系统。
1 | root@9f79e6469b67:/# echo 100 > /proc/sys/vm/dirty_writeback_centisecs |
最终,在清除缓存后,一切都变得正常起来。
1 | root@9f79e6469b67:/# dd if=500M.file bs=1M count=500 of=/dev/null |
1 | $ wget 10.10.194.18/dash/test.file |
Appendix
我觉得这篇文章要是就这么结束,内容应该有点太少了。在填坑过程中,我还研究了不少和系统资源控制相关的内容。
Cgroup
Cgroup 是 Linux 用于控制进程资源的一种方式,从 2.6.24 内核中开始搭载,v2 版本于 4.5 内核开始搭载。它的配置文件在文件系统中的组织方式是 /sys/fs/cgroup/{Resource}/{defaultConfigs}
和 /sys/fs/cgroup/{Resource}/{Groups}/.../{configs}
。对应的限制内容会被写在目录的文件下,限制进程的 pid
会被写在目录的 tasks
文件夹下。简单看看本文主角 blkio 文件夹下的结构。
1 | /sys/fs/cgroup/blkio$ ls |
之前我们修改的 systemd/nginx.service
的内容被放在 system.slice
下,docker 的资源限制被放在 docker/
和 docker/container_id
下。
看看 cgroup 支持哪些资源
1 | $ lssubsys -m |
简单验证一下生效的几个配置。
Docker 资源限制
Docker ID: 9f79e6469b67
1 | $ docker inspect 9f79e6469b67 | grep Pid |
1 | $ cd /sys/fs/cgroup/blkio/docker/9f79e6469b67... |
其中,252 : 0 是磁盘设备号 <major>:<minor>
,1048576 = 1024 * 1024
1 | $ ls -l /dev/vda |
这说明在我们启动一个资源受限的 Docker 时,Docker 会自动在自身 cgroup 资源限制组下生成名为 container_id
的文件夹,然后将对应容器的 Pid 和限制资源规则写入。
Systemd 资源限制
1 | $ cd /sys/fs/cgroup/blkio/system.slice/nginx.service |
1 | $ cat blkio.throttle.read_bps_device |
NginX 的所有进程 Pid 和 .../system.slice/nginx.service/tasks
中的 Pid 一一对应。
systemd
的资源限制工作方式和 Docker 类似。
有趣的是,Docker 采用 2 ^ 10 作为单位,而 systemd 采用 1000 作为单位。
References
- [Systemd] https://www.freedesktop.org/software/systemd/man/systemd.resource-control.html#Options
- [dd] https://www.gnu.org/software/coreutils/manual/html_node/dd-invocation.html#dd-invocation
- [Disk Cache] https://stackoverflow.com/questions/20215516/disabling-disk-cache-in-linux
- [Disk Cache] https://unix.stackexchange.com/questions/48138/how-to-throttle-per-process-i-o-to-a-max-limit
- [Disk Cache] https://unix.stackexchange.com/questions/109496/echo-3-proc-sys-vm-drop-caches-permission-denied-as-root
- [Cgroup] https://coolshell.cn/articles/17049.html
- [Cgroup] https://tech.meituan.com/2015/03/31/cgroups.html
- [Cgroup] https://cizixs.com/2017/08/25/linux-cgroup/
- [Cgroup blkio] https://andrestc.com/post/cgroups-io/
- [Cgroup blkio] https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt
- [Docker IO] https://stackoverflow.com/questions/36145817/how-to-limit-io-speed-in-docker-and-share-file-with-system-in-the-same-time
————
License: BY-NC-SA 4.0
Link: https://wasteland.touko.moe//blog/2020/03/blkio-debug/
Written with Passion and Hope