icon-cookie
The website uses cookies to optimize your user experience. Using this website grants us the permission to collect certain information essential to the provision of our services to you, but you may change the cookie settings within your browser any time you wish. Learn more
I agree
blank_error__heading
blank_error__body
Text direction?

(译) io.latency I/O控制器

原作者: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

Measure
Measure
Related Notes
Get a free MyMarkup account to save this article and view it later on any device.
Create account

End User License Agreement

Summary | 2 Annotations
CFQ不支持多队列块层特性
2020/09/04 09:27
块设备的请求队列是非常有限的
2020/09/04 09:28