首页 DBMS与mmap
文章
取消

DBMS与mmap

mmap 文件 I/O 是操作系统提供的一项功能,它将二级存储中的文件内容映射到程序的地址空间。然后,程序通过指针访问页面,就像文件完全位于内存中一样。操作系统只在程序引用时透明地加载页面,并在内存填满时自动驱逐页面。

mmap 的易用性几十年来一直吸引着数据库管理系统(DBMS)的开发者,作为实现 buffer pool 的一个可行的替代方案。然而,mmap 存在严重的正确性和性能问题,这些问题并不明显。这些问题使得在现代 DBMS 中正确有效地使用 mmap 很困难,甚至不可能。事实上,几个流行的 DBMS 最初使用 mmap 来支持大于内存的数据库,但很快就遇到了这些隐藏的危险,迫使他们在付出巨大的工程成本后转而自己管理文件 I/O。这样一来,mmap 和 DBMS 就像咖啡和辛辣的食物:一个不幸的组合,事后才发现。

由于开发者不断尝试在新的 DBMS 中使用 mmap,我们写这篇文章是为了给其他人一个警告,mmap 并不是传统 buffer pool 的合适替代品。我们详细讨论了 mmap 的主要缺点,我们的实验分析证明了明显的性能限制。基于这些发现,我们总结了 DBMS 开发者何时可以考虑使用 mmap 进行文件 I/O 的处方。

基于磁盘的 DBMS 的一个重要特征是它们能够支持大于可用物理内存的数据库。这种功能允许用户查询数据库,就像它完全驻留在内存中一样,即使它不能一下子装下。DBMS 通过按要求从二级存储(如 HDD、SSD)中读取数据页到内存中来实现这种幻觉。如果没有足够的内存来容纳新的页面,DBMS 将驱逐一个不再需要的现有页面,以腾出空间。

传统上,DBMS 在 buffer pool 中实现二级存储和内存之间的页面移动, buffer pool 使用读取和写入等系统调用与二级存储交互。 这些文件 I/O 机制将数据复制到用户空间中的缓冲区或从中复制数据,DBMS 保持对它传输页面的方式和时间的完全控制。

或者,DBMS 可以将数据移动的责任交给操作系统,操作系统维护自己的文件映射和 page cache 。 POSIX mmap 系统调用将二级存储上的文件映射到调用者(即 DBMS)的虚拟地址空间,然后操作系统将在 DBMS 访问它们时延迟加载页面。 对于 DBMS,数据库似乎完全驻留在内存中,但操作系统在后台处理所有必要的分页,而不是 DBMS 的 buffer pool。

从表面上看,mmap 似乎是一个在 DBMS 中管理文件 I/O 的有吸引力的实现选项。 最显着的好处是易于使用和低工程成本。 DBMS 不再需要跟踪哪些页面在内存中,也不需要跟踪访问页面的频率或哪些页面是脏的。 相反,DBMS 可以简单地通过指针访问磁盘驻留数据,就像访问内存中的数据一样,同时将所有低级页面管理留给操作系统。 如果可用内存已满,操作系统将通过透明地从 page cache 中逐出(理想情况下不需要的)页面来为新页面释放空间。

从性能的角度来看,mmap 的开销也应该比传统的 buffer pool 低得多。 具体来说,mmap 不会产生显式系统调用(即读/写)的成本,并且避免冗余复制到用户空间中的缓冲区,因为 DBMS 可以直接从操作系统 page cache 访问页面。

自 1980 年代初以来,这些假定的好处已经诱使 DBMS 开发人员放弃实施 buffer pool,转而依赖操作系统来管理文件 I/O [36]。 事实上,几个著名的 DBMS(请参阅第 2.3 节)的开发人员已经走上了这条道路,一些人甚至将 mmap 吹捧为实现良好性能的关键因素 [20]。

