在冯 · 诺依曼的计算机体系结构中,数据的读写是最基本的任务之一。强一致性这种简单直观的方法对于程序员来说是最容易理解的,但是一些读写一致性较弱的模型被广泛使用,这种方法提高了系统性能,但是代价是使系统行为更加复杂和容易出错。同时,带来了另一个问题,在系统crash时能否正确地恢复数据的读写呢?
许多应用程序都依赖于特定的文件系统实现,因此当在不同的文件系统或不同的配置上运行时,在系统崩溃后很容易出现意外的行为。
为了确保系统崩溃后的数据一致性,开发人员一般需要创建一个数据更新协议,即仔细构建的系统调用序列(例如文件写入、重命名和其他文件系统调用) ,以可恢复的方式更新底层文件和目录。因此,应用程序的正确性本质上取决于这些系统调用对系统崩溃的语义(即文件系统的崩溃行为)。然而,在所有应用程序中使用单一更新协议实现是不切实际的,可能取决于性能特征,例如,有些应用可能以顺序的磁盘 i/o 为目标,并且更喜欢不涉及寻求文件差异的更新协议。数据更新协议的选择还取决于可用性特征,也与应用程序的并发机制及其数据结构所使用的格式有着内在的联系。
潜意识中的认同
在系统崩溃时,应用程序可以依赖的是什么样的文件系统呢?
我们的潜意识中是这样认为的,在系统崩溃时,磁盘上已经存在的信息(文件数据、目录条目、文件属性等等)会被保存下来,除非有人明确地发出影响它的操作。文件系统中的 fsync ()和类似的数据结构保证在调用返回时文件的数据在存储设备上。
但是,在fsync ()中有一个细微之处,那就是关于“存储设备”的定义: 在 fsync ()将信息发送到磁盘后,它可能驻留在磁盘缓存中,因此在系统崩溃时可能丢失,只能希望操作系统能够提供了特定方案来尽其所能地刷新磁盘缓存。因为您可能在一个假的硬盘驱动器上运行,所以没有任何承诺。另外,文件的目录条目和文件本身是独立的实体,可以分别发送到磁盘,一个文件的 fsync ()并不意味着其他方面的持久性。
文件系统的崩溃行为
一般地,应用程序崩溃后的数据一致性恢复取决于文件系统错综复杂的崩溃行为。关于文件系统的崩溃行为存在着两个误区:
误区1 :POSIX 定义了崩溃行为
POSIX 定义了类 unix 操作系统导出的标准文件系统接口(打开、关闭、读取和写入) ,并且对于构建可移植应用程序至关重要。因此,人们可能认为 POSIX 要求文件系统对崩溃有一个合理且明确定义的响应,例如,将目录操作按顺序发送到磁盘。
误区2: 文件系统按顺序更新元数据
日志是维护文件系统元数据一致性的常用技术,它将不同的文件系统元数据更新集合(如目录操作)作为原子事务提交,并且传统上按顺序提交元数据更新。
然而,开发人员不应认为这是一种保证。日志是一种内部文件系统技术,在保持内部一致性的同时也会逐渐重新排序更多的操作。例如,ext3重新排序只覆盖文件数据,而 ext4还重新对排序文件进行追加。同时运行多个应用程序时,文件系统需要重新排序以获得良好的性能。
开发人员的应对
开发人员可以通过以下方法法来缓解应用崩溃后的数据一致性问题:
使用一个库
只要有可能,一个明智的策略是使用一个库,比如 SQLite,在应用程序的底层实现崩溃后的数据一致性。
文档承诺
应用程序提供的崩溃后数据一致性承诺可能令人困惑, 一些开发人员可能不清楚应用程序可以提供的承诺,因为通常都不清楚文件系统的那些行为。最好的文档承诺是提供所支持的文件系统配置列表。
测试
由于文件系统表现出令人困惑的崩溃行为,因此测试非常重要。尤其是一些用于测试文件系统的工具,可以用于任何运行在 Linux 上的应用程序,尽管效率较低。
未来的可能
能否帮助开发人员构建正确的数据更新协议呢?如果可以验证给定应用程序测试用例的各种模拟系统崩溃的正确性就好了。
文件系统本身能否提供更好的抽象呢?扩展和改进当前的文件系统接口(在 Unix 或 Windows 中)是不容易的。解决方案可能是使用当前的文件系统接口提供更好的崩溃行为。然而,按顺序更新在多任务环境中并不适用。如果不在这些环境中重新排序,应用程序的性能将在很大程度上取决于其他应用程序在后台编写的数据,因此是不可预测的。
能否对文件系统进行建模呢?用一个抽象的持久性模型来完全表达文件系统的崩溃行为是否可行呢?
除了文件系统之外,应用程序崩溃后的数据一致性是一个有趣的问题,整个存储堆栈都面对着这个问题.......