使用内存映射文件或普通 Stream.Write 时是不是有任何持久性保证

Posted

技术标签:

【中文标题】使用内存映射文件或普通 Stream.Write 时是不是有任何持久性保证【英文标题】:Are there any persistence guarantees when using memory mapped files or plain Stream.Write使用内存映射文件或普通 Stream.Write 时是否有任何持久性保证 【发布时间】:2019-06-09 23:40:17 【问题描述】:

我有很多数据想以二进制形式保存到磁盘,我想尽可能接近 ACID 属性。由于我有大量数据并且无法将其全部保存在内存中,因此我知道我有两种基本方法:

    有很多小文件(例如每分钟左右写入磁盘) - 万一发生崩溃,我只会丢失最后一个文件。但是,性能会更差。 有一个大文件(例如打开、修改、关闭)- 之后的最佳顺序读取性能,但如果发生崩溃,我最终可能会得到一个损坏的文件。

所以我的问题是:

如果我选择使用大文件选项并将其作为内存映射文件打开(或使用Stream.PositionStream.Write),并且断电,是否可以保证可能发生的情况有文件吗?

    是否有可能丢失整个大文件,或者只是导致数据在中间损坏?

    NTFS 是否确保一定大小(4k?)的块总是被完全写入?

    在 Unix/ext4 上的结果是更好还是更差?

我想避免使用 NTFS TxF,因为 Microsoft 已经提到它计划停用它。我正在使用 C#,但语言可能无关紧要。

(补充说明)

似乎应该有一定的保证,因为——除非我错了——如果在写入文件时可能会丢失整个文件(或遭受非常奇怪的损坏),那么现有的数据库将不会是 ACID , 除非他们 1) 使用 TxF 或 2) 在写入之前复制整个文件?如果您丢失了您甚至不打算触摸的部分文件,我认为日记不会帮助您。

【问题讨论】:

This thread 有一些关于追加操作的有趣信息,这是主要的操作类型。 现有数据库通过三件事实现持久性:1) 在确认操作之前先将所有修改写入事务日志文件,2) 具有允许无缓冲写入存储的操作系统和 3) 具有硬件这保证了如果写入成功,它将被写入物理存储,即使在确认写入后电源应该立即失效。一致性需要更多的工作,但也非常依赖于此。文件系统在这个故事中几乎无关紧要,因为元数据操作(新文件、增长文件)很少见。 ACID 的最大敌人,顺便提一下,是商业级硬件,它不提供关键保证 3:保证写入确实已写入。为了提高基准测试中的数字,许多驱动器/控制器会在被要求将某些内容真正写入磁盘时简单地撒谎,并在数据存储在(非电池支持的)缓存中后立即返回。通过这样的设置,软件几乎没有机会确保耐用性——最好希望电源永远不会失效,或者您的数据并不那么重要。这是您为服务器的电池支持 RAID 控制器支付的额外费用。 【参考方案1】:

您可以调用FlushViewOfFile,它启动脏页写入,然后调用FlushFileBuffers,according to this article,保证页面已被写入。

每次写入后调用FlushFileBuffers 可能“更安全”,但不建议这样做。你必须知道你能承受多少损失。有一些模式可以限制这种潜在的损失,即使是最好的数据库也可能遭受写入失败。您只需要以尽可能少的损失恢复生活,这通常需要一些多阶段提交的日志记录。

我想可以使用FILE_FLAG_NO_BUFFERINGFILE_FLAG_WRITE_THROUGH 打开内存映射文件,但这会占用您的吞吐量。我不这样做。我为异步 I/O 打开内存映射文件,让操作系统通过它自己的异步 I/O 完成端口实现来优化吞吐量。这是最快的吞吐量。我可以容忍潜在的损失,并已适当减轻。我的内存映射数据是文件备份数据...如果我检测到丢失,一旦硬件错误被清除,我可以检测并重新备份丢失的数据。

显然,文件系统必须足够可靠才能运行数据库应用程序,但我不知道有任何供应商建议您仍然不需要备份。坏事发生。计划损失。我做的一件事是我从不写入数据中间。我的数据是不可变的和版本化的,每个“数据”文件限制为 2gb,但每个应用程序采用不同的策略。

【讨论】:

【参考方案2】:

NTFS 文件系统(和 ext3-4)使用事务日志来操作更改。每个更改都存储在日志中,然后日志本身用于有效地执行更改。 除了灾难性的磁盘故障,文件系统被设计为在它自己的数据结构中保持一致,而不是你的:如果发生崩溃,恢复过程将决定回滚什么以保留一致性。在回滚的情况下,您的“尚未写入但要写入”的数据将丢失。 文件系统将保持一致,而您的数据则不一致。

此外,还涉及其他几个因素:软件和硬件缓存引入了一个额外的层,因此是一个故障点。通常操作在缓存中执行,然后缓存本身被刷新到磁盘上。文件系统驱动程序不会看到“在”缓存中执行的操作,但我们会看到刷新操作。 这样做是出于性能原因,因为硬盘是瓶颈。硬件控制器确实有电池,以保证即使在断电的情况下也可以刷新自己的缓存。

扇区的大小是另一个重要因素,但不应考虑此细节,因为出于互操作性目的,硬盘驱动器本身可能与其原始大小有关。

如果您有一个 mewmory 映射并且您在中间插入数据,当电源关闭时,如果文件内容超过内部缓冲区的大小,则文件内容可能部分包含您所做的更改。

TxF 是一种缓解问题的方法,但有几个影响限制了您可以使用它的环境:例如,它不适用于不同的驱动器或共享网络。

为了成为 ACID,您需要设计数据结构和/或使用它的方式,以免依赖实施细节。例如,Mercurial(版本控制工具)总是将自己的数据附加到自己的修订日志中。 有许多可能的模式,但是,您需要的保证越多,您获得的技术就越具体(并且与之相关)。

【讨论】:

请注意,除非通过“-o journal=data”特别请求,否则 ext4 不会记录数据,因为这会将磁盘 IO 性能降低一半(由于首先写入数据到日志然后到文件系统)。它通常只会记录元数据以确保文件系统没有损坏,并由应用程序确保它写入文件的内容是一致的。 你说得对,我忘了说默认是ordered,和journal。有序是默认值这一事实与文件系统自我修复而不是其数据修复的答案一致。感谢您指出这一点。

以上是关于使用内存映射文件或普通 Stream.Write 时是不是有任何持久性保证的主要内容,如果未能解决你的问题,请参考以下文章

共享内存之——mmap内存映射

内存映射文件MemoryMappedFile使用

如何等待 stream.write() 循环结束

共享内存实现原理

内存映射大文件

mmap()共享内存详解