不幸的是,mmap 有一个隐藏的阴暗面,它有许多肮脏的问题,这使得它不适合 DBMS 中的文件 I/O。 正如我们在本文中所描述的,这些问题涉及数据安全和系统性能问题。 我们认为克服它们所需的工程步骤否定了使用 mmap 所声称的简单性。 出于这些原因,我们认为 mmap 增加了太多的复杂性而没有相应的性能优势,并强烈敦促 DBMS 开发人员避免使用 mmap 作为传统 buffer pool 的替代品。

本文的其余部分安排如下。 我们从 mmap 的简短背景开始(第 2 节),然后讨论其主要问题(第 3 节)和我们的实验分析(第 4 节)。 然后,我们将讨论相关工作(第 5 节),并总结我们的指南,以便您考虑在 DBMS 中使用 mmap(第 6 节)。

本节提供有关 mmap 的相关背景。 我们从内存映射文件 I/O 和 POSIX mmap API 的高级概述开始。 然后,我们讨论基于 mmap 的系统的实际实现。

图 1 显示了如何使用 mmap 访问文件(“cidr.db”)的分步概述。 ① 程序调用 mmap 并接收指向内存映射文件内容的指针。 ② 操作系统保留部分程序的虚拟地址空间,但不加载文件的任何部分。 ③ 程序使用指针访问文件内容。 ④ 操作系统尝试检索页面。 ⑤ 由于指定的虚拟地址不存在有效映射,操作系统触发缺页异常以将文件的引用部分从辅助存储加载到物理内存页面。 ⑥ 操作系统在页表中添加一个条目,将虚拟地址映射到新的物理地址。 ⑦ 启动 CPU 内核还将此条目缓存在其本地 页表缓存 (TLB) 中,以加速未来的访问。

当程序访问其他页面时,操作系统会将它们加载到内存中,如果 page cache 已满,则根据需要逐出页面。 逐出页面时,操作系统还会从页表和每个 CPU 内核的 TLB 中删除它们的映射。 刷新启动核心的本地 TLB 很简单,但操作系统必须确保远程核心的 TLB 中没有过时的条目。 由于当前的 CPU 不为远程 TLB 提供一致性,因此操作系统必须发出昂贵的处理器间中断来刷新它们,这称为 TLB 击落 [11]。 正如我们的实验所示(第 4 节),TLB 击落会对性能产生重大影响。

我们现在回顾内存映射文件 I/O 的最重要的 POSIX 系统调用,并描述 DBMS 如何使用它们代替传统的 buffer pool。

mmap:如前所述,此调用导致操作系统将文件映射到 DBMS 的虚拟地址空间。 然后 DBMS 可以使用普通的内存操作读取或写入文件内容。 操作系统将页面缓存在内存中,并在使用 MAP_SHARED 标志时(最终)将任何更改写回基础文件。 或者,MAP_PRIVATE 标志将创建一个只能由调用者访问的写时复制映射(即,更改不会持久保存到支持文件)。

madvise:此调用允许 DBMS 向操作系统提供有关预期数据访问模式的提示,无论是在整个文件的粒度还是针对特定页面范围。 我们关注三个常见提示:MADV_NORMAL、MADV_RANDOM 和 MADV_SEQUENTIAL。 当在默认 MADV_NORMAL 提示的 Linux 中发生缺页异常时,操作系统将获取访问的页面,以及接下来的 16 页和之前的 15 页。 对于 4 KB 的页面,MADV_NORMAL 会导致操作系统从辅助存储读取 128 KB,即使调用者只请求了一个页面。 根据工作负载,这种预取可能有助于或损害 DBMS 的性能。 例如,只读取必要页面的 MADV_RANDOM 模式是大于内存的 OLTP 工作负载的更好选择,而 MADV_SEQUENTIAL 更适合顺序扫描的 OLAP 工作负载。

mlock:此调用允许 DBMS 将页面固定在内存中,确保操作系统永远不会驱逐它们。 但是,根据 POSIX 标准(和 Linux 的实现),允许操作系统随时将脏页刷新到备份文件,即使该页已固定。 因此,DBMS无法使用mlock来确保脏页永远不会写入二级存储,这对事务安全有严重影响

msync:最后,这个调用显式地将指定的内存范围刷新到辅助存储。 如果没有 msync,DBMS 就没有其他方法来保证更新持久保存到备份文件中。

