查看原文
其他

在NVMe驱动器时代,顺序IO仍然重要吗?

常华Andy Andy730 2024-03-16
Source: Jack Vanlightly,Is Sequential IO Dead In The Era Of The NVMe Drive? May 9, 2023

我非常熟悉的两个系统,Apache BookKeeper和Apache Kafka,是在磁盘、硬盘驱动器或HDD时代设计的。硬盘驱动器擅长顺序 IO,但由于寻道时间相对较长,因此不擅长随机 IO。难怪Kafka和BookKeeper在设计时都考虑了顺序IO。

Kafka 和 BookKeeper 都是分布式日志系统,因此您会认为顺序 IO 是仅追加日志存储系统的默认值。但是顺序和随机 IO 位于连续统一体上,一侧是纯顺序,另一侧是纯随机 IO。如果您有 5000 个文件,您以循环方式以小型写入方式追加并执行 fsync,则这不是这样的顺序 IO 访问模式,它位于随机 IO 端的更远处。因此,仅仅作为仅追加日志并不意味着您可以获得顺序 IO。

因此,在HDD时代,系统厂商将顺序IO设计到他们的系统中。Apache BookKeeper 竭尽全力通过确保一次只有一个活动文件来实现顺序 IO。它通过将来自不同逻辑日志的数据交错到一个物理日志中来实现这一点。像这样的交错对于写入来说很好,但读取成为一个问题,因为我们不再获得顺序读取。为了解决这个问题,BookKeeper 将数据写入两次:一次写入优化的预写日志 (WAL),然后再次写入长期读取优化存储。为了使长期存储读取优化,BookKeeper 在大型写入缓存中累积写入条目,然后定期对缓存进行排序并将其写入一个活动文件(一次)。按日志 ID 和条目 ID 排序可确保相关数据写入连续块,从而使读取更加连续。我们只需要添加一个可以指向这些连续块的索引。

通常,我们将WAL放在一个磁盘上,将长期存储放在另一个磁盘上。对 WAL 的写入是纯顺序的,对长期存储的写入是纯顺序的。读取可能偶尔必须命中索引,但通常也是顺序的。使用单个活动 WAL,BookKeeper 可以像每 1 毫秒一样同步一次,而且不会太耗时 —— 我们一次只写入一个文件。我们可以通过添加更多磁盘并创建更多 WAL 和长期存储引擎实例来横向扩展 WAL 和长期存储,每个实例都有自己的线程池。

Apache Kafka采用了不同的方法来实现顺序IO。它一次将一个分区映射到一个活动段文件,起初听起来很糟糕。如果代理托管 1000 个分区,那么它将一次写入 1000 个文件。这可能会变得耗时,尤其是对于 HDD。为了解决这个问题,Kafka有两个导入设计点。首先,它被设计为异步写入磁盘,它依赖于页面缓存将数据刷新到磁盘,这会导致更大的 (顺序)数据块写入磁盘。这降低了写入如此多打开文件的成本。最后,异步写入磁盘是不安全的,除非您还构建复制协议来处理日志头的任意丢失。以异步方式写入磁盘时,例如,如果服务器崩溃,则可能会丢失一些最近写入的条目。我最近写了一篇关于Kafka的恢复机制内置于其复制协议中的文章,该机制允许它使用这种异步日志写入。

NAND闪存时代

但是在当今的SSD世界中,这种设计理念已经过时了吗?高性能 SSD 可以在随机 IO 工作负载上提供高吞吐量和低延迟,那么这是否意味着我们应该抛弃顺序 IO?我们是否应该将分布式日志存储系统设计为利用随机 IO,从而摆脱这些技巧来实现顺序 IO?

事实是,SSD 驱动器(包括 NVMe)与 IO 访问模式无关。阿里云写了两篇关于影响 NVMe 驱动器性能的因素的有趣文章。NVMe 驱动器的一个有趣方面是所需的内务管理量,例如磨损均衡和垃圾收集。

磨损均衡可防止 NAND 块因读取和写入周期过多而磨损。它通过将热数据重新定位到磨损较少的块来实现这一点。垃圾回收是另一个内务处理过程,当碎片导致缺少可用块时,驱动器控制器会重写数据块。

在 NVMe 驱动器内,数据被写入页面(通常为 4 Kb),页面属于块(通常每个块 128 页)。

驱动器控制器可以直接写入空页,但不能覆盖页。控制器也无法擦除单个页面,只能擦除整个块。当控制器想要覆盖页面时,它实际上只是写入可用的空页面,更新逻辑到物理映射表并将旧页面标记为无效。这些无效页面会累积并且无法写入,因此控制器必须定期执行内务处理来处理无效页面 —— 此过程称为垃圾回收 (GC)。如果没有GC,您很快就会耗尽驱动器上的空间,因为所有块都将充满有效和无效页面。

GC 的工作方式是读取一个块并将所有有效页面重写为空块,更新映射表并擦除原始块。

这种重写会导致写入放大,因为对于写入的每一页,都会有一定量的重写。驱动器上的所有这些后端流量都会降低性能,还会缩短驱动器的使用寿命。

