作为调查云系统提供的耐用性的一部分,我想确保我理解了基础知识。我从阅读NVMe规范开始,以了解磁盘提供的保证。总结是,您应该假设您的数据在发出写入时到刷新或强制单元访问写入完成之后这段时间内已损坏。但是,大多数程序使用系统调用来写入数据。本文介绍Linux文件API提供的保证。这看起来应该很简单:程序调用write(),在它完成之后,数据是持久的。然而,write()只将数据从应用程序复制到内核的内存缓存中。要强制数据持久,您需要使用一些额外的机制。这篇文章是关于我所学到的杂乱无章的笔记集。(真正简短的总结是:使用fdatync或OPEN WITH O_DSYNC。)。有关更好、更清晰的概述,请参阅LWN';的《确保数据到达磁盘》,其中介绍了如何从应用程序代码遍历到磁盘。
在IEEE POSIX标准中,Write系统调用定义为尝试将数据写入文件描述符。在成功返回之后,需要读取以返回写入的字节,即使在其他进程或线程读取或写入时也是如此(POSIX标准write();原理)。在“与常规文件操作的线程交互”下面有一个附加说明,说明如果两个线程分别调用其中一个函数,则每个调用要么会看到另一个调用的所有指定效果,要么一个也看不到。";这表明所有文件I/O都必须有效地持有一个锁。
这是否意味着书写是原子的?从技术上讲,可以:将来的读取必须返回写入的全部内容,或者不返回任何内容。但是,写入不需要完成,只允许传输部分数据。例如,我们可以有两个线程,每个线程将1024字节附加到单个文件描述符。两次写入每一次仅写入一个字节是可以接受的。这仍然是原子的,但也会导致不需要的交错输出。有一个很棒的StackOverflow答案,有更多细节。
将数据放到磁盘上的最直接方法是调用fsync()。它请求操作系统将缓存中所有已修改的块连同所有文件元数据(例如访问时间、修改时间等)一起传输到磁盘。在我看来,元数据很少有用,所以除非您知道需要元数据,否则应该使用fdatync。Fdatync手册页指出,为了正确处理后续数据读取,需要刷新尽可能多的元数据,这是大多数应用程序所关心的。
一个问题是,这不能保证您可以再次找到该文件。特别是,当您第一次创建文件时,您需要在包含该文件的目录上调用fsync,否则失败后该文件可能不存在。原因基本上是,在UNIX中,由于硬链接,一个文件可以存在于多个目录中,因此当您对文件调用fsync时,无法知道应该写出哪些目录(更多详细信息)。看起来ext4实际上可以自动对目录进行fsync,但对于其他文件系统可能并非如此。
根据文件系统的不同,实现此功能的方式也会有所不同。我使用blktrace检查ext4和XFS使用哪些磁盘操作。它们都为文件数据和文件系统日志发出正常的磁盘写入,使用缓存刷新,然后以FUA写入日志结束,这可能是为了指示操作已提交。在不支持FUA的磁盘上,这涉及两次缓存刷新。我的实验表明,fdatync比fsync稍微快一些,blktrace显示fdatync倾向于写入更少的数据(ext4:fsync为20KiB,而fDatync为16KiB)。我的实验还显示,XFS比ext4稍微快一些,同样,blktrace显示它倾向于刷新更少的数据(XFS:fdatync为4KiB)。
在我的职业生涯中,我记得有三次与fsync相关的争议。第一次是在2008年,火狐3的用户界面在写入大量文件时会挂起。问题是UI使用SQLite数据库保存状态,这通过在每次提交后调用fsync来提供强大的持久性保证。在当时的ext3文件系统上,fsync写出系统上的所有脏页,而不仅仅是相关文件。这意味着点击Firefox中的一个按钮可能会等待数兆字节的数据写入磁盘,这可能需要几秒钟的时间。我从一篇博客中了解到,解决方案是将许多数据库提交移动到异步后台任务。这意味着Firefox之前使用了比它需要的更强的持久性保证,尽管ext3文件系统使问题变得更加严重。
2009年的第二个争议是,在系统崩溃后,新的ext4文件系统的用户发现许多最近创建的文件长度为零,而事实并非如此
2018年的第三个争议是,Postgres发现,当fsync遇到错误时,它可以将脏页标记为干净,因此以后调用fsync不会做任何事情。这会在内存中留下从未写入磁盘的修改过的页面。这是相当灾难性的,因为应用程序认为一些数据已经写入,但它没有。在这种罕见的情况下,当fsync失败时,应用程序可以做的事情很少。现在,Postgres和许多其他应用程序在发生这种情况时会崩溃。一篇题为“应用程序可以从fsync故障中恢复吗?”的论文。发表在USENIX ATC 2020上的文章详细研究了这个问题。目前最好的解决方案是将Direct I/O与O_SYNC或O_DSYNC一起使用,这将报告特定写入操作的错误,但需要应用程序自己管理缓冲区。有关fsync错误的更多详细信息,请参阅LWN文章或Postgres wiki页面。
回到系统调用以实现持久性。另一种选择是将O_SYNC或O_DSYNC选项与open()系统调用一起使用。这会导致每次写入的语义分别与后跟fsync/fdatync的写入相同。POSIX规范将这种同步I/O文件完整性完成和数据完整性完成称为同步I/O文件完整性完成和数据完整性完成。这种方法的主要优点是,您只需要一个系统调用,而不是在写入之后再执行fdatync。最大的缺点是使用该文件描述符的所有写入都将同步,这可能会限制应用程序代码的结构。
Open()系统调用有一个O_DIRECT选项,该选项旨在绕过操作系统的高速缓存,而直接对磁盘执行I/O操作。这意味着在许多情况下,应用程序的WRITE调用将直接转换为磁盘命令。但是,通常这不能替代fsync或fdatync,因为磁盘本身可以自由地延迟或缓存这些写入。更糟糕的是,有一些边缘情况意味着O_DIRECT I/O会退回到传统的缓冲I/O。最简单的解决方案是还使用O_DSYNC选项打开,这意味着每次写入后都会有效地跟随fdatync。
事实证明,XFS最近为O_DIRECT|O_DSYNC写入添加了一个快速路径。如果使用O_DIRECT|O_DSYNC覆盖块,XFS将发出Fua写入(如果设备支持),而不是使用缓存刷新。我使用blktrace确认在我的Linux5.4/Ubuntu20.04系统上发生了这种情况。这应该会更有效率,因为它将最少的数据写入磁盘,并且使用单个操作,而不是先写后刷新缓存。我找到了2018年实现此功能的内核补丁的链接,其中有一些关于为其他文件系统实现此优化的讨论,但据我所知,XFS是唯一这样做的。
Linux还有SYNC_FILE_RANGE,它允许将文件的一部分(而不是整个文件)刷新到磁盘,并触发异步刷新,而不是等待。然而,手册页声明它是极其危险的,并不鼓励使用它。对sync_file_range的一些差异和危险的最好描述是松森吉典关于它如何工作的帖子。值得注意的是,RocksDB似乎使用它来控制内核何时将脏数据刷新到磁盘,并且仍然使用fdatync来确保持久性。它的源代码中有一些有趣的注释。例如,对于ZFS,SYNC_FILE_RANGE调用实际上并不刷新数据。根据我的经验,很少使用的代码可能有bug,我建议在没有非常好的理由的情况下避免此系统调用。
我的结论是,持久I/O基本上有三种方法,它们都要求您在第一次创建文件时对包含目录调用fsync()。
我没有仔细衡量这些,其中许多差异非常小,这意味着它们可能是错误的,也可能是极有可能改变的。这些大致从最大影响到最小影响排序。
覆盖比追加更快(大约快2-100%):追加涉及额外的元数据更新,即使在错误定位系统调用之后也是如此,但效果的大小有所不同。我的建议是为了获得最佳性能,请调用falocate()预先分配所需的空间,然后显式地将其填零并执行fsync。这可确保数据块在文件系统中标记为";已分配";,而不是";未分配";,这是一个很小的(~2%)改进。此外,某些磁盘在首次访问数据块时可能会降低性能,这意味着填零可能会带来很大的改进(~100%)。值得注意的是,AWS EBS磁盘(非官方磁盘,我尚未确认)和GCP永久磁盘(官方磁盘;基准磁盘已确认)可能会出现这种情况。其他人也用不同的圆盘做了同样的观察。
系统调用越少,速度就越快(大约快5%):使用OPEN WITH O_DSYNC或pwritev2 WITH RWF_SYNC似乎比显式调用fdatync要快一些。我怀疑这是因为系统调用开销略低(只调用一次而不是两次)。但是,差别很小,所以只要能让您的应用程序逻辑更简单,就做什么都行。
I/O访问方法:I/O基础知识概述,包括有关如何在磁盘、操作系统和应用程序之间传输数据的一些重要图表。
确保数据到达磁盘:深入概述在Linux中将数据从应用程序获取到磁盘。
何时应该fsync包含的目录:一个很好的摘要(当您有一个新创建的文件";时)以及为什么(";因为UNIX文件系统链接可能遍及整个位置";)。
Linux上的SQL Server:FUA内部:描述SQL Server如何在Linux上实现持久写入。这在Windows和Linux系统调用之间进行了一些有趣的比较。我非常肯定这就是我发现XFS Fua优化的原因。