操作系统管理的 DBMS buffer pool 的吸引力已经存在了几十年 [36],QuickStore [40] 和 Dalí [22] 是 1990 年代基于 mmap 的系统的早期示例。 今天,一些 DBMS 继续使用 mmap 进行文件 I/O,如表 1 所示。例如,MonetDB 将各个列存储为内存映射文件 [12、21],SQLite 提供了使用 mmap 的选项,而不是默认的 read/write 系统调用[7]。 LMDB 完全依赖 mmap,开发人员甚至将其视为影响系统性能的主要因素 [20]。 其他具有基于 mmap 的存储引擎的系统包括 QuestDB [34] 和 RavenDB [4]。

尽管有这些明显的成功案例,但许多其他 DBMS 已经尝试(但都失败了)用基于 mmap 的文件 I/O 替换传统的 buffer pool。 在下文中,我们将讲述一些警示故事,以说明在您的 DBMS 中使用 mmap 会如何出现可怕的错误。

MongoDB 可以说是最著名的使用 mmap 进行文件 I/O 的 DBMS。 我们从开发人员那里了解到,他们选择将原始存储引擎 (MMAPv1) 基于 mmap 作为早期启动的权宜之计。 然而,该设计有许多缺点,包括过于复杂的复制方案以确保正确性以及无法对二级存储上的数据执行任何压缩。 对于后者,由于操作系统管理文件映射,内存中的数据布局需要与辅助存储上的物理表示相匹配,从而导致空间浪费和 I/O 吞吐量降低。 随着 2015 年 WiredTiger 作为默认存储引擎的引入,MongoDB 弃用了 MMAPv1,然后在 2019 年将其完全删除 [3]。 不过,在 2020 年,MongoDB 重新引入了 mmap 作为 WiredTiger 中的一个选项,但它以有限的方式使用,以避免用户空间和操作系统之间的 boundary-crossing penalties [17]。

InfluxDB 是一个时间序列 DBMS,在早期版本 [8] 中使用 mmap 进行文件 I/O。 然而,开发人员在观察到当数据库的大小超过几 GB 时写入 I/O 峰值后替换了 mmap,这可能是由于与页面驱逐相关的开销(第 3.4 节)。 在容器化环境或没有直连存储的机器上运行时(例如,云部署),他们还面临其他问题,这进一步排除了在他们的新 IOx 存储引擎 [1] 中使用 mmap 的可能性。

在简单的顺序扫描查询 [32] 上遇到性能不佳后,SingleStore 删除了基于 mmap 的文件 I/O。 DBMS 对 mmap 的调用每次查询需要 10-20 毫秒,这几乎占了整个查询运行时间的一半。 经过进一步调查,开发人员将问题的根源确定为对共享 mmap 写锁的争用。 通过切换到 read 系统调用,查询变得完全受 CPU 限制。

许多其他系统在开发初期就排除了 mmap。 例如,Facebook 创建 RocksDB 作为 Google 的 LevelDB 的一个分支,部分原因是后者使用 mmap [5] 导致读取性能瓶颈。 TileDB 发现 mmap 比 SSD 的 read 系统调用更昂贵 [27],我们也在我们的实验分析(第 4 节)中观察到这一点。 Scylla 是一种分布式 NoSQL DBMS,在页面驱逐策略和 I/O 操作调度 [23] 方面评估了文件 I/O 的几种替代方案,并且由于失去细粒度控制而拒绝了 mmap。 时间序列 DBMS VictoriaMetrics 确定了 mmap 因缺页异常阻塞 I/O 的问题 [37]。 由于内存映射文件 I/O 的 Windows 和 POSIX 实现不兼容 [26],RDF-3X 放弃了其最初基于 mmap 的引擎。

表面上看,mmap 似乎是个好主意——DBMS 不再需要管理自己的 buffer pool ,因为它将此责任交给了操作系统。 通过删除处理显式文件 I/O 的组件,DBMS 开发人员可以自由地专注于系统的其他方面。 然而,透明分页实际上给 DBMS 带来了几个严重的问题,我们将在下面讨论这些问题。