仅删除文件不会擦除驱动器中的所有页面。相反,这些页面被标记为无效,GC 进程最终将通过上述流程释放这些无效页面。

这意味着,一旦整个驱动器被写入一次,驱动器的吞吐量现在仅限于 GC 进程的吞吐量 —— GC 必须执行的工作越多,吞吐量就越低,磁盘操作的延迟就越高。

这就是顺序与随机 IO 问题变得相关的地方。顺序 IO 填充整个块,而随机 IO 倾向于将写入分散在块之间,从而将任何给定文件碎片化到多个块中。在空驱动器上,此差异没有影响,因为有大量可用块可用。但是,一旦驱动器处于负载状态数天和数周,此碎片就会对 GC 产生重大影响。

顺序 IO 会导致较低的 GC 开销,因为它会填充整个块,然后,当文件系统中的文件被删除时,整个块将变为无效。GC 进程不必重写仅由无效页面形成的块的页面。它只是擦除块。但是,对于随机 IO,所有块都混合了有效和无效数据,因此在擦除块以进行新写入之前,必须重写现有的有效块。这就是为什么顺序 IO 负载的写入放大接近 1,但随机 IO 通常要高得多。

最终,您的驱动器最终将包含其大部分非保留块,其中包含陈旧和正在使用的页面的混合。此时,新写入可能会导致显著的写入放大和性能下降,因为控制器将使用中的页面随机排列到其他清除的块中。一页的写作最终可能会在一连串的清理中移动数百页其他页面。

预留空间作为解决方案

但对于随机IO来说,这并不全是坏消息。通过为驱动器提供仅为其自身保留的额外空间,可以降低 GC 的成本,该空间可用于 GC。驱动器的可用空间越少,写入放大率就越高,性能下降幅度越大。通常,一旦我们传递了包含有效数据的驱动器的 50%,我们就会看到随机 IO 对性能的某种影响,随着我们达到 100%,该影响会越来越大。

预留空间 (OP,Over-provisioning) 是仅为驱动器控制器保留空间的概念。例如,当 OP 为 7% 时,驱动器控制器将 7% 分配给自己,操作系统无法写入它。企业级 SSD 内置一定比例的预留空间,而 AWS 本地实例存储 NVMe 驱动器则没有。自己实现 OP 有不同的技术。您可以简单地将驱动器的一部分保留为未分区或使用 hdparm 等工具。

进一步增加预留空间的空间可以减少 GC 的后端流量,但代价是存储密度降低和成本增加。闪存和驱动器制造商早就知道写入放大问题及其对随机 IO 工作负载的敏感性。尽管如此,这种限制经常被忽视,许多人都没有意识到这一点。

2010年的一篇论文,题为“闪存随机写入性能的基本极限:理解,分析和性能建模”(X.-Y. Hu, R. Haas) 涵盖了平均写入大小、控制器、垃圾回收算法和随机 IO 的交互和限制。他们发现,使用随机 IO,一旦盘利用率达到大约 2/3,性能就会真正开始断崖式下降。如图 7 所示,即使改变写入有效负载大小,对实际减速因子的影响也很小。

NVMe驱动技术仍在进步,但我们仍然面临着碎片化和内务管理等基本问题。《闪存随机写入性能的基本极限》一书的作者在论文结束时发表了以下观察: 

“闪存SSD的随机写入性能差及其性能下降可能是由设计/实现伪影引起的,随着技术的成熟,这些伪影可以消除,或者由于独特的闪存特性而导致的基本限制。识别和了解闪存 SSD 的基本限制不仅有利于构建高级闪存 SSD,还有助于以最佳方式将闪存集成到当前内存和存储层次结构中。”

我发现结论的这一部分很重要,特别是因为它是 13 年前写的。从那时起,技术肯定有所改进,具有更大的驱动密度、更好的控制器和更好的 GC 算法。但即使是最新的尖端 NAND 驱动器也面临着页面碎片和垃圾回收需求等同样的基本问题。

2022 年的一篇更现代的论文《通过 NVMe 接口中的地址重映射提高 I/O 性能》讨论了通过将随机 IO 转换为顺序 IO 的重映射算法来克服随机 IO 问题,并取得了一些有希望的实验结果。

上图显示了 8 核英特尔酷睿 i9-9900K、16GB 内存和三星 PM1725b NVMe SSD 上的随机写入吞吐量。

本文还有一个广泛的“相关工作”部分,其中包含许多其他关于 NVMe 驱动器性能优化的有趣研究的链接。

回到最初的问题

因此,我回到了顺序IO在NVMe驱动器时代是否已经死亡的问题,以及第二个问题,即为顺序IO设计的Apache Kafka和Apache BookKeeper现在是否已经过时了。在我看来,即使在NAND闪存的新时代,顺序IO的好处也依然存在。顺序 IO 对所有驱动器类型,尤其是 SSD 具有更多的共鸣,减少了写入放大,从而提高了性能,允许您使用更多的驱动器,因为预留空间不太重要,并延长了驱动器的使用寿命。
继续滑动看下一个
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存