在过去的十年中,我几乎已经在一家相当专业的产品公司中度过了,用于构建高性能I / O系统。我有机会看到存储技术迅速而果断地发展。谈论存储及其发展感觉就像向合唱团宣讲。
今年,我换了工作。与来自多个背景的工程师相比,我感到惊讶的是,尽管我的每个同行当然都非常聪明,但他们中的大多数人对如何最好地利用现代存储技术的性能产生了误解,导致了次优的选择。设计,即使他们意识到存储技术的不断改进。
当我反思这种脱节的原因时,我意识到,这种误解持续存在的很大一部分原因是,如果他们花时间用基准来验证其假设,则数据将表明它们的假设是,或者至少看起来是真的。
“好吧,可以在这里复制内存并执行这种昂贵的计算,因为它可以节省我们一次I / O操作,这甚至会更加昂贵”。
“我正在设计一个需要快速运行的系统。因此它必须在内存中”。
“如果将其拆分为多个文件,将会很慢,因为它将生成随机的I / O模式。我们需要对此进行优化以实现顺序访问并从单个文件读取”
直接I / O非常慢。它仅适用于非常专业的应用程序。如果您没有自己的缓存,那么您注定要失败。”
但是,如果您浏览了现代NVMe设备的规格,就会发现时延在几微秒范围内的商用设备以及几GB / s的吞吐量,可支持数十万个随机IOPS。那么断开连接在哪里?
在本文中,我将证明尽管硬件在过去十年中发生了巨大变化,但软件API却没有,或者至少还不够。遗留的API充满了内存副本,内存分配,过于乐观的预读缓存以及各种昂贵的操作,使我们无法充分利用现代设备。
在撰写本文的过程中,我感到非常高兴,可以从英特尔早日获得下一代Optane设备之一。尽管它们在市场上并不常见,但它们无疑代表了越来越快的设备趋势的加冕。您将在本文中看到的数字是使用此设备获得的。
为了节省时间,我将重点放在阅读文章上。写作有自己独特的问题集,还有改进的机会,我打算在以后的文章中介绍。
当旧版API需要读取未缓存在内存中的数据时,它们会产生页面错误。然后,在数据准备好之后,产生一个中断。最后,对于传统的基于系统调用的读取,您需要向用户缓冲区添加一个副本,而对于基于mmap的操作,则必须更新虚拟内存映射。
这些操作都不是便宜的:页面错误,中断,副本或虚拟内存映射更新。但是几年前,它们仍然比I / O本身的价格便宜约100倍,使这种方法可以接受。随着设备延迟接近单位数微秒,情况已不再如此。这些操作现在与I / O操作本身的数量级相同。
快速计算得出的结论是,在最坏的情况下,与设备本身的通信成本占总繁忙成本的不到一半。这还没有算完所有的浪费,这使我们陷入第二个问题:
尽管有一些细节我会介绍(例如文件描述符使用的内存,Linux中的各种元数据缓存),但是如果现代NVMe支持许多并发操作,则没有理由相信从许多文件中读取比从中读取更为昂贵一。但是,读取的数据总量肯定很重要。
操作系统以页面粒度读取数据,这意味着一次只能读取至少4kB的数据。这意味着,如果您需要读取分为两个文件(每个512字节)的1kB读块,则实际上是在读取8kB来提供1kB的数据,浪费了87%的读取数据。实际上,操作系统还将执行默认设置为128kB的预读,以期在您以后需要剩余数据时为您节省周期。但是,如果您从不这样做(通常是随机I / O的情况),那么您只需读取256kB即可提供1kB的服务,而浪费了其中的99%。
如果您想验证我的论点,即从多个文件读取基本上不会比从单个文件读取慢,那么您可能会证明自己是正确的,但这仅仅是因为读取放大使有效读取的数据量增加了很多。
由于问题是操作系统页面高速缓存,如果仅使用Direct I / O打开文件,而其他条件都相同,会发生什么情况?不幸的是,这可能也不会更快。但这是因为我们的第三个也是最后一个问题:
文件被视为字节的顺序流,并且数据是否在内存中对读取器是透明的。传统的API会一直等到您触摸不驻留的数据以发出I / O操作。由于预读,I / O操作可能大于用户请求的操作,但仍然只是其中之一。
但是,与现代设备一样快,它们仍然比CPU慢。在设备等待I / O操作返回时,CPU未执行任何操作。
使用多个文件是朝正确方向迈出的一步,因为它可以更有效地并行处理:当一个阅读器在等待时,另一个阅读器有望继续进行。但是,如果您不小心,可能只会放大前面的问题之一:
在基于线程轮询的API中,多个文件意味着多个线程,从而放大了每个I / O操作完成的工作量。
更不用说在很多情况下都不是您想要的:开始时可能没有那么多文件。
过去,我已经写了很多有关革命性创新的文章。但是,作为一个相当低级的接口,它实际上只是API难题的一部分。原因如下:
如果使用io_uring调度的I / O使用缓冲文件,则仍然会遇到前面列出的大多数问题。
Direct I / O充满了警告,并且io_uring作为原始接口甚至都不会尝试(也不应该)隐藏以下问题:例如,内存必须正确对齐,以及您要读取的位置。
这也是非常低的水平和原始的。为了使它有用,您需要累积I / O并分批调度。这就要求制定何时执行该操作的策略,以及某种形式的事件循环,这意味着它可以与已经提供了相应机制的框架更好地协同工作。
为了解决API问题,我设计了G lommio(以前称为Scipio),这是一个直接的,面向I / O的线程/内核Rust库。 Glommio建立在io_uring的基础上,并支持其许多高级功能,例如注册缓冲区和基于轮询(无中断)的完成功能,以使Direct I / O发挥作用。为了熟悉起见,Glommio确实以类似于标准Rust API(我们将在此比较中使用的API)的方式支持Linux页面缓存支持的缓冲文件,但它的目的是使Direct I / O成为众人瞩目的焦点。
随机访问文件将位置作为参数,这意味着无需维护搜索游标。但更重要的是:它们不将缓冲区作为参数。相反,他们使用io_uring的预注册缓冲区来分配缓冲区并返回给用户。这意味着没有内存映射,没有复制到用户缓冲区-只有从设备到glommio缓冲区的副本,并且用户会获得指向该缓冲区的引用计数指针。并且由于我们知道这是随机I / O,因此不需要读取比请求更多的数据。
另一方面,流假定您最终将遍历整个文件,因此它们可以负担使用较大的块大小和预读因子的情况。
流的设计主要是Rust的默认AsyncRead所熟悉的,因此它实现了AsyncRead特征,并且仍将数据读取到用户缓冲区。基于直接I / O的扫描的所有优势仍然存在,但是内部预读缓冲区和用户缓冲区之间存在一个副本。这是对使用标准API的便利性的一种征税。
如果需要额外的性能,glommio在流中提供了一个API,该API也公开了原始缓冲区,从而节省了额外的副本。
为了演示这些API,glommio提供了一个示例程序,该程序使用所有这些API(缓冲,直接I / O,随机,顺序)对I / O进行各种设置,并评估其性能。
我们从一个大约是内存大小的2.5倍的文件开始,并通过顺序读取它作为普通的缓冲文件来简单地开始:
考虑到该文件无法容纳在内存中,这当然不错,但是这里的优点全在于英特尔Optane出色的性能和io_uring后端。每当分派I / O时,它的有效并行度仍为1,尽管OS页面大小为4kB,但预读可以使我们有效地增加I / O大小。
实际上,如果我们尝试使用Direct I / O API(4kB缓冲区,并行度为1)来模拟相似的参数,结果将令人失望,“证实”我们对Direct I / O的确慢得多的怀疑。
但是,正如我们所讨论的那样,glommio的Direct I / O文件流可以采用显式的预读参数。如果主动式glommio会在当前读取的位置之前发出I / O请求,以利用设备的并行性。
Glommio的预读与操作系统级别的预读不同:我们的目标是利用并行性,而不仅仅是增加I / O大小。 glommio不会消耗整个预读缓冲区,然后才发送新批处理请求,而是在缓冲区的内容被完全消耗后立即调度一个新请求,并将始终尝试保持固定数量的缓冲区在运行,如下图所示。
如最初预期的那样,一旦我们通过设置预读因子正确地利用了并行性,直接I / O不仅与缓冲I / O配对,而且实际上速度更快。
该版本仍使用Rust的AsyncReadExt接口,该接口强制从glommio缓冲区到用户缓冲区的额外副本。
使用get_buffer_aligned API可让您对缓冲区进行原始访问,从而避免了最后一次内存复制。如果我们现在在读取测试中使用它,则可以将性能提高4%
最后一步是增加缓冲区大小。由于这是一个顺序扫描,因此我们不需要受4kB缓冲区大小的限制,除非与OS页面缓存版本进行比较。
现在,让我们在下一个测试中使用glommio和io_uring总结幕后发生的所有事情:
io_uring设置为轮询模式,这意味着没有内存副本,没有中断,没有上下文切换。
这比标准缓冲方法高出7倍以上。更好的是,内存利用率从未超过我们设置为预读因子乘以缓冲区大小的任何东西。在此示例中,为2.5MB。
众所周知,扫描对操作系统页面缓存有害。我们如何使用随机I / O进行操作?为了测试我们将在20秒钟内读取尽可能多的内容,首先将自己限制在可用内存的前10%(1.65GB)
直接I / O比缓冲读取慢20%。尽管完全从内存中读取数据的速度仍然更高-这并不会让任何人感到惊讶,但这与人们所预期的灾难相去甚远。实际上,如果我们要记住,缓冲版本要保留1.65GB的常驻内存来实现此目的,而Direct I / O仅使用80kB(20 x 4kB缓冲区),那么对于可能最好在其他地方使用该内存。
正如任何性能工程师都会告诉您的那样,一个良好的读取基准需要读取足以打入媒体的数据。毕竟,“存储速度很慢”。因此,如果现在我们从整个文件中读取数据,我们的缓冲性能将急剧下降65%
正如预期的那样,直接I / O具有相同的性能和相同的内存利用率,而与读取的数据量无关。
如果比较大的扫描点是我们的比较点,则直接I / O比缓冲文件快2.3倍,而不是更慢。
现代NVMe设备改变了如何在有状态应用程序中最佳执行I / O的性质。这种趋势已经持续了一段时间,但到目前为止,事实已经掩盖了以下事实:API(尤其是高级API)尚未发展到能够与设备以及最近的Linux Kernel层相匹配的水平。通过正确的API集,直接I / O成为了新的选择。
最新一代的设备,例如最新一代的英特尔Optane,就可以达成协议。毫无疑问,标准缓冲I / O绝对比直接I / O好。
对于扫描,量身定制的基于Direct I / O的API的性能简直优越得多。而且,尽管对于完全适合内存的随机读取,缓冲I / O标准API的执行速度提高了20%,但代价是内存利用率提高了200倍,因此折衷方案并非一帆风顺。
确实需要额外性能的应用程序仍将需要缓存其中一些结果,glommio正在提供一种简便的方法来集成专用缓存以与Direct I / O结合使用。