在基于 mmap 的 DBMS 中保证修改页面的事务安全所固有的挑战是众所周知的 [22, 18]。 核心问题是,由于透明分页,操作系统可以随时将脏页刷新到辅助存储,而不管写入事务是否已提交。 DBMS 无法阻止这些刷新并且在它们发生时不会收到警告。

因此,基于 mmap 的 DBMS 必须采用复杂的协议来确保透明分页不会违反事务安全保证。 我们将处理更新的方法分为三类:(1) OS copy-on-write,(2) 用户空间 copy-on-write,和 (3) shadow paging。 为了简化我们的解释,我们假设 DBMS 将数据库存储在一个文件中。

OS Copy-On-Write:这种方法背后的想法是使用 mmap 创建数据库文件的两个副本,这两个副本最初将指向相同的物理页面。 第一个用作主副本,而第二个用作事务可以暂存更新的私有工作区。 重要的是,DBMS 使用 mmap 的 MAP_PRIVATE 标志创建私有工作区,以启用操作系统的页面写时复制功能。 据我们所知,只有 MongoDB 的 MMAPv1 存储引擎使用了这种方法。 为执行更新,DBMS 修改私有工作区中受影响的页面。 操作系统会将内容透明地复制到新的物理页面,将虚拟内存地址重新映射到这些副本,然后应用更改。 主副本看不到这些更改,操作系统不会将它们保存到数据库文件中。 因此,为了提供持久性,DBMS 必须使用预写日志 (WAL) 来记录更改。 当事务提交时,DBMS 将相应的 WAL 记录刷新到辅助存储,并使用单独的后台线程将提交的更改应用到主副本。 维护更新页面的单独副本会导致两个主要问题。 首先,DBMS 必须确保已提交事务的最新更新已传播到主副本,然后才能允许冲突事务运行,这需要额外的簿记来跟踪具有待处理更新的页面。 其次,随着更多更新的发生,私有工作区将继续增长,并且 DBMS 最终可能会在内存中拥有数据库的两个完整副本。 为了解决第二个问题,DBMS 可以使用 mremap 系统调用定期缩小私有工作空间。 但是,DBMS 必须再次确保所有挂起的更新都已传播到主副本,然后再销毁私有工作区。 此外,为了避免在 mremap 期间丢失更新,DBMS 需要阻止挂起的更改,直到操作系统完成私有工作空间的压缩。

用户空间写时复制:与操作系统写时复制不同,此方法涉及手动将受影响的页面从 mmapbacked 内存复制到用户空间中单独维护的缓冲区。 SQLite、MonetDB 和 RavenDB 都使用这种方法的一些变体。 为了执行更新,DBMS 仅将更改应用于副本并创建相应的 WAL 记录。 DBMS 可以通过将 WAL 写入辅助存储来提交这些更改,此时它可以安全地将修改后的页面复制回 mmap 支持的内存。 由于复制整个页面对于小的更改来说是一种浪费,因此一些 DBMS 支持将 WAL 记录直接应用于 mmap 支持的内存。

Shadow 分页:LMDB 是这种方法最突出的支持者,它基于 System R 的 shadow 分页设计 [13]。 通过 shadow 分页,DBMS 维护数据库的单独主副本和 shadow 副本,两者均由 mmap 支持。 为执行更新,DBMS 首先将受影响的页面从主页面复制到卷影副本,然后在其中应用必要的更改。 提交更改涉及使用 msync 将修改后的 shadow 页面刷新到辅助存储,然后更新指针以将 shadow 副本安装为新的主存储。 然后,原始主副本用作新的卷影副本。 尽管这种方法实施起来似乎并不复杂,但 DBMS 必须确保事务不冲突或看到部分更新。 例如,LMDB 通过只允许一个 writer 来解决这个问题。

对于传统的 buffer pool ,DBMS 可以使用异步 I/O(例如 libaio、io_uring)来避免在查询执行期间阻塞线程。 例如,考虑一种常见的访问模式,如 B+ 树中的叶节点扫描。 DBMS 可以异步发出对这些可能不连续的页面的读取请求以屏蔽延迟,但 mmap 不支持异步读取。

