为啥带有“直接”(O_DIRECT)标志的 dd 速度如此之快?

Posted

技术标签:

【中文标题】为啥带有“直接”(O_DIRECT)标志的 dd 速度如此之快?【英文标题】:Why is dd with the 'direct' (O_DIRECT) flag so dramatically faster?为什么带有“直接”(O_DIRECT)标志的 dd 速度如此之快? 【发布时间】:2016-02-02 18:40:59 【问题描述】:

我有一台 RAID50 配置的服务器,有 24 个驱动器(两组,每组 12 个),如果我运行:

dd if=/dev/zero of=ddfile2 bs=1M count=1953 oflag=direct

我明白了:

2047868928 bytes (2.0 GB) copied, 0.805075 s, 2.5 GB/s

但如果我跑:

dd if=/dev/zero of=ddfile2 bs=1M count=1953

我明白了:

2047868928 bytes (2.0 GB) copied, 2.53489 s, 808 MB/s

我了解 O_DIRECT 会导致页面缓存被绕过。但据我了解,绕过页面缓存基本上意味着避免使用 memcpy。在我的桌面上使用bandwidth tool 进行测试,我的最坏情况下顺序内存写入带宽为 14GB/s,我想在更新的更昂贵的服务器上,带宽必须更好。那么为什么额外的 memcpy 会导致 2 倍以上的减速呢?使用页面缓存时真的涉及更多内容吗?这是非典型的吗?

【问题讨论】:

非典型(见thesubodh.com/2013/07/what-are-exactly-odirect-osync-flags.html)。不仅 memcpy,缓存管理也... OT,但是 12 磁盘 RAID 5? 11个数据盘?这将导致一些真正讨厌的读-修改-写操作。请参阅 Read-modify-write 部分:infostor.com/index/articles/display/107505/articles/infostor/… RAID-5(和 RAID-6)最适用于将写入块大小与将在所有 RAID 数据磁盘上写入整个条带的块大小。好的控制器可以隐藏问题,但在极端负载下你会看到它。 您对服务器上单线程内存带宽的猜测可能是错误的。与直觉相反,单线程内存 BW 受 max_concurrency/latency 的限制,单个桌面内核与 big Xeon 中的内核具有相同数量的 line-fill 缓冲区,但 big Xeon 具有更高的延迟(更多跳数在环形总线)在核心和 DRAM 或 L3 之间。 Why is Skylake so much better than Broadwell-E for single-threaded memory throughput?。最大聚合吞吐量是巨大的,但它需要比台式机更多的内核才能达到相同的 B/W。 我不同意结束这个问题。将 O_DIRECT 标志传递给 open() 对于任何编写 IO 代码的人来说都是一种可能的显着优化。我认为我们在这里过于努力地将所有内容都压缩到不同的类别中。 O_DIRECT 所做的不仅仅是避免一些 memcpys。它会更改发送到存储子系统的 I/O 请求的大小。当您使用 O_DIRECT 进行 1MB 读/写时,Linux 将不遗余力地实际进行单个 MB 读/写。当你在没有 O_DIRECT 的情况下读/写 1MB 的 glob 时,它通常会将它们分成小得多的块,这对于你的 I/O 子系统来说可能会慢得多。与节省 CPU 相比,O_DIRECT 的好处更多地与节省缓存 RAM 和执行更高效的 I/O 有关。 【参考方案1】:

oflag=direct 的情况下:

