原作者:Josef Bacik

原文链接:https://lwn.net/Articles/782876/

背景

在Linux中多个用户共用一个硬盘是件痛苦的事情。不同应用场景有不一样的I/O访问模型和延迟要求,而且是随时变化的。限流(throttling)[1]能让用户公平地使用属于自己的那份带宽,但是I/O往往使用writeback模式[2],数据已经缓存在page cache中了,此时限流已有些晚了,会对系统其它地方产生压力。硬盘千差万别,传统的机械盘,固态盘,有一般的固态盘,也有差的固态盘。每一类硬盘的性能特征都不一样,即便同一种盘,性能表现也要看跑什么负载。想用一个I/O控制器来搞定这些问题太难了,但是我们Facebook团队提出了一个很不错的方案。

过去,内核有过两个cgroup I/O控制器。第一个io.max,能为每个设备的IOPS或带宽设置上限值。第二个io.cfq.weight, 是CFQ I/O调度器提供的。当Facebook在pressure-stall informationversion-2 control-group interface投入一阵后,发现这两个控制器根本解决不了我们的问题。我们的典型场景是,有个业务负载(main workload)一直运行,另外有个系统工具在后台周期性运行。Chef工具一小时运行几次,更新系统配置,安装软件包。Fbpkg工具自动下载最新版本的应用,一天大概运行三四次。

Io.max控制器能让我们限制住那些系统管理工具,但是也让这些工具跑得极其慢,减少限制后,对主负载影响又太大,所以这个方案不是很好。CFQ io.cfq.weigh控制器就更没戏了,因为CFQ不支持多队列块层特性,更别说我们在实践中遇到CFQ在延迟上有各种各样的问题了,好多年以前我们已经把CFQ关掉了,改用deadline调度器了。

Jens Axboe开发的writeback-throttling特性,是一个监控和限制负载的新思路。工作原理简单来讲,一是设置延迟阈值,并监测从硬盘上读的延迟,二是如果读延迟超过阈值,就开始限制对硬盘的写操作。这个特性是工作在I/O调度器之上的,这点非常重要,因为一个块设备的请求队列是非常有限的,是非常重要的资源。队列深度可以通过/sys/block//queue/nr_requests设置。在限制写操作时,writeback-throttling会在分配request前降低队列设备,留更多request给读操作。

这个方案解决了fbpkg要下载数GB软件包来更新业务应用的问题。但是趋势是,现在好多个应用常常积攒起来,然后一起更新,引起一波密集的写操作,我们就会看到系统的整体延迟突然会变大,影响正在运行的业务。

一个新I/O控制器

Writeback throttling不能感知到cgroup的存在,只是关心一个设备上读与写的延迟平衡。但是,它有很多非常棒的思想,我斗胆借鉴来实现了一个新控制器,叫作io.latency。io.latency必须既能工作在机械盘上,也能工作在高端 NVMEe SSD上,所以overhead必须很小。我的目标是在热点路径上实现无锁化,基本上做到了。起初,我们非常想即能做到保护业务负载,又能实现proportional control。一方面,我们要不惜代价保护在线业务,另一方面我们也想适应各种各样其它负载。最终我们妥协了,决定还是先全力保护在线业务,以后再搞一个新的方案来实现proportional control。io.latency能以cgroup为单位设置延迟阈值。如果在一段时间(一般250ms)内持续超过阈值,控制器就会限制cgroup组里面阈值高的任务。限制机制和writeback-throttling一样:控制器把cgroup对应的队列深度调低。如图1所示,调控只作用于cgroup中的相关任务,比如如果fast超过了延迟阈值,那么只有slow会受到限制,而unrelated不会受影响。

control-group hierarchy

图1

我实现无锁化的方法是在parent和children中都设置一个cookie。举例来说,如果fast超过阈值,parent group(b)中的cookie就会减小,下一次slow在提交I/O请求时,控制器会计算b中的cookie和slow中的cookie的比值,如果比值减小了,控制器就相应减少slow的队列深度,如果比值增加了,slow队列深度也会增加。

在正常I/O路径上, io.latency添加了两个原子操作:一个是读parent的cookie,一个是从队列中获取一个slot。在completion路径上,我们有一个原子操作来释放队列slot,另一个是per-CPU操作对I/O消耗的时间记账。在slow路径上,也就是采样周期(250ms)到来时,我们必须对parent加锁并更新I/O统计数据,然后检查延迟是否超过阈值。

Io.latency很重要的部分就是对I/O时间记账。我们很关心应用遭受了多久延迟,所以把每个I/O操作从提交到完成的时间记录在per-CPU数据结构中,每隔一个周期再加起来。在测试中,我们发现使用平均延迟对机械盘还比较合适,但对固态盘就不够灵敏了。因此,对固态盘我们计算了延迟的百分位数,如果90分位延迟超过阈值,就应该限制低优先级的任务了。

