文件系统数据结构必须持久存在于存储设备上。在更新持久数据结构时,如果发生断电或系统崩溃,磁盘上的结构可能处于不一致状态,这就是崩溃一致性问题(crash-consistency problem)。
崩溃场景分析
假设工作负载为:向原有文件追加单个 4KB 数据块。 文件系统需要更新 3 个磁盘结构:
- inode (I[v2]):更新文件大小,添加指向新数据块的指针。
- 数据位图 (B[v2]):标记新数据块已分配。
- 数据块 (Db):用户写入的实际内容。
文件系统必须对磁盘执行 3 次单独写入。如果崩溃发生在这些写入之间,会导致以下场景:
只有一次写入成功
- 仅 Db 写入:数据在磁盘上,但无 inode 指向且位图未标记。如同未发生,无崩溃一致性问题。
- 仅 I[v2] 写入:inode 指向未写入的旧数据(垃圾数据)。且 inode 认为已分配而位图认为未分配,导致文件系统不一致(file-system inconsistency)。
- 仅 B[v2] 写入:位图标记已分配,但无 inode 指向。导致空间泄露(space leak)。
两次写入成功
- I[v2] 和 B[v2] 写入,无 Db:元数据一致,但 inode 指向垃圾数据。
- I[v2] 和 Db 写入,无 B[v2]:inode 指向正确数据,但位图未标记分配,存在不一致。
- B[v2] 和 Db 写入,无 I[v2]:数据已写入且位图已标记,但无 inode 指向,无法归属文件。
理想状态是将文件系统从一个一致状态原子地移动到另一个状态。但磁盘一次只提交一次写入,中间的崩溃会导致数据不一致、空间泄露或返回垃圾数据。
解决方案 1:文件系统检查程序 (fsck)
早期文件系统允许不一致发生,在系统重启时通过 fsck (file system checker) 扫描并修复。其目标仅是确保文件系统元数据内部一致。
fsck 的基本检查阶段:
- 超级块:检查文件系统大小等健全性。
- 空闲块:扫描 inode 及间接块,生成正确的分配位图,解决位图与 inode 的不一致。
- inode 状态:检查 inode 类型字段等,清除损坏的 inode。
- inode 链接:扫描目录树计算链接数,修复 inode 中的链接计数。无目录引用的 inode 移至
lost+found。 - 重复:处理两个 inode 指向同一块的情况(清除或复制块)。
- 坏块:清除指向超出有效范围地址的指针。
- 目录检查:验证目录格式完整性(如
.和..存在,引用的 inode 已分配)。
缺点:对于大容量磁盘,扫描整个磁盘和目录树极其缓慢且昂贵。
解决方案 2:日志(预写日志)
预写日志(write-ahead logging,在文件系统中称为日志 journaling)在覆写磁盘结构前,先在日志的已知位置写下描述即将执行操作的注记。崩溃后可通过重放日志恢复,避免扫描全盘。
数据日志 (Data Journaling)
Linux ext3 的数据日志模式将所有数据和元数据先写入日志。
更新步骤:
- 日志写入:将事务内容(事务开始块 TxB、I[v2]、B[v2]、Db)写入日志。
- 日志提交:将事务结束块(TxE)写入日志。TxE 写入完成标志事务已提交(committed)。
- 加检查点 (Checkpointing):将更新内容(I[v2]、B[v2]、Db)写入磁盘最终位置。
- 释放:加检查点后,在日志超级块中将事务标记为空闲,允许循环复用日志空间。
强制写入顺序:TxE 必须在其他块写入完成后发出。为避免磁盘内部调度导致 TxE 先于其他块写入(若此时崩溃,重放日志会写入垃圾数据),文件系统分两步发出日志写入:先写 TxB 及内容,完成后再写 TxE(必须是 512 字节以保证原子性)。 (优化方案:通过在 TxB 和 TxE 中包含日志内容的校验和,可一次性发出整个事务写入。恢复时若校验和不匹配则断定崩溃并丢弃。)
恢复机制:系统引导时扫描日志,重放已提交的事务(重做日志,redo logging)。未提交的事务直接跳过。
元数据日志 (Metadata Journaling / Ordered Journaling)
数据日志将每个数据块写入两次,开销大。元数据日志(有序日志)仅将元数据写入日志,用户数据直接写入文件系统。
数据写入顺序至关重要:必须先写数据,再提交元数据日志。否则若元数据先提交而数据未写入,崩溃恢复后 inode 会指向垃圾数据。
更新协议:
- 数据写入:将数据写入最终位置。
- 日志元数据写入:将 TxB 和元数据写入日志。
- 日志提交:将 TxE 写入日志(必须在步骤 1 和 2 完成后)。
- 加检查点元数据:将元数据更新写入最终位置。
- 释放:将事务标记为空闲。
块复用问题:在元数据日志中,删除目录释放的块可能被新文件复用。由于新文件数据不记日志,若发生崩溃,重放日志会用旧目录内容覆盖新文件数据。 解决方案:Linux ext3 引入撤销(revoke)记录。删除目录时写入撤销记录,重放时跳过被撤销的数据。
解决方案 3:其他方法
- 软更新 (Soft Updates):仔细对文件系统的所有写入排序,确保磁盘结构永远不处于不一致状态(如先写数据块再写 inode)。实现复杂。
- 写时复制 (Copy-On-Write, COW):如 ZFS。从不覆写文件或目录,而是将更新写入磁盘未使用的新位置,完成后翻转根结构指针。
- 基于反向指针的一致性 (BBC):每个块添加反向指针(如数据块指向 inode),通过检查正向和反向指针是否匹配来判断一致性。
- 乐观崩溃一致性:尽可能多地发出写入,利用事务校验和检测不一致,减少等待磁盘写入完成的次数以提高性能。