此外,由于操作系统可以透明地将页面逐出到辅助存储,如果只读查询试图访问被逐出的页面,它们可能会在不知不觉中触发阻塞缺页异常。 换句话说,访问任何页面都可能导致意外的 I/O 停顿,因为 DBMS 无法知道该页面是否在内存中。

为了避免这些问题,DBMS 开发人员可以使用第 2.2 节中描述的系统调用来实现变通方法。 最明显的选择是使用 mlock 固定 DBMS 希望在不久的将来再次访问的页面。 不幸的是,操作系统通常会限制单个进程可以锁定的内存量,因为固定太多页面可能会给并发运行的进程甚至操作系统本身带来问题。 DBMS 还需要仔细跟踪和取消固定不再使用的页面,以便操作系统可以驱逐它们。

另一种可能的解决方案是使用 madvise 向操作系统提供有关查询的预期访问模式的提示。 例如,需要执行顺序扫描的 DBMS 可以向 madvise 提供 MADV_SEQUENTIAL 标志,它告诉操作系统在读取页面后逐出页面,并预取接下来将访问的后续连续页面。 这种方法比使用 mlock 涉及的更少,但它提供的控制也少得多,因为标志只是操作系统可以自由忽略的提示。 此外,向操作系统提供错误的提示(例如,当访问模式是随机的时,MADV_SEQUENTIAL)可能会对性能产生可怕的影响,正如我们在实验中展示的那样(第 4 节)。

另一种可能性是产生额外的线程来预取(即尝试访问)页面,以便它们将在缺页异常而不是主线程的情况下阻塞。 然而,尽管这些解决方案可能(部分)解决了一些问题,但它们都引入了显着的额外复杂性,这首先违背了使用 mmap 的目的。

DBMS 的一项核心职责是确保数据完整性,因此错误处理至关重要。 例如,一些 DBMS(例如 SQL Server [6])维护页面级校验和以检测文件 I/O 期间的数据损坏。 从辅助存储读取页面时,DBMS 会根据存储在标头中的校验和来验证页面内容。 但是,对于 mmap,DBMS 需要在每次页面访问时验证校验和,因为操作系统可能在上次访问后的某个时间点透明地逐出该页面.

类似地,许多 DBMS(包括 2.3 节中提到的几个)是用内存不安全语言编写的,这意味着指针错误可能会损坏内存中的页面。 防御性编码的 buffer pool 实现可以在将这些页面写入辅助存储之前检查这些页面是否存在错误,但是 mmap 会默默地将损坏的页面保存到支持文件

最后,在使用 mmap 时,优雅地处理 I/O 错误变得更加困难。 传统的 buffer pool 允许开发人员在单个模块中包含 I/O 错误处理,而与 mmap 支持的内存交互的任何代码现在都可以产生 SIGBUS,DBMS 必须通过繁琐的信号处理程序来处理它。

mmap 透明分页的最大和最显着的缺点与性能有关。 尽管 DBMS 开发人员可以通过谨慎实施来克服其他问题,但我们认为 mmap 存在严重的瓶颈,如果不进行操作系统级别的重新设计,这些瓶颈是无法避免的。

传统观点 [28、23、29、16、17、30] 认为 mmap 应该优于传统的文件 I/O,因为它避免了两个主要的开销来源。 首先,mmap 规避了显式 read/write 系统调用的成本,因为操作系统在幕后处理文件映射和缺页异常。 其次,mmap 可以返回指向存储在操作系统页面缓存中的页面的指针,从而避免额外复制到用户空间中分配的缓冲区中。 作为一个额外的好处,基于 mmap 的文件 I/O 还可以降低总内存消耗,因为数据不会在用户空间中不必要地重复。

