LWN:替换 congestion_wait()!
共 4103字,需浏览 9分钟
·
2021-11-08 11:06
关注了就能看到更多这么棒的文章哦~
Replacing congestion_wait()
By Jonathan Corbet
October 25, 2021
DeepL assisted translation
https://lwn.net/Articles/873672/
内存管理在很多方面都是在追求平衡。例如,内核必须要对当前用户对内存的需求以及预期未来的需求之间进行一下权衡。内核还必须权衡是否要为其他用途而回收内存,因为这可能涉及到需要将数据写入永久存储(permanent storage),以及底层的存储设备进行数据写入时的速度等等。多年来,内存管理子系统一直把存储设备操作的拥塞作为一个信号,告诉它自己应该要放慢回收速度了。不幸的是,这个机制从一开始就有点问题,在很长一段时间内其实并没有效果。Mel Gorman 现在正试图提供一个 patch set 来解决这个问题,不过这样一来内核就不应该再等待拥塞出现了。
The congestion_wait() surprise
当内存变得紧张时,内存管理子系统必须得回收当前正在使用的 page 用于其他用途。这反过来又先需要把所有被修改过的 page 的内容写出去。如果要写入这个 page 内容的块设备已经被繁重的写入任务所占用了,那么这里就算让 I/O 请求堆积得更高,也没有什么效果了。早在黑暗和遥远的时代, Git 出现之前(2002 年),Andrew Morton 就提议为块设备增加一个拥塞追踪(congestion-tracking)机制:如果某个设备已经拥塞住了,那么内存管理子系统就会暂时不再发起新的 I/O 请求(并控制(也就是放慢)那些在申请更多内存的进程的运行),直到拥塞缓解。这一机制在 2002 年 9 月的 2.5.39 开发版内核中就位了。
在那以后的几年里,拥塞等待机制以各种方式演进。即将发布的 5.15 内核中仍然包含有一个叫做 congestion_wait() 的函数,它可以暂停当前的 task,直到拥挤的设备变得不那么拥挤(也就是通过 clear_bdi_congested() 调用来通知这个状态)或者出现 timeout 从而结束。或者,至少这曾是它想象中的行为。
碰巧,clear_bdi_congested() 的主要调用位置是一个叫做 blk_clear_congested() 的函数,该函数在 2018 年的 5.0 内核版本中被移除了。从那时起,除了少数文件系统(Ceph、FUSE 和 NFS)之外,没有任何东西调用 clear_bdi_congested() 了,这意味着对 congestion_wait()的调用几乎总是会坐等到 timeout 结束,这并不是开发者的本意。
又过了一年(来到了 2019 年 9 月),内存管理开发人员才搞清楚这一点,这时,block 子系统维护者 Jens Axboe 让大家知道:
拥堵已经不存在了。在我看来,这个机制一直是无效的,因为它天生就是有 race condition 的。我们在传统的做法中使用过去的 batch 批处理机制来发出 signal,而这只在一些设备上有效。
Norton 在他的原始提案中实际上已经注意到了拥塞机制的 race condition 问题。某个 task 可以检查设备并看到它此时没有拥塞,但在该任务排进队列一直到其 I/O 请求得到之前,情况可能会发生变化。随着存储设备所支持的命令队列长度的增加,拥塞检测也越来越难做到准确了。所以块设备的开发者决定在 2018 年摆脱这个概念。不幸的是,当时没有人告诉内存管理开发人员这件事,从而导致了 Michal Hocko 在事情被报出来之后当时的很不愉快的抱怨。
这是一个不好的例子,就像是一只手不知道另一只手在做什么。多年来,这个问题导致内存管理性能受损。但是,内核开发人员往往不会光坐在那里指责和抱怨,相反,他们开始思考如何解决这个问题。他们一定想得很周到,因为这个过程花费了两年时间,才有 patch 出现。
Moving beyond congestion
Gorman 的 patch set 一开始就指出,"即使拥塞控制这个功能有效,也不能说这是一个好主意"。有许多情况会拖慢 reclaim process (内存回收进程)。其中一种情况是有太多的 page 在进行 writeback,导致底层设备处理不过来。这种情况可能可以被一个(正常工作的)congestion-wait 机制来解决,但其他问题不会被同样解决了。因此,这个 patch set 删除了所有的 congestion_wait() 调用,并采用了一系列启发式的方法来取代它们。
在内存管理子系统中,有一些地方的需要对 reclaim 进行限流(throttle)。例如,如果 kswapd 线程发现目前正在写回的 page 已被标记为需要立即回收,这表明这些 page 在写入后备存储之前已经是在 LRU 中走完了一轮了。当这种情况发生时,进行 reclaim 的 task 将会被限流(throttle)一段时间。但并不是在等待那个已经不再存在了的 "congestion is done" 的 signal,而是会暂停 reclaim 直到当前 NUMA node 上的 page 已经有足够多的被写出去了为止。
请注意,一些线程(尤其是内核线程和 I/O worker)在这种情况下不会被节流,可能会需要它们继续工作来清理积压(backlog)。
许多内存管理操作,如 compaction 和 page migration,需要对后续将要操作的 page 给 "隔离(isolate)" 开。在这种情况下,隔离是指从那些 LRU list 中将此 page 删除。reclaim process 也需要到在写出去之前隔离 page。如果许多 task 最终会进行 direct reclaim,那么可以会有大量 page 被隔离,从而需要一些时间才能完全完成 reclaim 操作。如果内核对 page 进行隔离操作的处理速度比起 page 被回收的速度要快,那么这完全没有必要,其实是一种浪费。
如果被隔离的 page 数量过多的话,内核本身已经会对 reclaim 进行节流,但这种节流操作实际上是在等待(或试图等待)拥塞结束。戈尔曼指出。"这没有意义,过度的并行化处理本身与 writeback 或拥塞无关"。新的代码改为了使用一个 wait queue,供那些在进行 reclaim 时因隔离太多 page 而被限流的 task 来使用。当隔离 page 的数量下降或发生 timeout 时,就会唤醒这些 wait queue。
有时,进行执行 reclaim 操作的线程可能会发现它的工作基本上没有什么进展:扫描了很多 page,但成功回收的 page 很少。这可能是由于它在扫描 page 存在太多引用,或者其他一些因素。有了这个 patch,那些在 reclaim 方面进展不好的线程将被节流,一直等到系统中的某个地方取得进展。具体来说,内核将一直等待,直到运行中的回收线程成功完成至少 12%的 page 的扫描,然后才会唤醒那些没有进展的线程。这应该会减少浪费在无效回收这个工作上的时间。
如果由于内存太少而导致写出 dirty page 的时候失败了,那么 writeback 工作也会被限流。在这种情况下,只有等到一些 page 被成功写回之后(或者像平常一样发生超时),才会取消限流。
大多数情况下 timeout 时间被设置为十分之一秒。不过,等待那些被隔离的 page 数量下降的 timeout 是五十分之一秒,原因是这种情况应该会很快发生变化。设置这些 itmeout 的 patch 指出,这些数值是 "凭空产生的",但在有人找到更好的值之前,可以先开始使用。作为朝这个不断改进的方向迈出的第一步,在 benchmark 测试结果显示 no-progress 这类 timeout 太容易超时之后,就将它改为了 0.5 秒。
这组 patch set 提供了一组详尽的 benchmark 测试结果。作为测试的一部分,Gorman 增加了一个新的 "stutterp " test,期望能展示 reclaim 问题。结果差别很大,但总体上是积极的。例如,一项测试显示系统的 CPU 时间减少了 89%。戈尔曼总结说:
最起码,限流看起来是有效的,wakeup event 会减少最坏情况下的 stall (卡住情况)。timeout 值可能还有一些调整空间,但这些工作很可能是徒劳的,因为最坏的情况跟 workload、内存大小和存储速度息息相关。希望进一步改善这组 patch 的话,更好的方法是根据 task 进行 allocation 分配的速率来确定其优先级,但这种做法可能会引入非常昂贵的开销。
这些 patch 到目前为止已经经历了五轮修改,发生了各种变化。很难想象这项工作最终会有什么原因被 mainline 拒绝。毕竟当前 kernel 中的代码显然是无法使用的。但是这种核心的内存管理代码的改动总是很难得到 merge 的。现在世界上的 workload 种类那么多,当改为使用这种启发式方法进行处理之后,肯定有一些情况下的性能会下降。因此,虽然这样的改动看起来会被接受,但人们永远不知道在合入之前会出现多少次 timeout。
全文完
LWN 文章遵循 CC BY-SA 4.0 许可协议。
长按下面二维码关注,关注 LWN 深度文章以及开源社区的各种新近言论~