持久数据存储和 Linux 文件 API

在研究云系统中数据存储的可持续性时,我决定测试一下自己,以确保我理解基本的东西。 我 首先阅读 NVMe 规范 为了了解可持续数据存储的保证(即保证数据在系统故障后可用)为我们提供了 NMVe 磁盘。 我得出以下主要结论:从发出写入数据的命令到写入存储介质的那一刻,数据必须被视为损坏。 然而,大多数程序都非常乐意使用系统调用来记录数据。

在这篇文章中,我将探讨 Linux 文件 API 提供的持久存储机制。 看来这里一切都应该很简单:程序调用命令 write(),该命令完成后,数据将被安全地保存到磁盘。 但 write() 仅将应用程序数据复制到位于 RAM 中的内核缓存。 为了强制系统将数据写入磁盘,需要使用一些额外的机制。

持久数据存储和 Linux 文件 API

总的来说,本材料是与我所学到的有关我感兴趣的主题的笔记的集合。 如果我们非常简短地谈论最重要的事情,那么事实证明,要组织可持续的数据存储,您需要使用以下命令 fdatasync() 或打开带有该标志的文件 O_DSYNC。 如果您有兴趣了解更多有关数据从代码到磁盘的过程中发生的情况,请查看 文章。

使用 write() 函数的特点

系统调用 write() 标准中定义的 IEEE POSIX 尝试将数据写入文件描述符。 成功完成后 write() 数据读取操作必须准确返回先前写入的字节,即使从其他进程或线程访问数据(这里 POSIX 标准的相关部分)。 这是,在关于线程如何与正常文件操作交互的部分中,有一条注释说,如果两个线程各自调用这些函数,则每个调用必须看到另一个调用的所有指定结果,或者根本看不到。结果。 由此得出的结论是,所有文件 I/O 操作都必须对其所操作的资源持有锁。

这是否意味着该操作 write() 它是原子的吗? 从技术角度来看,是的。 数据读取操作必须返回全部写入内容或不返回任何内容 write()。 但操作 write()根据标准,不一定要以写下要求写下的所有内容来结束。 她只被允许写入部分数据。 例如,我们可能有两个线程,每个线程将 1024 字节附加到由同一文件描述符描述的文件中。 从标准的角度来看,可接受的结果是每次写入操作只能向文件附加一个字节。 这些操作将保持原子性,但完成后,它们写入文件的数据将混合在一起。 这里 Stack Overflow 上关于这个主题的讨论非常有趣。

fsync() 和 fdatasync() 函数

将数据刷新到磁盘的最简单方法是调用该函数 同步()。 该函数要求操作系统将所有修改的块从缓存传输到磁盘。 这包括所有文件元数据(访问时间、文件修改时间等)。 我相信这个元数据很少需要,所以如果你知道它对你来说不重要,你可以使用该功能 fdatasync()。 在 帮帮我fdatasync() 据称,在该功能运行过程中,磁盘上保存的元数据量“对于正确执行后续数据读取操作是必需的”。 而这正是大多数应用程序所关心的。

这里可能出现的一个问题是,这些机制不能保证在可能发生故障后可以发现该文件。 特别是,在创建新文件时,需要调用 fsync() 对于包含它的目录。 否则,失败后,可能会发现该文件不存在。 原因是在UNIX中,由于硬链接的使用,一个文件可以存在于多个目录中。 因此,调用时 fsync() 文件无法知道哪个目录数据也应该刷新到磁盘(这里 您可以阅读更多相关内容)。 看起来 ext4 文件系统能够 自动 申请 fsync() 到包含相应文件的目录,但其他文件系统可能并非如此。

该机制可以在不同的文件系统上以不同的方式实现。 我用了 跟踪 了解 ext4 和 XFS 文件系统中使用的磁盘操作。 两者都向磁盘发出定期写入命令以获取文件内容和文件系统日志,刷新缓存,然后通过执行 FUA(强制单元访问,绕过缓存,直接将数据写入磁盘)写入日志来退出。 他们这样做可能是为了确认交易已经发生。 在不支持 FUA 的驱动器上,这会导致两次缓存刷新。 我的实验表明 fdatasync() 快一点 fsync()。 公用事业 blktrace 表明 fdatasync() 通常向磁盘写入较少的数据(在 ext4 中 fsync() 写入 20 KiB,并且 fdatasync() - 16 KiB)。 另外,我发现 XFS 比 ext4 稍快。 在这里,在帮助下 blktrace 设法发现 fdatasync() 将更少的数据刷新到磁盘(XFS 中为 4 KiB)。

使用 fsync() 时出现的歧义情况

我可以想到三种模棱两可的情况 fsync()这是我在实践中遇到的。

