14

我正在寻找有关如何为我在 Ubuntu Linux 14.04 上运行的应用程序获得高效和高性能异步 IO 的建议。

我的应用程序处理事务并在磁盘/闪存上创建文件。随着应用程序通过交易进行,会创建额外的块,这些块必须附加到磁盘/闪存上的文件中。该应用程序还需要在处理新事务时经常读取此文件的块。除了创建一个必须附加到该文件的新块之外,每个事务可能还需要从此文件中读取不同的块。有一个传入的事务队列,应用程序可以继续处理队列中的事务,以创建足够深的 IO 操作管道,以隐藏磁盘或闪存上读取访问或写入完成的延迟。对于尚未写入磁盘/闪存的块(由先前的事务放入写入队列)的读取,应用程序将停止,直到相应的写入完成。

我有一个重要的性能目标——应用程序应该产生尽可能低的延迟来发出 IO 操作。我的应用程序需要大约 10 微秒来处理每个事务,并准备好对磁盘/闪存上的文件进行写入或读取。发出异步读取或写入的额外延迟应尽可能小,以便应用程序可以在只需要文件写入时以每个事务尽可能接近 10 微秒的速率完成处理每个事务。

我们正在试验一种使用 io_submit 发出写入和读取请求的实现。对于满足我们要求的最佳方法的任何建议或反馈,我将不胜感激。io_submit 是否会给我们最好的性能来实现我们的目标?我应该对每次写入 io_submit 的延迟和每次读取 io_submit 的延迟有什么期望?

使用我们的实验代码(在 2.3 GHz Haswell Macbook Pro、Ubuntu Linux 14.04 上运行),我们测量了在扩展输出文件时写入 io_submit 大约 50 微秒。这太长了,我们甚至没有接近我们的性能要求。任何帮助我以最短延迟启动写入请求的指导将不胜感激。

4

2 回答 2

29

Linux AIO(有时称为 KAIO 或libaio)是一种黑魔法,经验丰富的从业者知道其中的陷阱,但出于某种原因,将他们不知道的陷阱告诉别人是禁忌。通过在网上摸索和经验,我想出了一些例子,其中 Linux 的异步 I/O 提交io_submit()可能会(默默地)同步,从而将其变成阻塞(即不再快速)调用:

  1. 您正在提交缓冲(也称为非直接)I/O。您受 Linux 缓存的支配,并且您的提交可以在以下情况下同步:
    • 您正在阅读的内容尚未在“读取缓存”中。
    • “写缓存”已满,在完成某些现有写回之前,无法接受新的写请求。
  2. 您要求对文件系统中的文件进行直接 I/O,但无论出于何种原因,文件系统决定忽略O_DIRECT“提示”(例如,您提交 I/O 的方式不符合O_DIRECT对齐约束、文件系统或特定文件系统的配置不't support O_DIRECT) 并且它选择静默地执行缓冲 I/O,从而导致上述情况。
  3. 您正在对文件系统中的文件进行直接 I/O,但文件系统必须执行同步操作(例如通过写回读取元数据/更新元数据)才能完成您的 I/O。一个常见的例子是发出“分配写入”(例如,因为您正在追加/扩展文件的末尾或填充未分配的洞),这听起来像提问者正在做的事情(“附加到文件”) . 一些文件系统(例如 XFS)更努力地提供良好的 AIO 行为,但即使在那里,用户也必须小心避免将某些操作并行发送到文件系统,否则io_submit()在其他操作完成时再次变成阻塞调用。Seastar框架包含一个文件系统特定案例的小型查找表
  4. 您提交了太多未完成的 I/O。您的磁盘/磁盘控制器将具有可同时处理的最大 I/O 请求数,并且内核中每个特定设备的最大请求队列大小(请参阅/sys/block/[disk]/queue/nr_requests文档和未记录的文档)。/sys/block/[disk]/device/queue_depth使I/O 请求备份并超过内核队列的大小会导致阻塞
    • 如果您提交“太大”的 I/O(例如,大于/sys/block/[disk]/queue/max_sectors_kb真正的限制可能比 512 KiB 更小),它们将在块层中被拆分并继续处理多个请求。
    • 系统全局并发 AIO 请求的最大数量(请参阅/proc/sys/fs/aio-max-nr文档)也会产生影响,但结果将显示在io_setup()而不是io_submit().
  5. Linux 块设备堆栈中的请求和提交到磁盘之间的一层必须阻塞。例如,Linux 软件 RAID (md) 之类的东西可以使通过它的 I/O 请求停止,同时更新单个磁盘上的 RAID 1 元数据。
  6. 您的提交导致内核等待,因为:
    • 它需要获取一个i_rwsem正在使用的特定锁(例如 )。
    • 它需要分配一些额外的内存或分页。
  7. 您将 I/O 提交给不是“常规”文件或块设备的文件描述符(例如,您的描述符是管道或套接字)。

上面的列表并不详尽。

对于 >= 4.14 内核,该RWF_NONBLOCK标志可用于使上述一些阻塞场景产生噪声。例如,当使用缓冲并尝试读取尚未在页面缓存中的数据时,该RWF_NONBLOCK标志将导致提交失败,EAGAIN否则会发生阻塞。显然,您仍然 a) 需要支持此标志的 4.14(或更高版本)内核,并且 b) 必须了解它未涵盖的情况。我注意到有一些补丁已被接受或建议EAGAIN在更多情况下返回,否则这些情况会阻塞,但在撰写本文时(2019)RWF_NONBLOCK不支持缓冲文件系统写入