您让内核能够立即写出数据,而不是填充缓冲区并等待达到阈值/超时(这反过来意味着数据不太可能在不相关的同步之后被阻止)数据)。 您正在保存内核工作(无需从用户空间到内核的额外副本,无需执行大多数缓冲区缓存管理操作)。 在某些情况下,脏缓冲区比刷新速度快会导致生成脏缓冲区的程序等待直到释放任意限制的压力(请参阅SUSE's "Low write performance on SLES 11/12 servers with large RAM")。

更一般地说,这个巨大的块大小(1 MByte)可能大于 RAID 的块大小,因此 I/O 将在内核中拆分,并且那些较小的块并行提交,因此足够大,以至于您从带有微小 I/O 的缓冲写回中获得的合并不会有太大价值(exact point that the kernel will start splitting I/Os depends on a number of factors。此外,虽然 RAID 条带大小可以大于 1 MB,但内核并不总是意识到这一点硬件 RAID。在软件 RAID 的情况下,内核有时可以针对条带大小进行优化 - 例如,我所在的内核知道md0 设备的条带大小为 4 MByte,并暗示它更喜欢该大小的 I/O通过/sys/block/md0/queue/optimal_io_size)。

鉴于以上所有情况,如果您在原始缓冲副本期间最大限度地使用单个 CPU,并且您的工作负载不会从缓存/合并中获得太多好处,但磁盘可以处理更多吞吐量,那么执行 O_DIRECT 副本应该会更快由于内核开销的减少,有更多的 CPU 时间可用于用户空间/服务磁盘 I/O。

那么为什么额外的 memcpy 会导致 2 倍以上的减速呢?使用页面缓存真的涉及更多内容吗?

这不仅仅是一个额外的 memcpy per I/O - 想想所有必须维护的额外缓存机制。在Linux async (io_submit) write v/s normal (buffered) write 问题的答案中有一个很好的explanation about how copying a buffer to the kernel isn't instantaneous and how page pressure can slow things down。但是,除非您的程序能够足够快地生成数据,并且 CPU 过载以至于无法足够快地为磁盘提供数据,否则它通常不会出现或不重要。

这是非典型的吗?

不,您的结果是非常典型的您使用的那种工作负载。不过,如果块大小很小(例如 512 字节),我想这将是一个非常不同的结果。

让我们比较一下 fio 的一些输出来帮助我们理解这一点:

$ fio --bs=1M --size=20G --rw=write --filename=zeroes --name=buffered_1M_no_fsync
buffered_1M_no_fsync: (g=0): rw=write, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=psync, iodepth=1
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][r=0KiB/s,w=2511MiB/s][r=0,w=2510 IOPS][eta 00m:00s]
buffered_1M_no_fsync: (groupid=0, jobs=1): err= 0: pid=25408: Sun Aug 25 09:10:31 2019
  write: IOPS=2100, BW=2100MiB/s (2202MB/s)(20.0GiB/9752msec)
[...]
  cpu          : usr=2.08%, sys=97.72%, ctx=114, majf=0, minf=11
[...]
Disk stats (read/write):
    md0: ios=0/3, merge=0/0, ticks=0/0, in_queue=0, util=0.00%, aggrios=0/0, aggrmerge=0/0, aggrticks=0/0, aggrin_queue=0, aggrutil=0.00%

因此,使用缓冲我们以大约 2.1 GBytes/s 的速度写入,但为此消耗了整个 CPU。然而,块设备 (md0) 说它几乎没有看到任何 I/O (ios=0/3 - 只有三个写 I/O),这可能意味着大部分 I/O 都缓存在 RAM 中!由于这台特定的机器可以轻松地在 RAM 中缓冲 20 GB,我们将使用end_fsync=1 再次运行以强制将在运行结束时可能仅在内核的 RAM 缓存中的任何数据推送到磁盘,从而确保我们记录所有数据实际到达非易失性存储所需的时间:

$ fio --end_fsync=1 --bs=1M --size=20G --rw=write --filename=zeroes --name=buffered_1M
buffered_1M: (g=0): rw=write, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=psync, iodepth=1
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [F(1)][100.0%][r=0KiB/s,w=0KiB/s][r=0,w=0 IOPS][eta 00m:00s]      
buffered_1M: (groupid=0, jobs=1): err= 0: pid=41884: Sun Aug 25 09:13:01 2019
  write: IOPS=1928, BW=1929MiB/s (2023MB/s)(20.0GiB/10617msec)
[...]
  cpu          : usr=1.77%, sys=97.32%, ctx=132, majf=0, minf=11
[...]
Disk stats (read/write):
    md0: ios=0/40967, merge=0/0, ticks=0/0, in_queue=0, util=0.00%, aggrios=0/2561, aggrmerge=0/2559, aggrticks=0/132223, aggrin_queue=127862, aggrutil=21.36%

好的,现在速度已经下降到大约 1.9 GBytes/s,我们仍然使用所有 CPU,但 RAID 设备中的磁盘声称它们有能力提高速度 (aggrutil=21.36%)。接下来是直接 I/O:

$ fio --end_fsync=1 --bs=1M --size=20G --rw=write --filename=zeroes --direct=1 --name=direct_1M 
direct_1M: (g=0): rw=write, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=psync, iodepth=1
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][r=0KiB/s,w=3242MiB/s][r=0,w=3242 IOPS][eta 00m:00s]
direct_1M: (groupid=0, jobs=1): err= 0: pid=75226: Sun Aug 25 09:16:40 2019
  write: IOPS=2252, BW=2252MiB/s (2361MB/s)(20.0GiB/9094msec)
[...]
  cpu          : usr=8.71%, sys=38.14%, ctx=20621, majf=0, minf=83
[...]
Disk stats (read/write):
    md0: ios=0/40966, merge=0/0, ticks=0/0, in_queue=0, util=0.00%, aggrios=0/5120, aggrmerge=0/0, aggrticks=0/1283, aggrin_queue=1, aggrutil=0.09%

直接使用不到 50% 的 CPU 来处理 2.2 GBytes/s(但请注意 I/O 没有合并,以及我们如何进行更多的用户空间/内核上下文切换)。如果我们要为每个系统调用推送更多 I/O,情况会发生变化:

