写入文件:实践中的数据一致性

Write file: Data consistency in practice

我正在研究现实世界中的多用户文件存储系统 该系统必须正视系统崩溃或断电的情况 失败,所以我正在研究一致性和耐用性。

许多数据库系统都支持 ACID,现代计算机系统 支持的日志文件系统。我注意到一个日志系统 对于这样的系统将非常重要,我们可以使用日志系统 弄清楚坠机前发生了什么和没有发生什么, 所以当系统重启时我们可以做一个合适的恢复工作。

一个典型的日志系统工作步骤:

  1. 写入日志(数据或只是元数据)
  2. 写入实际数据
  3. 提交该日志

所以当系统崩溃事件发生时,只有几种可能性:

  1. 日志不完整:请忽略它
  2. 日志未提交:因此数据不完整 - 执行回滚
  3. 日志已提交:操作已完成

一些日志文件系统就是这样工作的。

我不知道数据库系统是如何工作的,一般来说 数据库系统是用户空间中的软件运行,据我所知, 文件写入功能和磁盘之间有几件事 表面:

  1. 进程缓存
  2. 系统缓存
  3. 磁盘缓存

所以当函数returns时,数据可能不在磁盘上,也可能是 在这些缓存中。

开启 Windows 可以通过 FILE_FLAG_NO_BUFFERING 禁用系统缓存 CreateFile 时标记,MSDN 说“禁用缓存时,所有读取 写操作直接访问物理磁盘”,我的第一个 问题是,FILE_FLAG_NO_BUFFERING 是否关闭磁盘缓存 出色地 ?或者我怎样才能确保数据已经到达磁盘表面 ?

还有一个问题:SATA 和 SCSI 磁盘正在使用“命令 队列”技术,队列中的命令可以重新排序 被更有效地处理,但是日志系统依赖于 时间顺序,命令排队对日志系统(在用户空间中)不利吗? 或者我怎样才能确保 A 在 B 之前被写入?

以安全方式覆盖数据的基本方法是:

  1. 先将数据写入新的存储位置。 (您实际上还没有覆盖任何内容。)
  2. 使用 POSIX fsync 函数之类的东西,告诉 OS 将上面的内容刷新到稳定存储。这意味着刷新缓存和所有内容,因此当函数 returns 时,数据实际上在磁盘上。
  3. 在某处写一个 "journal" 条目,表明此更新的所有新数据都已写入并准备好提交。
  4. 将日志条目刷新到磁盘。
  5. 读取您在步骤 1 中写入的数据并将其写入 "real" 存储位置。 (这是您进行实际覆盖的地方。)
  6. 写另一个日记条目,说明更改已提交。
  7. 删除您在步骤 1 中创建的临时文件。

刷新用作write barriers:它们确保刷新之前的所有内容都已安全地存储在磁盘上,然后才能写入刷新之后的任何内容。 一对屏障之间,写入的重新排序(例如,由于具有命令队列的磁盘)不是问题,因为屏障确保顺序在重要的地方是正确的。在第 1 步中,您不关心磁盘是否在写入前半部分之前物理写入文件的后半部分;您只关心整个文件是否已在证明新文件已完成的日志条目之前写入。

崩溃后,您浏览日志并处理每个条目:

  • 如果您发现第 1 步中的文件没有第 3 步中的相应条目,则将该文件视为不完整并丢弃它。这是不完整更改的回滚。
  • 如果第 3 步中的条目存在但第 6 步中的条目不存在,请重复第 5 步。可能在崩溃之前第 5 步已部分完成,但这无关紧要;这只是意味着您可能会用相同的字节覆盖一些数据,这是无害的。
  • 如果第 6 步中的条目存在,重复第 7 步,删除文件(如果它仍然存在)。

您可能会发现阅读 PostgreSQL's documentation on reliability and write-ahead logging(这是 PostgreSQL 对上述那种日志记录机制的术语。)它包含额外的安全措施,例如 WAL(日志)条目的校验和以防止损坏和磁盘刷新被延迟和批处理以在正常操作期间获得更好的性能(以崩溃恢复可能需要更长的时间为代价)。

然而,说到数据库,实际 使用 一个数据库可能比尝试使用它更容易和更安全——它具有强大且经过良好测试的一致性和持久性机制自己动手。如果像 PostgreSQL 这样的完整数据库服务器对您的应用程序来说太重量级了,请考虑使用像 SQLite or Berkeley DB (which is a low-level key-value store, not an SQL relational database). Both support atomic commits.

这样的轻量级服务器。