最后要讲的Io.latency部分是一个定时器,每秒触发一次。控制器尽可能做了无锁优化,是受I/O操作驱动的。但是,如果你为了保护重要的业务负载而把slow任务限制的太狠,导致I/O停止,我们就不能解除对slow group的限制了。不管I/O从哪里来,定时器就会被触发来解决这个问题,确保继续处理slow group的I/O。

这个方案是完美的吗?

很不幸,我们花了很多时间在生成环境下测试,发现总是被各种优先级翻转问题折磨。内核是一个多组件交互的庞大系统,如果任务被限制,导致submit_bio()耗时增加,会影响其它组件的行为。

我们的测试场景是这样的,1. 运行一个负载很重的web服务,即fast workload;2. 同时,在slow cgroup中,故意运行一个有内存泄露的任务,即slow workload 。会发生什么呢? Fast workload会触发内存回收,在内存紧张时会进行swap I/O。页是属于所在cgroup的,意味着利用这些页所做的I/O都算在了其cgroup头上。那么,我们高优先级的任务在通过swap机制去使用原来属于低优先级cgroup的页时,高优先级任务就会以外的受到限制。

要是放在过去,这个问题不难解决:只要增加一个REQ_SWAP标志,让I/O控制器对设置有这个标志的操作不要做限制就行了。REQ_META标志就是这么做的,要不然高优先级任务有可能阻塞在低优先级cgroup提交的元数据更新上。但是,现在所有REQ_SWAP I/O是不受限制的,是没有任何代价的,那么极端情况下像这样的“坏”负载:只是消耗内存而不做任何I/O, 那么只能眼睁睁看着它拖累fast workload,而没有任何办法做限制。一旦内存吃紧,fast workload的延迟就会飚高,因为很多业务负载时内存密集型的,而不是I/O密集的。

为了彻底解决这个问题,还得想想别的办法。我们已经知道问题是:fast workload在内存紧张时,会触发swap I/O,“替”slow cgroup任务遭到了惩罚性限制。所以,我们只需要找个办法告诉内存管理层:谁才是应该惩罚的。 为此,我在cgroup的结构体里面增加了一个拥塞计数器(congestion counter),当一个cgroup做了很多有REQ_SWAP标志的I/O后,我们就会增加拥塞计数器。这样,我们就能知道了这些swap I/O出去的页应该算在谁头上,然后给这个cgroup打上拥塞标志,内存管理层就能知道什么时候应该开始限制这个cgroup了。

我们要解决的另个难题是:mmap_sem信号量。在我们的场景中,有段监控代码要不断做类似ps命令的动作:获取mmap_sem,然后读/proc//cmdline。我们知道page-fault handler也会取获取mmap_sem,试想如果有个任务已经在page-fualt中拿到了mmap_sem,恰好也正在被限制,那么fast workload有肯能会阻塞在mmap_sem上直到被限制的I/O处理完成。这意味着什么呢?意味着我们必须想办法避免在持有内核锁的路径上去做限制。为此,我们增加了blkcg_maybe_throttle_current()函数。什么时机调用呢?我们知道在执行路径即将返回到用户空间时,任务肯定没有占用任何内核锁,所以我们可以借助这个时机,根据需要在此处插入适当延迟来达到限制的目的。

具备了这些机制后,我们终于有了一个可以正常工作的io.latency控制器。

使用效果

在没有io.latency以前,当我们运行带有内存泄露的负载一段时间后,系统就会陷入不断的内存交换,过数分钟后,会出现两种情况:要么业务负载被oom杀死,要么在我们的健康检查工具发现系统不稳定后,系统被重启。从机器重启到业务完全恢复正常往往需要40多分钟。

在同样情况下,用上io.latency和我们研发的oomd监控工具后,当内存泄露严重后,oomd会发现和杀死导致内存泄露的任务。测试发现,在负载很重的web服务器上,每秒处理的请求会下降10%,而在一般负载下,性能损失更小。

展望

Io.latency控制器,配合着其它cgroup特性和oomd,已经运行在我们的生成环境中:所有web服务器,构建服务器和消息传递服务器,而且已经稳定运行了一年,很大程度上减少了宕机次数。下一步,我们正在开发一个叫io.weight的proportional I/O控制器,应该很快能开源出来。现在各种优先级翻转的问题已经解决了,往后添加新的I/O控制器就容易多了。

引用

[1] IO throttle:https://www.kernel.org/doc/Documentation/cgroup-v1/blkio-controller.txt

[2] Writeback:https://www.kernel.org/doc/Documentation/filesystems/ext4.txt