$ fio --bs=4M --size=20G --rw=write --filename=zeroes --name=buffered_4M_no_fsync
buffered_4M_no_fsync: (g=0): rw=write, bs=(R) 4096KiB-4096KiB, (W) 4096KiB-4096KiB, (T) 4096KiB-4096KiB, ioengine=psync, iodepth=1
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][r=0KiB/s,w=2390MiB/s][r=0,w=597 IOPS][eta 00m:00s]
buffered_4M_no_fsync: (groupid=0, jobs=1): err= 0: pid=8029: Sun Aug 25 09:19:39 2019
  write: IOPS=592, BW=2370MiB/s (2485MB/s)(20.0GiB/8641msec)
[...]
  cpu          : usr=3.83%, sys=96.19%, ctx=12, majf=0, minf=1048
[...]
Disk stats (read/write):
    md0: ios=0/4667, merge=0/0, ticks=0/0, in_queue=0, util=0.00%, aggrios=0/292, aggrmerge=0/291, aggrticks=0/748, aggrin_queue=53, aggrutil=0.87%

$ fio --end_fsync=1 --bs=4M --size=20G --rw=write --filename=zeroes --direct=1 --name=direct_4M
direct_4M: (g=0): rw=write, bs=(R) 4096KiB-4096KiB, (W) 4096KiB-4096KiB, (T) 4096KiB-4096KiB, ioengine=psync, iodepth=1
fio-3.1
Starting 1 process
Jobs: 1 (f=1): [W(1)][100.0%][r=0KiB/s,w=5193MiB/s][r=0,w=1298 IOPS][eta 00m:00s]
direct_4M: (groupid=0, jobs=1): err= 0: pid=92097: Sun Aug 25 09:22:39 2019
  write: IOPS=866, BW=3466MiB/s (3635MB/s)(20.0GiB/5908msec)
[...]
  cpu          : usr=10.02%, sys=44.03%, ctx=5233, majf=0, minf=12
[...]
Disk stats (read/write):
    md0: ios=0/4667, merge=0/0, ticks=0/0, in_queue=0, util=0.00%, aggrios=0/292, aggrmerge=0/291, aggrticks=0/748, aggrin_queue=53, aggrutil=0.87%

对于 4 MB 的大块大小,由于没有剩余 CPU,因此缓冲 I/O 在“仅”2.3 GBytes/s(即使我们没有强制刷新缓存)成为瓶颈。直接 I/O 使用大约 55% 的 CPU 并设法达到 3.5 GBytes/s,因此它比缓冲 I/O 快大约 50%。

总结:您的 I/O 模式并没有真正从缓冲中受益(I/O 很大,数据没有被重用,I/O 是顺序流式传输的)所以您处于O_DIRECT 的最佳方案中快点。查看这些slides by the original author of Linux's O_DIRECT(更长的PDF document that contains an embedded version of most of the slides)了解其背后的原始动机。

【讨论】:

内核为什么要制作额外的副本?一旦数据在内核内存中,我可能会天真地期望指针只是被传递。我知道如果没有直接标志,它应该需要精确地制作 1 个副本才能将用户空间数据放入内核空间缓冲区,但不知道为什么之后需要副本。 @JosephGarvin 当然,但每次进出用户空间的转换都是另一个“副本”,因此是“副本”。从理论上讲,将数据放入特定内存区域的需要也可能导致复制,但这是特定于平台(例如具有大量内存的 32 位 Linux)的。 但是 1 个额外的副本应该只会将带宽减半。它从 14 GB/s 降到了不到 1! @JosephGarvin 您假设用户空间->内核副本只是一个 memcpy,而它还有更多内容(请参阅链接的 PDF)。您的 memcpy 测试人员可能会假设您正在从一个地方到下一个不间断地进行千兆字节的 memcpy,而您的 dd 一次只复制一兆字节,每个地方之间都有一个用户空间->内核->用户空间转换。最后,这不仅仅是您保存副本的事实 - 它是您节省的所有额外工作 - 您的 CPU 用于缓冲的繁忙程度,如果它被最大化,那么这可能是您的瓶颈。 那个 html 转换包含关于无法打印的内容的错误,你有原件吗?

以上是关于为啥带有“直接”(O_DIRECT)标志的 dd 速度如此之快?的主要内容,如果未能解决你的问题,请参考以下文章

open(2) 中的 O_SYNC 和 O_DIRECT 标志有何不同/相似?

关于O_DIRECT的那些事儿

O_DIRECT 的最小写入大小

从具有 O_DIRECT 的 HDD 读取()失败并显示 22(EINVAL,无效参数)

为啥调用带有 IRF_NO_WAIT 标志的 InternetReadFileEx 函数仍然等待?

为啥 npm 将命令行标志直接传递给我的脚本?