备择方案

如果您的内核 >=5.1,您可以尝试使用io_uringwhich 在不阻止提交方面做得更好(这是一个完全不同的界面,是 2020 年的新界面)。

参考

有关的:

希望这篇文章对某人有所帮助(如果对您有所帮助,您可以投票吗?谢谢!)。

于 2017-09-23T08:30:17.233 回答
12

我在这里作为提议的 Boost.AFIO的作者发言。

首先,Linux KAIO (io_submit) 几乎总是阻塞,除非 O_DIRECT 开启并且不需要范围分配,如果 O_DIRECT 开启,您需要在 4Kb 对齐的边界上读取和写入 4Kb 倍数,否则您强制设备进行读取-修改-写入。因此,除非您将应用程序重新架构为 O_DIRECT 和 4Kb 对齐的 i/o 友好,否则使用 Linux KAIO 将一无所获。

其次,永远不要在写入期间扩展输出文件,您会强制分配范围,并且可能会刷新元数据。而是将文件的最大范围分配到某个适当大的值,并保留文件末尾的内部原子计数器。这应该将问题减少到只是对 ext4 是批处理和惰性的扩展分配 - 更重要的是,您不会强制刷新元数据。这应该意味着 ext4 上的 KAIO 大部分时间都是异步的,但不可预知的是会同步,因为它会将延迟的分配刷新到日志中。

第三,我可能解决您的问题的方法是使用原子追加(O_APPEND)而不使用 O_DIRECT 或 O_SYNC,因此您所做的是将更新附加到内核页面缓存中不断增长的文件中,这非常快速且并发安全。然后,您不时地垃圾收集日志文件中的哪些数据是陈旧的,以及可以使用 fallocate(FALLOC_FL_PUNCH_HOLE) 解除分配的范围,因此物理存储不会永远增长。这将合并写入存储的问题推到了内核上,在内核中已经花费了很多精力来加快速度,并且因为它是一个始终向前推进的写入,您将看到写入以与写入它们的顺序相当接近的顺序到达物理存储使功率损耗恢复简单。

万一上述所有工作看起来很繁重,操作系统已经提供了所有三种技术,这些技术整合到一个高度调整的实现中,也就是众所周知的内存映射:4Kb 对齐 i/o、O_DIRECT、从不扩展文件,所有异步输入/输出。在 64 位系统上,只需将文件分配到非常大的数量并将其映射到内存中。随心所欲地阅读和写作。如果您的 i/o 模式混淆了可能发生的内核页面算法,您可能需要在各处使用 madvise() 来鼓励更好的行为。madvise() 少即是多,相信我。

很多人尝试使用各种 O_DIRECT 算法复制 mmap,却没有意识到 mmap 已经可以完成您需要的一切。我建议先探索这些,如果 Linux 无法正常运行,请尝试使用具有更可预测的文件 i/o 模型的 FreeBSD,然后才深入研究推出自己的 i/o 解决方案的领域。作为一个整天都在做这些的人,我强烈建议你尽可能避免使用它们,文件系统是古怪和怪异行为的恶魔坑。将永无止境的调试留给其他人。

于 2016-01-03T16:56:09.317 回答