鉴于这些优势,人们预计 mmap 和传统文件 I/O 方法之间的性能差距应该会随着更好的闪存存储(例如 PCIe 5.0 NVMe)的出现而继续扩大,这些闪存将提供与内存相当的带宽 [19]。 令人惊讶的是,我们发现对于高带宽辅助存储设备上大于内存的 DBMS 工作负载,操作系统的页面驱逐机制无法扩展到几个线程之外。 我们认为,这些性能问题基本上没有引起注意的主要原因之一是由于历史上文件 I/O 带宽有限。

具体而言,我们确定了困扰基于 mmap 的文件 I/O 的三个关键瓶颈:(1) 页表争用,(2) 单线程页面逐出,以及 (3) TLB 击落。 对操作系统进行相对直接的调整可以部分缓解前两个问题,但 TLB 击落会带来更棘手的问题。

回想一下 2.1 节,当内核需要使远程 TLB 中的映射无效时,TLB 击落发生在页面逐出期间。 虽然刷新本地 TLB 的成本很低,但发出处理器间中断以同步远程 TLB 可能需要数千个周期 [39]。 这个问题的解决方法涉及提议的微体系结构更改 [39] 或操作系统内部的广泛修改 [15、9、10]。

正如上一节所解释的,mmap 的一些问题可以通过谨慎的实施来克服,但我们认为,如果不进行重大的操作系统级重写,就无法解决其固有的性能限制。 在本节中,我们将介绍我们的实验分析,以经验证明这些问题。

我们在配备 AMD EPYC 7713 处理器(64 核,128 硬件线程)和 512 GB RAM 的单路机器上运行所有实验,其中 100 GB 可用于 Linux (v5.11) 的页面缓存。 对于持久存储,该机器有 10 × 3.8 TB 三星 PM1733 SSD(额定读取速度为 7000 MB/s,写入速度为 3800 MB/s)。 我们将 SSD 作为块设备进行访问,以避免潜在的文件系统开销 [19]。

作为基准,我们使用带有直接 I/O (O_DIRECT) 的 fio [2] 存储基准测试工具 (v3.25) 来绕过操作系统页面缓存。 我们的分析专门针对只读工作负载,这代表了基于 mmap 的 DBMS 的最佳情况; 否则,他们将需要实施复杂的更新保护(第 3.1 节),这会产生大量额外开销 [30]。 具体来说,我们评估了两种常见的访问模式:(1) 随机读取和 (2) 顺序扫描。

对于第一个实验,我们在 2 TB SSD 范围内使用随机访问模式来模拟大于内存的 OLTP 工作负载。 由于页面缓存只有 100 GB 的内存,95% 的访问都会导致缺页异常(即,工作负载受 I/O 限制)。

图 2a 显示了 100 个线程每秒随机读取的次数。 我们的 fio 基线表现出稳定的性能并实现了每秒近 900K 次读取,这符合 100 次出色的 I/O 操作和大约 100 𝜇s 的 NVMe 延迟的预期性能。 换句话说,这个结果表明 fio 可以完全饱和 NVMe SSD。

另一方面,mmap 的性能明显更差,即使使用与工作负载的访问模式匹配的提示也是如此。 我们在实验中观察到 MADV_RANDOM 的三个不同阶段。 mmap 最初在前 27 秒内与 fio 相似,然后在大约 5 秒内下降到接近零,最后恢复到 fio 性能的一半左右。 这种性能突然下降发生在页面缓存填满时,迫使操作系统开始从内存中逐出页面。 毫不奇怪,其他访问模式提示表现出更差的性能。

在 3.4 节中,我们列举了页面驱逐开销的三个主要来源。 第一个问题是 TLB 击落,我们使用 /proc/interrupt 测量并显示在图 2b 中。 如前所述,TLB 击落代价高昂(即数千个周期 [39]),因为它们涉及发送处理器间中断以刷新每个内核的 TLB。 其次,操作系统仅使用单个进程 (kswapd) 进行页面驱逐,这在我们的实验中受 CPU 限制。 最后,操作系统必须同步页表,这与许多并发线程竞争激烈。

顺序扫描是 DBMS 的另一种常见访问模式,尤其是在 OLAP 工作负载中。 因此,我们还比较了 fio 和 mmap 在 2 TB SSD 范围内的扫描性能。 我们首先仅使用一个 SSD 运行实验,然后使用 10 个带软件 RAID 0 (md) 的 SSD 重新运行相同的工作负载。