第一起此类案件发生在2008年。 如果大量文件写入磁盘,Firefox 3 界面就会冻结。 问题在于接口的实现使用 SQLite 数据库来存储有关其状态的信息。 每次界面发生变化后,都会调用该函数 fsync(),为数据的稳定存储提供了良好的保障。 在当时使用的ext3文件系统中,函数 fsync() 将系统中的所有“脏”页面转储到磁盘,而不仅仅是那些与相应文件相关的页面。 这意味着单击 Firefox 中的按钮可能会触发将兆字节的数据写入磁盘,这可能需要很多秒的时间。 据我了解,问题的解决方案 材料是将数据库工作转移到异步后台任务。 这意味着 Firefox 之前实现了比实际需要更严格的存储要求,而 ext3 文件系统的功能只会加剧这个问题。

第二个问题发生在2009年。 然后,在系统崩溃后,新 ext4 文件系统的用户面临着许多新创建的文件长度为零的事实,但旧的 ext3 文件系统不会发生这种情况。 在上一段中,我谈到了 ext3 如何将太多数据刷新到磁盘,这导致速度减慢很多。 fsync()。 为了改善这种情况,在 ext4 中,只有那些与特定文件相关的脏页才会刷新到磁盘。 来自其他文件的数据在内存中保留的时间比 ext3 长得多。 这样做是为了提高性能(默认情况下,数据保持此状态 30 秒,您可以使用以下命令进行配置) dirty_expire_centisecs; 这里 您可以找到有关此的其他材料)。 这意味着发生故障后可能会丢失大量数据且无法挽回。 这个问题的解决方案是使用 fsync() 在需要确保稳定的数据存储并尽可能保护它们免受故障后果的应用程序中。 功能 fsync() 使用 ext4 比使用 ext3 工作效率更高。 这种方法的缺点是,与以前一样,它的使用会减慢某些操作的执行速度,例如安装程序。 查看有关此的详细信息 这里 и 这里.

第三个问题关于 fsync(),起源于2018年。 然后,在PostgreSQL项目的框架内,发现如果函数 fsync() 遇到错误,它将“脏”页面标记为“干净”。 结果,以下调用 fsync() 他们不会对这些页面做任何事情。 因此,修改后的页面存储在内存中,并且永远不会写入磁盘。 这是一场真正的灾难,因为应用程序会认为某些数据已写入磁盘,但实际上不会。 此类失败 fsync() 很少见,在这种情况下应用程序几乎无法解决该问题。 如今,当这种情况发生时,PostgreSQL 和其他应用程序就会崩溃。 这是,在《应用程序可以从fsync失败中恢复吗?》的材料中,详细探讨了这个问题。 目前解决此问题的最佳方案是使用带有标志的 Direct I/O O_SYNC 或带有旗帜 O_DSYNC。 通过这种方法,系统将报告特定写入操作期间可能发生的错误,但这种方法需要应用程序自行管理缓冲区。 阅读更多相关内容 这里 и 这里.

使用 O_SYNC 和 O_DSYNC 标志打开文件

让我们回到Linux提供稳定数据存储的机制的讨论。 也就是说,我们正在讨论使用标志 O_SYNC 或旗帜 O_DSYNC 使用系统调用打开文件时 打开()。 通过这种方法,每个数据写入操作就像在每个命令之后执行一样 write() 系统收到相应的命令 fsync() и fdatasync()。 在 POSIX 规范 这称为“同步 I/O 文件完整性完成”和“数据完整性完成”。 这种方法的主要优点是,为了确保数据完整性,您只需要进行一次系统调用,而不是两次(例如 - write() и fdatasync())。 这种方法的主要缺点是使用相应文件描述符的所有写入都将被同步,这会限制构建应用程序代码的能力。

使用带 O_DIRECT 标志的直接 I/O

系统调用 open() 支持标志 O_DIRECT,其设计目的是绕过操作系统缓存,通过直接与磁盘交互来执行 I/O 操作。 在许多情况下,这意味着程序发出的写命令将直接转换为旨在操作磁盘的命令。 但是,一般来说,这种机制并不能替代功能 fsync() или fdatasync()。 事实上,磁盘本身可以 延迟或缓存 相应的数据写入命令。 并且,更糟糕的是,在某些特殊情况下,使用该标志时执行的 I/O 操作 O_DIRECT, 播送 进入传统的缓冲操作。 解决这个问题最简单的方法就是使用flag打开文件 O_DSYNC,这意味着每个写操作后面都会有一个调用 fdatasync().

