Rethinking File Mapping for Persistent Memory
摘要
持久主存 (PM) 显着提高了 IO 性能。我们发现这导致 PM 上的文件系统花费多达 70% 的 IO 路径在实际工作负载上执行文件映射(将文件偏移量映射到存储介质上的物理位置)。然而,即使是 PM 优化的文件系统也会根据几十年前的假设执行文件映射。现在重新访问 PM 的文件映射至关重要。
我们通过在 PM 优化文件的上下文中构建和评估几种文件映射设计,包括不同的数据结构、缓存以及元数据和块分配方法来探索 PM 文件映射的设计空间系统。基于我们的发现,我们设计了 HashFS,一种基于散列的文件映射方法。 HashFS 对所有映射和分配操作使用单个哈希操作,绕过文件系统缓存,而是通过 SIMD 并行预取映射并显式缓存翻译。与其他替代方案相比,HashFS 的低延迟提供了卓越的性能。与最先进的 Strata PM 优化文件系统中的页面缓存范围树相比,HashFS 将 LevelDB 上的 YCSB 的吞吐量提高了 45%。
1 介绍
持久性主存储器(PM,也称为非易失性主存储器或 NVM)是一种新的存储技术,它弥补了传统慢速存储设备(SSD、HDD)和快速、易失性随机存取存储器(DRAM)之间的差距。 英特尔傲腾 DC 持久内存模块 [20] 以及其他 PM 变体 [1、2、4、30、35、50] 预计将与传统 DRAM 一起在 DIMM 插槽中变得司空见惯。 PM 提供字节可寻址的持久性,延迟仅比 DRAM 高 2-3 倍,这对文件系统设计人员非常有吸引力。 事实上,之前的工作已经重新设计了许多专门针对 PM 的文件系统组件,从而减少了 IO 路径中的开销(例如,通过消除内存副本和绕过内核)[11、14、24、29、48、52、56]。
然而,并非 IO 路径的所有方面都经过详细检查。 令人惊讶的是,迄今为止,文件映射在 PM 优化文件系统中几乎没有受到关注。 文件映射——从逻辑文件偏移量到底层设备物理位置的转换——在 PM 优化文件系统中占实际工作负载 IO 路径的 70%(如第 4.8 节所示)。 即使对于内存映射文件,文件映射仍然涉及文件追加。 然而,现有的 PM 优化文件系统要么简单地重用最初为较慢的块设备 [29] 设计的映射方法 [11、14、24],要么在没有严格分析的情况下设计新方法 [15、57]。
PM 为文件映射带来了许多需要考虑的挑战,例如处理碎片和并发问题。 值得注意的是,PM 接近 DRAM 的延迟需要重新考虑文件映射的许多方面,例如映射结构布局、页面缓存在维护文件映射结构的易失性副本中的作用以及物理空间分配的开销。 例如,一些 PM 优化的文件系统在 DRAM 的页面缓存中维护其块映射结构的副本,这对于较慢存储设备上的文件系统来说是传统的 [11、24、29],但其他文件系统则没有 [14、15、 48] 而是依赖 CPU 缓存。 目前还不清楚在 DRAM 中维护映射结构的易失性副本与依赖 CPU 缓存来缓存映射结构或设计专门的文件映射缓存相比是否有益。
在这项工作中,我们在 PM 优化文件系统的背景下构建并严格评估了一系列文件映射方法。 PM 的随机访问字节寻址能力使得设计完全随机访问映射结构(即哈希表)成为可能,从而扩展了可能的文件映射结构的设计空间。为此,我们评估和 PM 优化了两种经典的每个文件映射方法(即,每个文件都有自己的映射结构),它们使用范围树和基数树。我们提出并评估了两种进一步的全局映射方法(即,单个结构映射整个文件系统中的所有文件)。第一个使用布谷鸟散列,第二个是 HashFS,一种使用线性探测的组合全局散列表和块分配方法。我们使用这些方法来评估一系列设计点,从强调顺序访问性能到强调最小化内存引用、缓存和非缓存以及物理空间分配的作用。
我们在一系列基准测试中评估这些文件映射方法,这些基准测试不同工作负载、访问和存储模式、IO 大小和结构大小下的文件系统 IO 延迟和吞吐量。*我们展示了在 PM 优化文件系统 Strata [29] 中使用 PM 优化文件映射如何显着降低文件 IO 操作的延迟并大幅提高应用程序吞吐量。*在 LevelDB [17] 上运行的 YCSB [10] 工作负载上对 Strata 的评估显示,使用 PM 优化文件映射 (§4),吞吐量提高了 45%。我们的分析表明文件映射的性能独立于文件系统,允许我们的设计应用于其他 PM 文件系统。
先前的工作研究了 PM 优化存储结构的设计 [31,36,54,59] 和 PM 优化键值存储的索引结构 [28,51,58]。这些结构在内存映射文件之上运行并依赖于文件映射以从物理内存问题中抽象出来。此外,PM 优化的存储和索引结构设计为具有每个结构的一致性(例如,影子分页)[31,54,59]。文件系统已经提供了跨多个元数据结构的一致性,这使得每个结构的方法变得多余且昂贵。在我们的评估中,我们表明存储结构在文件映射(第 4.9 节)方面表现不佳。
总之,我们做出以下贡献:
• 我们介绍了四种文件映射方法的设计和实现,并探索了对它们的各种 PM 优化(第 3 节)。我们在 Strata 文件系统 [29] 中实现了这些方法,它提供了最先进的 PM 性能,特别是对于文件映射最重要的小型随机 IO 操作(第 4.4 节)。我们对页面缓存、映射结构大小、IO 大小、存储设备利用率、碎片、隔离机制和文件访问模式如何影响这些文件映射方法的性能进行了广泛的实验分析(§4)。
• 我们展示了我们的 PM 优化的 HashFS 是性能最佳的映射方法,因为它在随机读取和插入期间内存开销低且延迟低(第 4.1 节)。我们证明,与 Strata 在 YCSB 工作负载中的页面缓存范围树相比,这种方法可以将吞吐量提高多达 45%(第 4.8 节)。
• 我们表明,使用传统的页面缓存对 PM 优化文件映射没有好处(第 4.7 节)。我们还证明了 PM 优化的存储结构不适合用作文件映射结构,并且不能简单地放入 PM 优化的文件系统中(第 4.9 节)。
2 文件映射背景
什么是文件映射? 文件映射是将文件中的逻辑偏移映射到底层设备上的物理位置的操作。*文件映射由文件系统维护的一个或多个元数据结构(文件映射结构)实现。*这些文件映射结构以固定的粒度将逻辑位置(文件和偏移量)映射到物理位置(设备偏移量)。文件映射结构具有三个操作:查找,由现有位置的文件读写引起;插入,由文件追加或 IO 导致稀疏文件的未分配区域;和删除,由文件截断(包括文件删除)或稀疏文件中的区域取消分配引起。插入和删除需要物理位置的(解除)分配,通常由维护单独的空闲块结构的块分配器提供。 PM 文件系统通常以至少 4KB 的块粒度映射文件,以限制跟踪文件系统空间所需的元数据量 [15,29,52]。块大小通常对应于内存页面大小,允许基于块的 PM 文件系统更有效地支持内存映射 IO。
2.1 文件映射挑战
以下属性使设计高效的文件映射具有挑战性。必须仔细考虑在这些属性所呈现的权衡空间中找到点,以最大限度地提高给定工作负载和存储设备的性能。 PM 加剧了许多这些属性的影响。
碎片化。 当文件的数据分布在存储设备上的非连续物理位置时,就会出现碎片。当文件无法分配到设备的单个连续区域中,或者当文件增长并且所有相邻块已分配给其他文件时,就会发生这种情况。随着文件系统的老化 [42],碎片化是不可避免的,并且会迅速发生 [9, 23]。
碎片化会放大某些文件映射设计的开销。许多传统文件系统使用紧凑的文件映射结构,如扩展树(第 3.1 节),它可以使用单个扩展条目来映射大范围的连续文件位置。碎片化导致位置变得不连续并且映射结构变得更大,这会增加搜索和插入时间(§4)。这被称为逻辑碎片,是文件系统 [19] 中开销的主要来源,尤其是在 PM 上(因为文件映射构成了 PM 文件系统上 IO 路径的较大部分)。碎片化的另一个后果是减少了顺序访问。碎片化导致设备上数据结构位置的分离,这会导致设备上出现额外的随机 IO。这会降低 IO 性能,尤其是在 PM 设备上 [22]。
在文件映射过程中,必须将碎片视为不可避免的情况,因为碎片整理并非总是可行或不可取的。碎片整理是一项昂贵的操作——它会导致许多文件写入,在 SSD [19] 的情况下,这会使设备寿命降低 10% 以上。 PM 也存在类似的问题,这可能使得碎片整理对于 PM 优化的文件系统来说也是不可取的 [18, 59]。
参考地点。 访问文件时的引用位置可用于减少文件映射开销。具有局部性的访问通常通过缓存先前的访问和预取相邻的访问来加速。操作系统页面缓存和 CPU 缓存可以透明地实现这一点,我们将在 §2.2 中讨论操作系统页面缓存在 PM 文件映射中的作用。但是,特定于文件映射的方法可以产生更多的好处。例如,我们可以通过记住先前查找的元数据位置并预取下一次查找的位置来隐藏部分文件映射结构遍历开销以进行局部访问。此外,使用有效的缓存,文件映射结构本身应该针对随机(即非本地)查找进行优化,因为该结构主要用于未缓存的映射。
映射结构大小。 文件映射结构的总大小作为可用文件系统空间的一部分是文件映射效率的重要指标。理想情况下,文件映射结构会占用一小部分可用空间,为实际文件数据存储留出空间。此外,映射结构的大小会影响可以保持缓存驻留的数据量。
传统的文件映射结构被设计为具有弹性——结构的大小与文件系统中分配的位置数量成正比。这意味着随着文件数量和大小的增加,文件映射结构的大小也会增加。然而,弹性映射结构通过需要调整大小来引入开销,这会产生相关的空间(取消)分配和管理成本。
并发。 提供隔离(确保并发修改不会影响一致性)可能是一项代价高昂且可扩展性有限的操作。这是支持任意范围操作的数据库映射结构(称为索引)的一个众所周知的问题。例如,基于树的范围索引条目的插入和删除可能需要更新内部树节点,这些节点与对不相关键的操作[16、49]进行竞争。
在每个文件映射结构中,隔离更简单,其中可能同时发生的操作的种类和分布是有限的。除稀疏文件外,更新仅在文件大小发生变化时发生,即在追加新块或截断以删除块时。文件读取和就地写入(写入已分配的块)只会导致文件映射结构读取。出于这个原因,通常使用覆盖整个结构的粗粒度读写锁来保护每个文件的映射结构就足够了[29]。
每个文件映射结构并发访问的最常见场景是由具有一个写入器(通过追加或截断更新文件映射结构)和并发读取器(文件读取和写入现有偏移量)的工作负载呈现的。在这种情况下,文件映射读取和写入可以继续进行,而不会在常见情况下发生争用。其他映射结构操作可能会影响并发性,但它们通常很少发生。例如,某些文件映射结构需要偶尔调整大小。为了保持一致性,此操作发生在关键部分内,但仅当结构增长超过阈值时才需要。范围树可以拆分或合并范围,这些操作需要部分树锁定(锁定内部节点),但仅在更新时发生。
对于全局文件映射结构,争用的可能性更高,因为可能存在并发写入者(追加/截断不同的文件)。全局文件映射结构的设计必须考虑到并发性。在这种情况下,争用不利于使用基于树的索引,但对于具有每桶锁的哈希表结构来说是易于处理的,因为写入者之间的争用仅在哈希冲突时发生。
2.2 文件映射非挑战
碰撞一致性。 文件系统的一个首要问题是提供一致性——将元数据结构从一种有效状态转换到另一种状态,即使在系统崩溃时也是如此。文件系统有许多元数据结构,通常需要在崩溃时自动更新,即为文件分配新位置时的空闲列表和文件映射结构。文件系统通常采用某种形式的日志记录,以确保这些更新可以重放并保持原子性和一致性,即使在崩溃的情况下也是如此。这使得崩溃一致性成为文件映射的非挑战。这与 PM 持久数据结构不同,后者通常旨在提供结构本身内的崩溃一致性。
页面缓存。 传统上,文件系统在将数据和元数据(包括文件映射结构)提供给用户之前将其读取到 DRAM 中的页面缓存中。文件系统使用此页面缓存批量更新并写回脏页。这是必要的优化,以减少每次 IO 操作直接从块设备读取的开销。但是,页面缓存有开销。必须管理 DRAM 页面池并在打开新文件时将其重新分配给新文件,并且必须读取和写回缓存中的页面以确保更新是一致的。
对于 PM,文件映射结构可能不再需要 DRAM 中由操作系统管理的页面缓存。一项分析 [48] 发现,避开 inode 和目录的页面缓存可以提高 PM 优化文件系统的性能。到目前为止,对于文件映射结构的最佳设计点还没有达成共识:一些 PM 文件系统仅在 PM [15] 中维护文件映射结构,而另一些则仅在 DRAM [52、56] 中维护文件映射结构,还有一些人在 DRAM [11, 29] 中管理基于 PM 的文件映射结构的缓存。然而,正如我们在第 4.7 节中所展示的,页面缓存管理被证明是一项非挑战,因为在 PM 优化的文件系统上为文件映射结构绕过页面缓存总是更有效。
3 PM文件映射设计
基于我们对挑战的讨论(第 2.1 节),我们现在描述四种 PM 优化文件映射方法的设计,我们在第 4 节中对其进行分析。 我们首先描述了两种传统的、每个文件的映射方法及其 PM 优化,然后是两种全局映射方法。 我们讨论了每种方法面临的独特挑战,然后描述了该方法的映射结构。
3.1 传统的,每个文件映射
在大多数传统文件系统中,文件映射是按文件完成的。每个文件都有自己的弹性映射结构,它随着结构需要维护的映射数量而增长和缩小。映射结构是通过一个单独的、固定大小的 inode 表找到的,该表由文件的 inode 编号索引。每个文件映射通常提供高空间局部性,因为单个文件的所有映射都组合在一个结构中,该结构传统上缓存在 OS 页面缓存中。相邻的逻辑块通常表示为映射结构中的相邻映射,从而导致更有效的顺序文件操作。由于受限的每个文件访问模式(第 2.1 节),并发访问是每个文件映射方法的一个小问题。物理块分配是通过一个单独的块分配器执行的,该块分配器是在我们的测试平台文件系统(§4)中使用红黑树实现的。
挑战。 每个文件的映射结构必须支持调整大小,这可能是一项昂贵的操作。由于它们需要增长和缩小,每个文件的映射结构具有多个间接级别,每个映射操作需要更多的内存引用。碎片化破坏了这些结构的紧凑布局,并通过增加间接数量和内存访问来加剧开销。
我们现在讨论用于每个文件映射的两种常见映射结构。
3.1.1 范围树
范围树是经典的每个文件映射结构,但仍在现代 PM 优化文件系统中使用 [11, 29]。 扩展区树是包含扩展区的 B 树,它将文件逻辑块映射到物理块,以及一个指示映射表示的块数的字段。 范围也可以是间接的,指向更远的范围树节点而不是文件数据块。 为了执行查找,在每个节点上执行二进制搜索以确定哪个条目包含所需的物理块。 这种搜索是在扩展树的每一层上执行的,如图 1 所示。
我们基于 Linux 内核 [33] 的实现创建了范围树的 PM 优化变体。 传统上,扩展树操作是在页面缓存中的树块的副本上执行的。 由于对 PM 映射结构使用页面缓存会导致不必要的复制开销(参见第 4.7 节),因此我们将扩展树设计为直接在 PM 上操作。 并发范围访问下的一致性由每个范围条目有效位保证,该位仅在可读取范围时启用。 撤消日志提供了复杂操作(调整大小、分割范围等)的一致性。 我们还在最后访问路径的 DRAM 中保留了一个光标 [5],从而提高了局部访问的性能。
设计注意事项。 范围树具有我们评估的任何映射结构的最紧凑表示,因为多个映射可以合并为一个范围,从而导致结构尺寸小。 然而,extent 树需要许多内存访问,因为它们必须在extent 树的每个级别执行二进制搜索以找到最终映射。 游标可以简化搜索,但仅适用于通过结构的顺序扫描和重复块访问。
3.1.2 基数树
基数树是另一种流行的按文件映射结构 [15],如图 2 所示。基数树中的每个节点都占用设备上的一个物理块(通常为 4KB),但可以存储在其中的一些条目除外直接inode。查找从顶级节点开始,解析逻辑块号的前 N 位。使用 8 字节指针和每个节点 4KB,我们可以解决 N = l o g 2 ( 4096 8 ) = 9 b i t s N = log_2 (\frac{4096}{8}) = 9 bits N=log2(84096)=9bits每个基数树节点。第二级节点解析接下来的 N N N 位,以此类推。基数树动态增长和收缩以使用包含映射数量所需的尽可能多的级别(例如, N < 9 N < 9 N<9 的文件只需要单级树)。最后一级节点包含到物理块的直接映射。为了加速顺序扫描,DRAM 中的游标通常用于缓存在树中搜索到的最后一个位置。由于每个指针只是一个 64 位整数(物理块号),因此可以通过 CPU 的原子指令来保证一致性。通过首先在撤消日志中记录修改来提供调整大小的一致性。
设计注意事项。 基数树不如范围树紧凑,通常需要更多的间接性。因此,树遍历平均需要更长的时间,因为基数树比紧凑范围树生长得更快。然而,基数树具有计算上最简单的映射操作——一系列偏移量计算,每级树只需要一次内存访问。因此,它们通常比扩展树执行更少的内存访问,因为扩展树对每个扩展树节点进行 O ( l o g ( N ) ) O(log(N)) O(log(N)) 次内存访问(二进制搜索)以找到要遍历的下一个节点。
3.2 PM 特定的全局文件映射
基于我们对文件映射性能和每个文件映射方法带来的挑战的分析,我们设计了两种特定于 PM 的映射方法,与现有 PM 优化文件系统中使用的每个文件映射方法不同,它们是全局的。这意味着,这些方法不是每个文件都有一个映射结构,而是将一个文件(由其 inode 编号 (inum) 表示)和一个逻辑块映射到一个物理块。因此,它们不需要单独的 inode 表。
我们设计的两个全局映射结构都是哈希表。这些设计背后的直觉是利用哈希表结构所需的少量内存访问,但要避免随着文件的增长和缩小而调整每个文件映射结构的大小的复杂性。我们能够静态地创建这些全局结构,因为文件系统的大小(物理块的最大数量)在文件系统创建时是已知的。哈希表不受碎片的影响,因为它们不使用紧凑的布局。由于平面寻址方案,并发访问下的一致性通常可以在每个哈希桶的基础上解决。
我们在 DRAM 中采用固定大小的转换缓存,缓存逻辑到物理块的转换,以加速本地查找。为简单起见,它使用与完整哈希表相同的哈希值进行索引。固定大小转换缓存的目标是为哈希表结构提供一种机制,该机制类似于范围和基数树用于加速顺序读取的恒定大小游标 [5]。该缓存包含 8 个条目,并嵌入在我们的测试平台文件系统 Strata 维护的 DRAM 内 inode 结构中(第 4 节)。这种固定大小的翻译缓存不同于页面缓存,后者缓存映射结构,而不是翻译。转换缓存减少了顺序读取操作所需的 PM 读取次数,这得益于在文件系统操作期间访问的 DRAM inode 结构的缓存局部性(第 4.1 节)。
全球结构挑战。 由于这些全局映射结构是哈希表,因此由于哈希方案的随机性,它们表现出较低的局部性。 这些全局哈希表可能表现出比每个文件哈希表可能遇到的更低的局部性,因为每个文件哈希表将只包含与该文件相关的映射,其中全局哈希表中的映射可能随机分布在 更大的内存区域。 然而,翻译缓存改善了这一挑战并加速了局部映射。
我们现在讨论两种全局文件映射哈希表设计。
3.2.1 全局布谷鸟哈希表
我们在图 3 中展示了第一个全局哈希表结构图。在这个哈希表中,每个条目映射 ,:(<# of blocks>)。该哈希表使用布谷鸟哈希 [38],这意味着每个条目使用两个不同的哈希函数进行两次哈希处理。对于查找,最多必须咨询两个位置。对于插入,如果两个潜在的插槽都已满,则插入算法会选择现有条目之一并通过将其移动到其备用位置将其逐出,继续传递直到不再有冲突。我们在此设计中使用布谷鸟哈希而不是线性探测,以避免在病态情况下必须遍历潜在的长链冲突,将查找单个索引所需的内存访问次数限制为 2。
哈希表被设置为一个连续的数组,在文件系统创建时静态分配。通过在持久化密钥(inum,逻辑块)之前首先持久化映射信息(物理块号,大小)来确保一致性,有效地将密钥用作有效指示符。复杂插入的一致性(即,导致先前条目打乱的插入)由文件系统撤消日志中的第一次记录操作来维护。由于复杂的更新对于原子比较和交换操作来说太大了,我们使用 Intel TSX [21] 代替每个条目的锁来提供隔离。这种隔离是必需的,因为插入可能会在文件间同时发生——这对于每个文件的映射结构来说不是挑战,它可以依赖每个文件的锁定机制进行隔离。
哈希表的一个问题是它们通常呈现一对一的映射,这不利于像传统映射结构那样表示连续分配的块的范围。作为补偿,该哈希表中的每个条目还包含一个字段,该字段包括与该条目相邻的文件块的数量。为一系列连续块中的每个块维护此映射;例如,如果逻辑块 21…23 连续映射到设备块 100…102,则哈希表将具有大小为 3 的 100(在图 3 中显示为 100(3))、大小为 2 的 101 的条目,等等。每个条目还包含一个反向索引字段,该字段描述了在连续范围内有多少块在它之前,因此如果从连续块范围的末尾删除单个块,则可以相应地更新组中的所有条目。此哈希表还可以对多个块进行并行查找(例如,通过使用 SIMD 指令并行计算哈希),以便在碎片很高的情况下对范围访问(即一次读取多个块)进行更有效的查找。远程节点对于大型 IO 操作的性能至关重要(我们将在 §4.4 中进一步讨论)。范围节点也缓存在转换缓存中,以加速小型的顺序 IO 操作。
设计注意事项。在布谷鸟散列和线性探测之间存在权衡。 Cuckoo hashing 最多访问 2 个位置,但它们随机位于哈希表内,导致空间局部性较差。当存在冲突时,线性探测可能会访问更多位置,但它们是连续的,因此相对于第一次查找具有较高的空间局部性。因此,具有低负载因子的线性探测哈希表平均只能访问 1 或 2 个缓存行,这在实践中将优于布谷鸟哈希表。我们通过将此全局哈希表方案与线性探测方案(在第 3.2.2 节中描述)进行比较来探索这种权衡,以确定哪种方案在实践中具有更好的性能——我们在微基准测试中分别测量查找和修改的延迟(§4.1–§4.5)和我们的应用程序工作负载中的端到端性能(§4.8)。
3.2.2 哈希FS
我们还设计了第二个全局哈希表,旨在专门降低插入操作的成本,这通常涉及与块分配器的交互。为此,我们构建了一个也提供块分配的单个哈希表,我们称之为 HashFS。我们在图 4 中展示了这种结构的示意图。
HashFS 分为元数据区域和文件数据区域。物理块存储在文件数据区域中,该区域从 fileDataStart 开始。查找首先解析到元数据区域。对应的物理块位置是根据元数据区域中条目的偏移量计算得出的。例如,如果 <inum=1, lblk=21> 的哈希解析为元数据区域中的偏移量 i,则对应物理块的位置为 (fileDataStart +i×blockSize),blockSize = 4KB。
与 cuckoo 哈希表(第 3.2.1 节)不同,该表没有任何范围节点,而是提供逻辑块和物理块之间的纯一对一映射。这使用文件系统中每 4KB 数据块 8 字节的恒定空间,总空间开销 < PM 的 0.2%。这种方案的另一个优点是哈希表条目非常简单——一个组合的 inum 和逻辑块,适合 64 位整数。因此,可以简单地通过内在的原子比较和交换来保证一致性。
为了有效地实现大 IO 和顺序访问,我们使用 SIMD 指令执行向量散列操作。由于 PM 的加载/存储寻址能力和足够的带宽,这是可能的。如果 IO 操作未使用最大 SIMD 带宽,则剩余带宽用于预取后续条目,然后将其缓存在转换缓存中。全局哈希表使用线性探测,将冲突的哈希值插入到同一区域的偏移槽中[8]。这与分离链不同,分离链为链表分配单独的内存来处理冲突。线性探测的一个优点是冲突条目存储在相邻位置,通过避免 PM 缓冲区中的未命中来减少搜索冲突的开销 [43]。此外,与杜鹃散列不同,线性探测从不重新定位条目(即重新散列),它保留了元数据条目的索引和文件数据块之间的对应关系。如前所述,我们在评估中衡量了线性探测和布谷鸟散列的权衡(§4.1-§4.5 和 §4.8)。
HashFS 从根本上不限于 8 字节条目。其他原子更新技术可以确保隔离(例如,英特尔 TSX),而不是对 8 字节条目执行原子比较和交换操作。无论 HashFS 条目的大小如何,现有的日志记录机制都提供崩溃一致性。即使条目较大(例如,小于 16 字节条目的文件系统总容量的 0.4%),总体空间开销也很低。
设计注意事项。 HashFS 与全局布谷鸟哈希表一样,空间局部性也很低,但通过 SIMD 并行性预取提高了顺序访问。 HashFS 的计算开销平均较低,因为 HashFS 每次查找只需计算一个哈希函数。然而,冲突代价高昂,因为它们是通过线性跟踪条目链来解决的。这意味着 HashFS 在高负载因子下(即文件系统变得更满)可能表现更差。
4 评价
我们在一系列微基准和应用程序工作负载上对我们的 PM 文件映射方法的性能进行了详细评估,并讨论了每个映射结构的性能特征。然后,我们证明我们在第 2.2 节中讨论的非挑战在 PM 优化文件系统中确实是非挑战。我们在表 1 中总结了我们在分析中回答的问题。
实验装置。 我们在带有四个 128GB Intel Optane DC PM 模块的 Intel Cascade Lake-SP 服务器上运行我们的实验 [20]。为了执行我们的分析,我们将我们的映射方法集成到 Strata 文件系统 [29] 中。我们选择 Strata 进行分析,因为它是性能最好的开源 PM 优化文件系统之一,被最先进的研究[39]积极使用。我们将 Strata 配置为仅使用 PM(即不使用 SSD 或 HDD 层)。我们的文件映射结构被集成到 Strata 的用户空间组件(LibFS,只读取映射结构)和 Strata 的内核空间组件(KernFS,它根据“消化”的用户更新日志读取和更新映射结构[29] 来自 LibFS)。每个实验都从冷缓存开始,除非另有说明,否则映射结构不是页面缓存的。
产生碎片。我们修改了 Strata 中的 PM 块分配器,以在文件系统初始化时接受一个布局分数参数,该参数控制我们实验期间遇到的碎片级别。布局分数是文件系统碎片的度量,它表示连续文件块与非连续文件块的比率[42]。布局得分 1.0 意味着所有块都是连续的,布局得分 0.0 意味着没有块是连续的。我们以碎片块的形式分配块,以便生成的文件具有由初始化参数指定的平均布局分数,该参数将分配的文件数据和可用空间碎片化。这些碎片块随机分布在整个设备中,以模拟现实世界碎片所经历的缺乏局部性。通过进行这种修改,我们可以有效地模拟碎片,而无需使用高开销的老化方法 [25, 42]。除非另有说明,否则我们在实验中使用 0.85 的布局分数,因为这被确定为过去研究中文件系统的平均布局分数 [42]。
实验结果。除非另有说明,否则我们会报告 10 次重复测量的文件映射操作的平均延迟,包括与映射结构相关的所有开销,例如哈希计算和撤消日志记录。误差线报告 95% 的置信区间。对于插入/截断操作,文件映射操作的延迟包括块分配器的开销。请注意,HashFS 使用自己的块分配机制(第 3.2.2 节)。所有其他评估的文件映射结构都使用 Strata 中已经存在的块分配器。
4.1 局部性
我们分析了特定的访问模式如何影响文件映射的性能。我们使用 1GB 文件执行实验,在“冷缓存”场景(即仅执行一个操作然后重置实验)或“热缓存”场景(即执行 100,000操作)。我们分别在执行文件映射结构读取和文件映射结构写入时执行读取和附加。我们测量文件映射操作的延迟并在图 5 中报告平均值(以 CPU 周期为单位)。
平均而言,热缓存操作会为所有文件映射方法带来低延迟,特别是对于顺序读取,为此设计了每个文件游标优化和全局 SIMD 预取。最大的区别在于冷缓存情况,其中全局文件映射比每个文件映射快 15 倍。这是由于在冷缓存上操作时树操作的成本。树操作必须遍历多个间接级别,每个间接级别进行多个内存访问;另一方面,全局文件映射结构对 PM 的平均访问次数少于两次。全局文件映射结构的性能优势的例外是冷全局布谷鸟哈希表插入的性能,其性能与基数树(在置信区间内)和范围树相当。导致这些结构的冷缓存插入性能高度可变的主要因素是块分配器的开销,它还具有持久性和易失性元数据结构,会导致最后一级缓存未命中(例如,持久性位图,易失性自由列表维护为红黑树,以及用于在内核线程之间提供排除的互斥体 [29])。相比之下,不使用 Strata 的块分配器的 HashFS 在这种情况下比所有其他文件映射结构快 14-20 倍。总的来说,这个初始实验向我们展示了全局哈希表结构可以显着优于每个文件的树结构,特别是对于没有局部性的访问模式。
4.2 碎片化
我们现在测量文件系统碎片对文件映射的影响。我们在单个 1GB 文件上执行此实验,并将文件的碎片从无碎片(布局分数 1.0)变为高度碎片(布局分数 0.7 表示根据先前工作 [42] 的高度碎片化文件系统)。我们测量了每个操作的 10 次试验的平均“冷缓存”文件映射延迟(即重置前的一个操作,如前一个实验中所述),并在图 6 中展示了我们的实验结果。
在不同的碎片级别之间,唯一的主要区别是范围树的性能。直观地说,这种差异源于范围树表示范围的方式——如果文件没有碎片,范围树可以小得多,因此遍历速度要快得多。其他评估的映射方法不受读取碎片的影响。对于插入,块分配器平均需要更长的时间来找到空闲块,这给了 HashFS 一个优势。我们得出结论,每个文件映射在碎片文件系统上表现不佳,而 HashFS 不受碎片影响。
4.3 文件大小
每个文件的树结构的深度取决于文件的大小,而全局哈希表结构是扁平的。 因此,我们测量了三种文件大小的文件映射延迟:4KB、4MB 和 4GB,分别代表小型、中型和大型文件。 我们在图 7 中报告了每个文件映射操作的平均延迟(超过 10 次试验)。
正如预期的那样,每个文件的结构随着文件大小的增加而增长,这导致每个文件映射的间接遍历操作更多,从而导致更长的延迟。 由于数据结构的固有局部性,顺序读取的这种增加范围自然较小(对于顺序读取,范围树和基数树小于 10%,但随机读取为 34%)。 然而,哈希表的映射开销是静态的,表明哈希表结构不会因文件大小而降低性能。
4.4 IO大小
我们之前的实验执行单个块操作(即读取或写入单个 4KB 块)。但是,许多应用程序将其 IO 批处理以包含多个块。因此,我们测量了应用程序在 4KB(单个块)、64KB(16 个块)和 1MB(256 个块)上运行时对文件映射的影响。然后,我们测量包含文件映射的 IO 路径的比例,并在图 8 中报告平均值(超过 10 次试验)。由于 IO 延迟随着 IO 大小而增加,而与映射结构无关,我们将此实验的结果表示为IO 延迟的比率,以规范这种延迟的增加,并具体显示文件映射操作对整体延迟的影响。
随着组中读取的块数增加,树结构和哈希表结构之间的差距缩小。基数和范围树将块范围一起定位在同一叶节点中,从而加速更大的 IO 操作。同时,随着块数量的增加,文件系统花费在文件映射上的时间比例急剧下降。例如,对于 1MB 的 IO 大小,基数树映射仅占用 IO 路径的 3% 用于读取。在这个 IO 大小下,基数树在顺序读取和随机读取方面表现最好。但是,在应用程序级别(第 4.8 节)看不到这种性能提升的影响。我们还注意到,如果没有为 cuckoo 哈希表(第 3.2.1 节)引入远程节点优化,全局 cuckoo 哈希表的映射延迟率将保持不变,而不是随着 IO 大小的增加而降低。
我们还看到了 HashFS 相对于全局布谷鸟哈希表的优势。与布谷鸟哈希相比,HashFS 不仅所有操作的文件映射延迟更低,而且插入时间最短,因为它能够绕过传统的块分配开销。
4.5 空间利用
使用全局哈希表结构的一个潜在缺点是冲突随着所用结构的百分比而增长,这意味着随着文件系统利用率的增长,文件映射延迟可能会随着冲突数量的增加而增加。我们通过创建一个总容量为 128GB 的文件系统来衡量这种效果。然后,我们测量当整体空间利用率从中等满 (80%) 到极度满 (95%) 时,包含文件映射的 IO 路径的比例。我们之前的实验展示了在大部分为空的文件系统上的性能。我们使用“冷缓存”(即每个文件一个操作)执行此微基准测试,按照之前的实验进行单块文件映射操作。我们在图 9 中报告了平均文件映射延迟比(超过 10 次试验)。由于 IO 延迟随着整体文件系统利用率的增加而增加,我们将这个实验的结果作为 IO 延迟的一小部分来解释增加在整体延迟。
正如预期的那样,当比较低利用率和高利用率时,每个文件树结构的映射操作的延迟没有变化。主要区别在于两个全局哈希表结构的延迟。如之前的实验(§4.4)所示,在低利用率下,HashFS 优于全局布谷鸟哈希。在 80% 的利用率下,HashFS 和全局 cuckoo 哈希表的随机和顺序读取性能非常相似(在 ±3% 以内,或在误差以内)。 HashFS 在插入方面仍然优于全局布谷鸟哈希表。然而,在 95% 的利用率下,HashFS 的读取延迟比全局 cuckoo 哈希表多 12-14%,并且具有相同的插入性能。
我们得出的结论是,虽然较高的利用率会导致 HashFS 相对于全局布谷鸟哈希的性能下降,但即使在高利用率下,两种文件映射结构仍然优于每个文件映射结构。
4.6 并发
我们现在进行一个实验来量化我们的文件映射方法在并发访问下的执行情况。为了模拟高争用场景,我们进行了一个实验,其中多个线程读取和写入同一个文件(每个线程读取然后附加到同一个文件)。这会导致 Strata 异步更新文件映射结构的 KernFS 组件和读取文件映射结构的用户空间 LibFS 之间的高读写争用,从而导致每个文件和全局文件映射的争用结构。每个文件映射结构都管理自己的同步——Strata 通过每个文件的读写锁管理其他文件元数据的同步。
我们在图 10 中展示了实验的结果。我们看到,随着线程数量的增加,文件映射的延迟略有增加(对于 8 倍的并发性增加,扩展树的延迟高达 13%)。文件映射方法。此外,方法之间的排名不会在任何数量的线程上发生变化。这表明常见的并发文件访问模式允许每个文件映射方法使用粗粒度一致性机制而不影响可伸缩性,而我们的两种全局文件映射方法的事务内存和哈希桶布局优化可以有效地隐藏同步间接费用。
总之,我们发现我们的 PM 优化文件映射结构中使用的隔离机制不是瓶颈。对于常见的文件访问模式,所有结构都是可扩展的。
4.7 页面缓存
我们现在讨论为什么不应该将传统的页面缓存用于 PM 文件映射结构。为了说明原因,我们提供了一个微基准,将 Strata 的默认映射结构(页面缓存范围树)与我们的范围树实现(基于 Strata 的实现,但绕过页面缓存并直接在 PM 上运行)进行比较。在这个实验中,我们打开一个 1GB 的文件并对其执行 1,000,000 次操作(单块读取或插入),并以周期为单位报告平均文件映射延迟。
我们在图 11 中展示了结果。我们可以看到,即使经过多次迭代,页面缓存的动态分配开销也没有在读取情况下分摊。由于页面缓存开销,插入情况也较慢 - PM 优化的范围树将更新直接写回 PM。
根据本实验的结果,直接在 PM 上执行文件映射显然更有利,因为它减少了读取和写入设备的字节数,减少了 DRAM 开销,并降低了 IO 路径的整体开销.因此,我们提倡使用开销较低的缓存方法,例如游标,而不是依赖页面缓存。
4.8 应用程序工作负载
我们提供了两个应用程序基准来衡量我们的 PM 优化文件映射结构对应用程序吞吐量的总体好处。我们将我们的文件映射结构与 Strata 中存在的文件映射结构进行比较,Strata 是一个按文件、页面缓存的范围树。我们使用这种文件映射结构作为我们的基线,因为 Strata 是最先进的 PM 优化文件系统 [29, 39]。
文件台。 我们使用 filebench [45] 测试我们的文件映射结构,这是一种流行的文件系统测试框架,已用于评估许多 PM 优化的文件系统 [29,52,56]。我们选择文件服务器(写入繁重)和 webproxy(读取繁重)工作负载。这些工作负载测试文件映射结构的读取、更新和删除,并模拟映射结构在重复修改下老化时的性能。我们在表 2 中描述了这些工作负载的特征。
我们在图 12 中展示了 filebench 实验的结果。在文件服务器工作负载中,HashFS 的性能优于基线 26%,而其他映射结构的性能与基线相似。该结果由 IO 大小微基准(第 4.4 节)解释。在这个微基准测试中,HashFS 在插入方面表现最好,这是这个工作负载中的主要操作。在这个工作负载中,基数树的性能最差,与基线相比,吞吐量下降了 10%。在我们的文件映射方法中,基数树的插入性能是最差的(图 8)。范围树和全局 cuckoo 哈希表对于大 IO 读取和较小块插入的性能相似,因此它们在此处的性能相似,并不是对基线的改进。
webproxy 工作负载在文件映射方法之间没有显示出任何主要的吞吐量差异(均在 ±2% 以内,或在误差范围内)。这是因为对于具有热缓存的相对较小的文件,这是一个读取繁重的工作负载,我们在 §4.1 中展示了在这种情况下跨文件映射结构的性能非常相似。
YCSB。我们还使用 LevelDB [17] 上的 YCSB [10] 评估键值工作负载的端到端性能,LevelDB [17] 是衡量 PM 优化文件系统性能的常用基准 [29]。我们测量所有标准 YCSB 工作负载 (A-F) 的吞吐量。 YCSB 使用 Zipfian 分布来选择操作的键。我们在表 3 中报告了这些工作负载的特征。我们将 YCSB 测试配置为使用包含 160 万个 1 KB 键值对(1.5 GB 数据库)的键值数据库,LevelDB 的最大文件大小为 128 MB后端在 4 个线程上运行。我们的实验根据 YCSB 工作负载对数据库执行 100,000 次操作。
我们在图 13 中展示了 YCSB 测试的结果。工作负载 A 和 E 主要受 LevelDB 的内存中操作(例如执行读取和扫描操作)的限制,不受文件系统的限制,因此我们看到所有映射方法的吞吐量相似(在基线的 3% 以内)。但是,对于其他工作负载(B、C、D、F),HashFS 提供了最佳性能,与其他文件映射方法相比,工作负载 F 的吞吐量提高了 10-45%。基数树和全局布谷鸟哈希表都比 HashFS 平均慢 2-4%,但 PM 优化的范围树在工作负载 C 和 F 中的性能比 HashFS 差 13%。这是由于性能普遍较差支配这些工作负载的随机读取的范围树结构。在这些工作负载中,Strata 中的默认文件映射结构将 70% 的文件 IO 路径用于文件映射——这为改进提供了充足的机会,这就是为什么 HashFS 在这些情况下能够将整体吞吐量提高多达 45%。我们在关于并发性的讨论中进一步讨论了基线的性能(第 4.6 节)。
我们还通过使用单线程(图 14)重新运行我们的 YCSB 基准测试来展示并发如何影响实际工作负载中的文件映射。该实验与多线程版本(图 13)的不同之处在于吞吐量的总体幅度。此外,多线程实验中吞吐量相对于基线的增加(45%)远高于单线程实验(23%)。经过进一步调查,我们发现这是因为 Strata 页面缓存不可扩展。 Strata 的页面缓存维护一个全局页面列表,使用单个共享锁来保持一致性。
概括。我们从这些实验中得出两个结论:(1)HashFS 在我们的 PM 优化映射结构中产生了最佳的整体吞吐量; (2) HashFS 总是匹配或超过 Strata 的默认映射结构(页面缓存范围树)的性能。因此,我们得出结论,HashFS 文件映射在实际应用程序工作负载中提供了最佳的整体性能。
4.9 通过级别散列进行文件映射?
我们检查 PM 存储结构是否可以有效地用于文件映射,从而能够重用先前的工作。我们选择级别散列 [7, 59],一种用于 PM 的最先进的散列表存储结构,作为案例研究。级别哈希优于 RECIPE 转换的结构 [32],并且作为哈希表,它是我们性能最佳的文件映射结构(也是哈希表)的有力竞争者。本案例研究的目的是查看在先前工作中提出的通用 PM 数据结构是否可以按原样用作文件映射结构。因此,我们不会对级别哈希应用任何文件系统特定的优化。
我们评估在我们的文件台工作负载中,级别哈希相对于我们的 PM 文件映射结构的执行情况,如图 12 所示。在所有情况下,HashFS 都优于级别哈希。特别是对于文件服务器工作负载(写入最繁重的工作负载),级别哈希的性能甚至低于 Strata 的基线。特别是,即使级别哈希提供了有效的调整大小操作,我们的全局哈希表结构都不需要调整大小,因为它们的总大小在文件系统创建时是已知的。这个实验表明了文件系统特定优化对 PM 文件映射结构的重要性,这表明 PM 存储结构不应该直接用于文件映射。
5 讨论
结果的普遍性。 在我们的微基准测试中,我们报告了独立于文件系统其余部分的文件映射操作的性能;这些结果适用于使用持久映射结构的其他 PM 文件系统(例如,ext4-DAX、SplitFS [24]、NVFS [40] 和 ZoFS [13])。
Strata 通过 LibFS 中的应用程序日志批处理文件映射更新(SplitFS 具有类似的批处理系统),这在我们的宏基准测试结果中进行了衡量。批处理分摊了映射结构的更新开销。出于这个原因,我们预测 HashFS 在不进行批量更新的 PM 文件系统(例如 ext4-DAX、NFVS 和 ZoFS)上会以更大的优势胜过其他映射结构。
弹力。对崩溃和数据损坏的恢复不是文件映射结构独有的挑战,可靠性问题通常在文件系统级别处理,而不是专门针对文件映射结构。由于我们将日志用于非幂等文件映射结构操作(第 3.1.1 节)并且 Strata 记录其他文件映射操作,因此我们所有的文件映射结构都等效地具有崩溃一致性。为了应对数据损坏和其他设备故障,我们的映射结构可以使用现有方法(例如,来自 NOVA-Fortis [53] 的 TickTock 复制)。
6 相关工作
很少有先前的工作专门分析文件映射结构的性能。 BetrFS [23] 发现写入优化的全局目录和文件映射结构在优化写入繁重的工作负载方面是有效的。但是,此分析是在 SSD 上执行的。
PM 文件系统中的文件映射。 PMFS [15] 使用 B 树,分配大小为 4KB、2MB 和 1GB 内存页面的文件数据块。因此,PMFS 分配器类似于 OS 虚拟内存分配器,尽管具有不同的一致性和持久性要求。 PMFS 与使用范围进行文件映射的系统形成对比,但除了它透明地支持大页面这一事实之外,没有为其方案提供任何理由 [21]。因此,我们不知道它的文件映射方案是否适合 PM 文件系统。这个问题延伸到 DevFS,它重用了 PMFS [27] 中存在的元数据结构。 Strata 和 ext4-DAX 都使用扩展树进行文件映射,Strata 在其存储设备层次结构的所有级别使用扩展树 [11, 29]。这两个系统都使用基于 ext4 遗留的范围树,如果范围树对于 PM 是最佳的,则不提供分析。
PM 优化的存储结构。许多工作已经提出了 PM 优化的存储结构,无论是通用的 [6、12、31、32、37、46、47、54、58、59] 还是在数据库应用程序的上下文中,例如键值存储 [26 , 28, 51]。它们尽可能提供就地原子更新,以避免必须保留单独的日志。然而,常见的文件系统操作通常需要对多个文件系统结构进行原子更新——例如,在分配块时,还必须修改块位图。因此,仅对单个数据结构实施一致性和原子性是不够的——我们需要分析 PM 文件系统中的文件映射结构,以实现有效的元数据一致性和持久性。
内存映射。将虚拟内存位置映射到物理内存位置类似于文件映射。几十年来,大量研究改进了虚拟内存 [3, 44, 55] 并设计了类似的结构;页表是许多平台上的基数树,最近的工作提出杜鹃散列作为一种更具可扩展性的替代方案 [41]。主要区别在于缓存和一致性。文件映射缓存针对通过游标和 SIMD 预取的顺序访问进行了优化;它们在所有线程之间共享,从而简化了频繁的并发更新。 MMU 通过不跨 CPU 内核共享的转换后备缓冲区 (TLB) 优化随机读取访问,需要昂贵的 TLB 击落以进行并发更新。此外,由于文件映射结构是在软件而不是硬件中维护的,它们允许更广泛的设计,这些设计可能难以在硬件中有效实现(即,范围树或 HashFS 的线性探测)。
7 结论
文件映射现在是 PM 文件系统上 IO 路径开销的重要组成部分,页面缓存无法再减轻这些开销。我们设计了四种不同的 PM 优化映射结构来探索与 PM 上的文件映射相关的不同挑战。我们对这些映射结构的分析表明,我们的 PM 优化哈希表结构 HashFS 平均表现最佳,对实际应用程序工作负载提供高达 45% 的改进。