图 3 中的结果表明,fio 可以利用一个 SSD 的全部带宽,同时保持稳定的性能。 与之前的实验一样,mmap 的性能最初与 fio 相似,但我们再次观察到页面缓存在大约 17 秒后填满后性能急剧下降。 此外,正如预期的那样,对于此工作负载,MADV_NORMAL 和 MADV_SEQUENTIAL 标志的性能优于 MADV_RANDOM。

图 4 显示了使用 10 个 SSD 重复顺序扫描实验的结果,进一步突出了现代闪存存储理论上可以提供的内容与 mmap 可以实现的内容之间的差距。 我们观察到 fio 和 mmap 之间大约有 20 倍的性能差异,而 mmap 与使用一个 SSD 的结果相比几乎没有任何改进。

总之,我们发现 mmap 在初始加载阶段仅在单个 SSD 上表现良好。 一旦页面逐出开始或使用多个 SSD 时,mmap 比 fio 差 2-20 倍。 随着 PCIe 5.0 NVMe 即将发布,预计每个 SSD 的带宽将增加一倍,我们的结果表明 mmap 无法与传统文件 I/O 的顺序扫描性能相媲美。

据我们所知,还没有对于在现代 DBMS 中使用基于 mmap 的文件 I/O 相关的问题进行深入研究。 在下文中,我们描述了一些先前的研究工作,这些研究工作检查了 mmap 的不同方面。

鉴于使用 mmap 时确保事务安全的问题,一项工作引入了新的故障原子 msync 系统调用 [31、38]。 通常,如果系统在调用 msync 期间崩溃,DBMS 无法知道哪些页面已成功写入辅助存储。 Failure-atomic msync 提供与 msync 相同的 API,但确保所有涉及的页面都是原子写入的。 作为实现的副作用,failureatomic msync 禁用操作系统透明地逐出页面的能力,这消除了对我们在第 3.1 节中描述的许多安全机制的需要。

Tucana [28] 和 Kreon [29] 是围绕基于 mmap 的文件 I/O 构建的实验性键值 DBMS。 然而,他们都注意到了 mmap 的几个核心问题(例如,失去对 I/O 调度的细粒度控制),这促使 Kreon 实现了自己的自定义系统调用(kmmap)。 这些系统还必须结合复杂的写时复制方案以确保事务的一致性

其他研究项目以超越 buffer pool 替代的方式创造性地使用了 mmap。 例如,一个项目通过 mmap 利用操作系统的虚拟分页机制作为一种低开销的方式将冷数据迁移到二级存储 [35]。 RUMA 利用 mmap 来“重新布线”页面映射以执行各种操作(例如,排序)而无需物理复制数据 [33]。

最后,最近的几种方法 [18、24、25] 提倡指针调配,而不是依赖 mmap 来避免页面映射开销。 正如我们在本文中所讨论的那样,我们相信这些轻量级缓冲区管理技术是正确的方法,因为它们可以提供与 mmap 类似的性能而没有任何缺点。

本文反对在 DBMS 中将 mmap 用于文件 I/O。 尽管有明显的好处,我们还是介绍了 mmap 的主要缺点,并且我们的实验分析证实了我们与性能限制相关的发现。 最后,我们为 DBMS 开发人员提供以下建议。

何时不应在 DBMS 中使用 mmap:

  • 你需要以事务安全的方式执行更新。
  • 你希望在不阻塞慢速 I/O 的情况下处理页面错误,或者需要明确控制内存中的数据。
  • 你关心错误处理并需要返回正确的结果。
  • 您需要在快速持久存储设备上实现高吞吐量。 什么时候应该在 DBMS 中使用 mmap:
  • 您的工作集(或整个数据库)适合内存并且工作负载是只读的。
  • 您需要将产品快速推向市场,而不关心数据一致性或长期的工程难题。
  • 否则,永远不要。
本文由作者按照 CC BY 4.0 进行授权

热门标签