原来,XFS文件系统最近为 O_DIRECT|O_DSYNC-数据记录。 如果使用重写块 O_DIRECT|O_DSYNC,那么如果设备支持,XFS 将执行 FUA 写入命令,而不是刷新缓存。 我通过使用该实用程序验证了这一点 blktrace 在 Linux 5.4/Ubuntu 20.04 系统上。 这种方法应该更有效,因为使用时,会将最少量的数据写入磁盘,并且使用一个操作,而不是两个操作(写入和刷新缓存)。 我找到了一个链接 补丁 2018内核,实现了这个机制。 那里有一些关于将此优化应用于其他文件系统的讨论,但据我所知,XFS 是迄今为止唯一支持此优化的文件系统。

sync_file_range() 函数

Linux有一个系统调用 同步文件范围(),它允许您仅将文件的一部分而不是整个文件刷新到磁盘。 此调用启动异步数据刷新,并且不等待其完成。 但在证书上 sync_file_range() 据说该团队“非常危险”。 不建议使用它。 特点及危险 sync_file_range() 很好地描述了 材料。 具体来说,此调用似乎使用 RocksDB 来控制内核何时将脏数据刷新到磁盘。 但同时,为了保证数据存储稳定,也采用了 fdatasync()。 在 代码 RocksDB 关于这个主题有一些有趣的评论。 例如,看起来调用 sync_file_range() 使用 ZFS 时,它不会将数据刷新到磁盘。 经验告诉我,很少使用的代码很可能包含错误。 因此,除非绝对必要,否则我建议不要使用此系统调用。

有助于确保数据持久性的系统调用

我得出的结论是,可以使用三种方法来执行确保数据持久性的 I/O 操作。 它们都需要函数调用 fsync() 用于创建文件的目录。 这些是方法:

  1. 调用函数 fdatasync() или fsync() 后功能 write() (最好使用 fdatasync()).
  2. 使用使用标志打开的文件描述符 O_DSYNC или O_SYNC (更好 - 带标志 O_DSYNC).
  3. 命令用法 pwritev2() 与国旗 RWF_DSYNC или RWF_SYNC (最好带有标志 RWF_DSYNC).

性能说明

我没有仔细测量我所研究的各种机制的性能。 我注意到他们的工作速度差异非常小。 这意味着我可能是错的,同一件事在不同的条件下可能会产生不同的结果。 首先我会说什么对性能影响较大,然后什么对性能影响较小。

  1. 覆盖文件数据比将数据附加到文件更快(性能提升可达 2-100%)。 将数据附加到文件需要对文件的元数据进行额外更改,即使在系统调用之后也是如此 fallocate(),但这种影响的程度可能会有所不同。 为了获得最佳性能,我建议致电 fallocate() 预先分配所需的空间。 那么这个空间必须显式地用零填充并调用 fsync()。 这将确保文件系统中的相应块被标记为“已分配”而不是“未分配”。 这会带来小幅(约 2%)的性能提升。 此外,某些磁盘对块的首次访问可能比其他磁盘慢。 这意味着用零填充空间可以显着(约 100%)提高性能。 特别是,这可能发生在磁盘上 亚马逊云服务 (这是非官方数据,我无法证实)。 存储也是如此 GCP 持久磁盘 (这已经是官方信息,经过测试证实)。 其他专家也做了同样的事情 意见,与各种磁盘相关。
  2. 系统调用越少,性能越高(增益可达到5%左右)。 看起来像是一个挑战 open() 与国旗 O_DSYNC 或致电 pwritev2() 与国旗 RWF_SYNC 比打电话更快 fdatasync()。 我怀疑这里的要点是,这种方法发挥了作用,因为解决同一问题所需执行的系统调用更少(一次调用而不是两次)。 但性能差异非常小,因此您可以完全忽略它并在应用程序中使用不会使其逻辑复杂化的东西。

如果您对可持续数据存储主题感兴趣,这里有一些有用的材料:

  • I/O 访问方式 ——输入/输出机制的基础知识概述。
  • 确保数据到达磁盘 — 一个关于数据从应用程序到磁盘的过程中发生的情况的故事。
  • 什么时候应该 fsync 包含目录 - 何时使用问题的答案 fsync() 对于目录。 简而言之,事实证明您需要在创建新文件时执行此操作,而此建议的原因是在 Linux 中可能存在对同一文件的多个引用。
  • Linux 上的 SQL Server:FUA 内部结构 ——这里描述了Linux平台上的SQL Server是如何实现持久数据存储的。 这里有一些 Windows 和 Linux 系统调用之间有趣的比较。 我几乎可以肯定,正是通过这份材料,我了解了 XFS 的 FUA 优化。

您是否丢失了您认为安全存储在磁盘上的数据?

持久数据存储和 Linux 文件 API

持久数据存储和 Linux 文件 API

来源: habr.com