原力注入

全新视角解析 Linux 非缓存缓冲 I/O “RWF_UNCACHED”:性能提升 65%~75%

背景

    相关文章:Linux 基础知识 - 内存水位线

    page cache 大家是有爱又恨,它提供了很好的性能,但是应用程序对于page cache 的管理几乎没有任何方法,只能通过设置相关的内核参数来进行有限的间接控制。

    内存异步回收以及内存直接回收有时候就是性能杀手,所以很多时候大家只能抛弃 page cache,要么用 direct IO,要么自己管理缓存,总之都不好做。而 Uncached Buffered I/O 正是针对这种场景提供了一种新的 IO 模式,从作者的测试来看,性能提升不少!个人建议大家先看一下文章三:Buffered I/O without page-cache thrashing(https://lwn.net/Articles/806980/)。

    作者:Jens Axboe,io_uring 的作者!

Image

    先做一下个人关于 “RWF_UNCACHED” Buffed IO 的总结,权当抛砖引玉:Uncached Buffered I/O (RWF_UNCACHED) 功能与应用场景总结

核心功能

  1. 1. 结合页面缓存但避免缓存持久化:

  • • 页面存在时: 直接利用页面缓存完成 I/O,避免额外的开销。

  • • 页面不存在时: 将数据临时加载到页面缓存中进行操作,但在操作完成后立即移除缓存,从而不改变缓存的整体状态。

  • 2. 避免页面缓存污染:

    • • 保留已有缓存状态,避免因不必要的数据填充页面缓存导致系统性能下降。

    • • 避免内存被不需要的缓存数据占用,从而减少回收压力和相关资源消耗(如避免 kswapd 高负载问题)。

  • 3. 保持缓冲 I/O 的优势:

    • • 提供缓冲 I/O 的易用性和高效性,特别是在数据已存在缓存中的情况下。

    • • 支持异步 I/O 方式,用户无需担心复杂的直接 I/O 限制。

  • 4. 减少直接 I/O 的复杂性:

    • • 避免直接 I/O 的对齐限制(块大小、偏移、长度)和同步操作的复杂性。

    应用场景

    1. 1. 一次性数据访问:适合那些只需要读取或写入一次的数据场景,无需长期缓存支持。

    2. 2. 用户态缓存管理:适合应用程序自行管理缓存的场景,避免与内核缓存重复。

    3. 3. 大数据集处理:特别是在数据集远大于系统内存(例如随机读取超过 10 倍内存大小的数据集)的情况下,RWF_UNCACHED 可显著提升系统 I/O 性能,同时降低 CPU 和内存消耗。

    优点总结

    • • 避免缓冲 I/O 导致的页面缓存污染问题。

    • • 提供更流畅、更可预测的 I/O 性能(如无 kswapd 负载现象)。

    • • 支持多种工作负载,特别是高吞吐量和低延迟需求的场景。

    • • 提供了“缓冲 I/O 的便利”与“直接 I/O 的性能”的折中方案。

    实现与支持

    1. 1. 实现方式:

    • • 用户通过 preadv2() 或 pwritev2() 设置 RWF_UNCACHED 标志即可启用功能。

    • • 对于 io_uring,设置 sqe->rw_flags 标志即可。

  • 2. 代码复杂度低:

    • • 初始实现代码量不大(约 200 行),主要添加了通用的页面缓存处理逻辑和对文件系统的支持。

  • 3. 文件系统支持:

    • • 当前支持 Ext4、XFS 和 Btrfs,未来可扩展至其他文件系统。

  • 4. 进一步优化与测试:

    • • 针对性能问题(如写操作的优化)已有改进,但仍需完善测试基础设施和语义文档。

    总之,RWF_UNCACHED 提供了一种性能优越、易用性高的缓冲 I/O 新模式,有望广泛应用于内存资源紧张或高性能 I/O 需求的场景中。

    接下来我们再来看几篇文章:

    文一:Uncached Buffered IO Is Performing Great, Working Now On Btrfs / EXT4 / XFS

    原文:https://www.phoronix.com/news/Uncached-Buffered-IO-2024

    正如上周报道的那样,Linux I/O 专家 Jens Axboe 最近 重新推进非缓存缓冲 I/O(https://www.phoronix.com/news/Uncached-Buffered-IO-2024,文章二) 的开发工作。这项基于 "RWF_UNCACHED" 的工作最初始于 2019 年,最近的努力展示出读写性能提升约 65%,目前已扩展支持 EXT4、Btrfs 和 XFS 文件系统。

    在上周取得良好进展后,本周他将这一补丁系列提交到邮件列表,以便进一步审查和协作。在 补丁系列(https://lore.kernel.org/linux-fsdevel/[email protected]/T/#u) 中,Axboe 为那些未跟进最新进展或对之前的 RWF_UNCACHED 工作不熟悉的人优雅地总结了关键点:

    “你可能会问,为什么要做这个?简而言之,设备速度只会越来越快,而内存回收速度不会。执行普通的缓冲 I/O 可能非常不可预测,并消耗大量资源用于回收。这导致人们使用 O_DIRECT 作为变通方法,但 O_DIRECT 在 I/O 的大小、偏移和长度方面有其自身的限制。它本质上是同步的,现在你还需要异步 I/O。虽然后者不一定是个大问题,因为我们有不错的选择,但当你只想读取或写入一些不缓存的数据时,这也不应成为一个必要要求。

    即使在桌面系统上,普通的 NVMe 设备也能在几秒内填满整个页面缓存。在我用于测试的大型系统上,虽然内存更多(1TB),设备数量也更多,但结果仍然类似。从以下补丁中的一些结果可以看出,即使有 1TB 内存,几秒内仍能填满。因此,这不仅是‘大型超大规模系统’的问题,而是普遍存在的。”

    但真正令人印象深刻的是,这种改进带来了性能上的轻松提升:读写性能提升约 65%,同时降低了 CPU 使用率。

    “性能结果在补丁 8(读取)和补丁 10(写入)中总结,简而言之,我观察到读取和写入的性能均提高了约 65%,同时 I/O 时间完全可预测。CPU 使用率也显著降低,使用非缓存 I/O 时完全没有 kswapd 的回收活动。

    在应用中使用非常简单——只需在读取或写入时通过 pwritev2(2) 或 preadv2(2) 设置 RWF_UNCACHED 标志即可。对于 io_uring,也是一样,只需在 sqe->rw_flags 中设置 RWF_UNCACHED 即可完成缓冲读写操作。就这么简单。”

    希望这项非缓存缓冲 I/O 支持能尽快被合并到主线 Linux 内核中。

    文二:Fresh Take On Linux Uncached Buffered I/O "RWF_UNCACHED" Nets 65~75% Improvement

    原文:https://www.phoronix.com/news/Linux-RWF_UNCACHED-2024

    Meta 的 Linux I/O 专家以及 block/IO_uring 维护者 Jens Axboe 最近重新审视了他关于非缓存缓冲 I/O 的补丁。早在 2019 年,Axboe 就开始了 "RWF_UNCACHED" 的开发工作,旨在解决页面缓存填满后性能出现的吞吐量断崖式下滑问题。这项工作曾一度被搁置,但最近 Axboe 开始重新设计一套新的补丁,用于实现非缓存缓冲 I/O,并且已经展示出极为可喜的成果。

    Jens Axboe 今日在 Twitter/X 上分享了他在 2024 年开发的 Linux 非缓存缓冲 I/O 的工作进展:

    “非缓存缓冲 I/O 在经历了 5 年的沉寂后重新归来。现在的实现更加简单、干净。在我的系统上性能提升了 65-75%,CPU 使用率减半。此外,还避免了页面缓存带来的不确定性问题。”

    这些新补丁目前可以在他的 buffered-uncached.2 Git 分支(https://git.kernel.dk/cgit/linux/log/?h=buffered-uncached.2) 中找到。其中,用于添加 RWF_UNCACHED 标志的 补丁(https://git.kernel.dk/cgit/linux/commit/?h=buffered-uncached.2&id=a29e99bd45b3e99dc13e3a8b245435a86a1afe55) 中,他解释道:

    “为读取操作添加 RWF_UNCACHED 标志,这意味着完成后从页面缓存中移除读取的数据。此功能利用页面缓存进行同步,并在操作完成后简单地修剪已实例化的 folio。...
    可以将非缓存缓冲 I/O 看作是 O_DIRECT 的更具吸引力的表亲——它没有 O_DIRECT 的种种限制。是的,它会复制数据,但与常规缓冲 I/O 不同的是,它不会受到页面缓存回收不确定性的影响。例如,在一个拥有 32 个驱动器的测试机器上,使用缓冲 I/O 读取的表现如下:

    Reading bs 65536, uncached 0
    1s: 145945MB/sec
    2s: 158067MB/sec
    3s: 157007MB/sec
    4s: 148622MB/sec
    5s: 118824MB/sec
    6s: 70494MB/sec
    7s: 41754MB/sec
    8s: 90811MB/sec
    9s: 92204MB/sec
    10s: 95178MB/sec
    11s: 95488MB/sec
    12s: 95552MB/sec
    13s: 96275MB/sec

    从中可以清楚地看到页面缓存填满的时刻——性能从良好变得波动,最终稳定在一个较低的速率。...
    如果对缓冲读取设置 RWF_UNCACHED 标志,则同一测试案例的输出如下:

    Reading bs 65536, uncached 0
    1s: 153144MB/sec
    2s: 156760MB/sec
    3s: 158110MB/sec
    4s: 158009MB/sec
    5s: 158043MB/sec
    6s: 157638MB/sec
    7s: 157999MB/sec
    8s: 158024MB/sec
    9s: 157764MB/sec
    10s: 157477MB/sec
    11s: 157417MB/sec
    12s: 157455MB/sec
    13s: 157233MB/sec
    14s: 156692MB/sec

    性能稳定在约 155GB/sec 的读取速率。...
    测试程序独占 CPU,回收操作仅发生在主线程之外。性能不仅提高了 65%,系统使用的 CPU 资源也减少了一半。”

    这无疑是一个极具吸引力的改进。

    另一份 补丁 (https://git.kernel.dk/cgit/linux/commit/?h=buffered-uncached.2&id=3e4915125ca07d5f22d165ebe041a3e9713acae2)添加了对缓冲写入操作中 RWF_UNCACHED 的支持:

    “如果为写入操作设置 RWF_UNCACHED 标志,将使用 drop_writeback 标记正在写入的 folio。然后写回完成后会丢弃这些页面。write_iter 处理程序仅启动这些页面的写回,写回完成后会处理其余部分……行为完全可预测,即使在页面缓存原本会被填满脏数据之后,性能依然保持一致。此外,与常规缓冲写入相比,性能提高了约 75%,系统 CPU 使用率减少了一半。”

    这些令人振奋的工作令人期待,希望它们能够很快被合并到主线 Linux 内核中。

    文章三:缓冲 I/O 不再导致页面缓存过载

    原文:https://lwn.net/Articles/806980/

    Linux 提供了两种文件 I/O 模式:缓冲和直接。缓冲 I/O 会经过内核的页面缓存,使用相对简单,并且对于多次访问的数据可以显著提升性能。而直接 I/O 则直接在用户空间缓冲区与存储设备之间传输数据,这对于不需要操作系统缓存的场景可能更快,但使用起来较为复杂,并且有许多潜在的陷阱。现在,Jens Axboe 似乎 找到了一种方法,能够以更少的麻烦获得直接 I/O 的许多优势。

    直接 I/O 可以通过多种方式实现比缓冲 I/O 更好的性能。其中之一是避免在用户空间和页面缓存之间复制数据的开销;虽然这一开销可能显著,但在许多情况下,这并不是最大的问题。真正的问题可能是缓冲 I/O 对页面缓存的影响。

    一个处理大量缓冲 I/O 的进程(尤其是针对一个或多个大文件,相较于可用内存而言)很快会将页面缓存(以及内存)填满。这种情况下,如果该进程在 I/O 完成后不再访问这些页面,那么将这些数据保存在内存中是没有意义的,但数据仍会保留在那里。为了为其他用途分配内存,内核需要从某处回收一些页面。这对整个系统来说可能非常昂贵,即使“某处”正是与此 I/O 活动相关的数据。

    内存管理子系统试图在这种情况下做出正确的选择。通过缓冲 I/O 添加到缓存的页面会进入非活动列表;除非这些页面在短时间内被再次访问,否则它们将是最先被回收的页面。但实现这一行为仍然需要相当多的开销;Axboe 运行了一个简单的测试,并这样描述了结果:

    测试用例非常基础:在一个大小为内存 10 倍的数据集上随机读取。起初性能表现良好,但页面缓存被填满后,吞吐量急剧下降。I/O 线程的 CPU 使用率上升,而 kswapd 占用一个核心 100% 的时间来尝试跟上回收速度。

    这种问题可以通过切换到直接 I/O 来避免,但直接 I/O 本身也带来了挑战和问题。Axboe 认为可能存在一种折中的第三种方法,能够提供两种模式的优点。

    这种第三种方法是一种新标志 RWF_UNCACHED,它可以传递给 preadv2() 和 pwritev2() 系统调用。如果设置了该标志,根据目标文件页面是否已存在于页面缓存中,I/O 操作会以两种不同的方式执行。如果数据已经存在于页面缓存中,操作会像没有 RWF_UNCACHED 标志一样继续;数据会从缓存中读取或写入。而如果页面不存在,则它们会暂时添加到页面缓存中,仅在操作期间保留;操作完成后,这些页面会从页面缓存中移除。

    换句话说,结果是不会改变页面缓存状态的缓冲 I/O:之前在缓存中的内容会保持不变,但不会添加新的内容。通过这种方式进行的 I/O 可以获得大多数缓冲 I/O 的好处,包括易用性和对已缓存数据的访问,但不会因为不需要的数据填满内存。Axboe 表示,这一结果是显而易见的:

    通过这种方式,我们可以实现 100% 流畅的缓冲读取或写入,而不会让内核进入 kswapd 忙于回收的状态。事实上,这种负载根本没有表现出来。

    因此,这个新标志对于各种工作负载来说似乎是一项显著的改进。尤其是对于已知数据只会使用一次的工作负载,或者应用程序在用户空间自己进行缓存的情况,使用 RWF_UNCACHED 标志可能会带来显著的好处。

    实现这一新行为并不复杂;整个补丁集(还为 io\_uring 添加了支持)仅涉及 200 多行代码。当然,正如 Dave Chinner 指出的,仍然缺少一些东西:需要完整的测试基础设施来确保 RWF_UNCACHED 表现如预期且不会破坏数据。Chinner 还提到写入实现中的一些性能问题,建议一次刷新整个 I/O 操作,而不是当前补丁集中采用的逐页方法。Axboe 已经重新设计了代码来解决这些问题;后续的测试和语义文档编写工作将在未来进行。

    如果 RWF_UNCACHED 在实际工作负载中表现良好,最终可能会被认为是那些“早该有人想到”的功能之一。这种情况经常发生。解决问题并不难;难的是弄清楚究竟哪个问题需要被解决。当然,还有测试和文档编写的工作。