跳转至

崩溃一致性、日志与磁盘调度

一次高层文件操作可能修改多个持久化块。若系统在中间断电,部分写入已经落盘、部分尚在缓存,文件系统可能进入“每个块单独看都合法,组合起来却矛盾”的状态。崩溃一致性研究怎样定义并限制这种中间状态。

最小问题:创建文件

创建 /dir/a 可能需要:

  1. 分配 inode。
  2. 初始化 inode 元数据。
  3. 在目录数据中加入名字 a
  4. 更新空闲 inode 位图。
  5. 更新目录时间戳。

若位图先写盘,目录项没写盘,inode 可能泄漏;若目录项先落盘,inode 尚未初始化,目录会指向垃圾。单个原子扇区写无法覆盖跨多个块的事务。

写入顺序为什么不可靠

即使应用按顺序调用 write,数据仍可能经过:

  • 用户态缓冲。
  • 页缓存。
  • 文件系统写回队列。
  • 块层重排与合并。
  • 设备控制器缓存。

程序顺序、内核提交顺序和非易失介质上的持久化顺序不是同一件事。需要屏障、flush/FUA 类设备语义和文件系统协议共同建立顺序。

崩溃一致性的目标

常见目标不是保证最后一次调用的所有数据都存在,而是保证恢复后文件系统元数据自洽,并明确哪些已确认操作必须持久化。

应用还要区分:

  • 原子可见性:其他观察者看到旧状态或新状态,不看到半状态。
  • 持久性:崩溃恢复后新状态仍存在。
  • 顺序性:A 的持久化发生在 B 之前。

一次操作对其他进程可见,不表示已经持久化。

写前日志

日志把一次更新作为事务记录。简化协议:

  1. 把将要执行的更新写入日志。
  2. 确认日志事务完整持久化。
  3. 把更新写到最终位置 checkpoint。
  4. 标记日志空间可复用。

恢复时:

  • 完整提交的事务可重放。
  • 未完整提交的事务忽略或回滚。

核心不变量是:最终位置更新不能在可恢复日志之前被视为持久提交。

日志模式

只记录元数据

日志记录 inode、位图、目录项等元数据,文件数据直接写最终位置。开销较低,恢复后结构一致,但最近文件内容可能是旧数据或由具体排序语义决定。

有序数据模式

数据不写入日志,但要求新数据在相关元数据提交前写到最终位置,避免元数据指向未初始化块。它提供比纯元数据日志更强的常见安全性,但仍不等同于数据事务。

数据日志

数据和元数据都先写日志,语义更强,写放大和日志带宽成本也更高。

不同文件系统模式与接口保证不同,不能只凭“使用 journaling”推断应用数据已经安全。

事务批处理

多个文件操作可以合并进一个日志事务,摊薄提交和刷盘成本:

\[ cost_{avg}\approx\frac{cost_{commit}}{N}+cost_{update}. \]

批量越大,吞吐通常越好,但单个更新等待提交的延迟和崩溃时可能丢失的最近窗口也增大。

写时复制文件系统

写时复制方案不覆盖当前树中的块,而是写新数据块、新元数据节点,最后原子切换根指针到新版本:

旧根 -> 旧树
新数据与新路径写到空闲位置
新根 -> 新树
原子发布新根

优点包括快照、校验和与避免原地部分覆盖。代价是碎片、写放大、空间回收和引用计数/树更新复杂度。日志与 COW 不是简单的新旧替代,具体系统可能组合使用多种技术。

fsync、重命名与应用协议

应用安全保存配置的常见模式:

  1. 在同目录创建临时文件。
  2. 写入完整新内容。
  3. 同步临时文件数据和必要元数据。
  4. 原子重命名覆盖目标名字。
  5. 若需保证目录项持久化,再同步目录。
write temp -> sync temp -> rename -> sync directory

精确要求依操作系统和文件系统而异,应以目标环境文档和故障测试为准。仅调用 close 通常不表示强制持久化。

一致性检查

传统 fsck 扫描元数据,重新建立位图、链接计数和目录可达性等不变量。它无需日志也能修复许多结构错误,但大文件系统全盘扫描耗时长,且无法凭空恢复应用最新语义。

日志把恢复工作限制到近期事务,显著缩短启动恢复时间,但仍可能需要后台检查、校验和或人工处理介质损坏。

HDD 上的调度动机

机械磁盘访问时间近似由寻道、旋转等待和传输组成:

\[ T_{io}=T_{seek}+T_{rotation}+T_{transfer}. \]

随机请求中前两项常占主导。调度器重排请求可以减少磁头移动:

  • FCFS:公平直观,寻道可能很差。
  • SSTF:选择最近请求,平均寻道低,远端请求可能饥饿。
  • SCAN:磁头像电梯一样沿一个方向服务,再反向。
  • C-SCAN:只在一个方向服务,返回时不处理,等待更均匀。

请求合并把相邻块合成更大传输,提高顺序带宽。

SSD 与 NVMe 改变了什么

SSD 没有机械寻道,但仍有:

  • 闪存页读取与更大擦除块。
  • 闪存转换层地址映射。
  • 垃圾回收、磨损均衡和写放大。
  • 内部并行通道和队列深度。

NVMe 支持多提交/完成队列和高并行,传统“减少磁头移动”的算法不再是核心,调度更关注公平、合并、优先级和设备并行度。把 HDD 时代结论原样套到 SSD 会误判。

队列深度与延迟

向设备保持多个未完成请求能利用内部并行并提高吞吐,但排队时间会增加尾延迟。Little 定律给出稳态直觉:

\[ L=\lambda W, \]

其中 \(L\) 是系统中平均请求数,\(\lambda\) 是吞吐率,\(W\) 是平均停留时间。盲目增加队列深度可能只把等待从应用移到设备队列。

RAID 与故障边界

多盘阵列可通过镜像或校验提高可用性和吞吐,但不是备份:误删除、软件错误和勒索修改会同步传播。写洞、重建期间第二故障和潜在扇区错误也要考虑。

端到端完整性还需要校验和、错误报告、冗余、备份和恢复演练配合。

常见误区

  • write 返回成功不一定已经持久化。
  • 日志文件系统不一定记录用户数据。
  • SSD 没有寻道,不等于所有访问延迟相同。
  • 更深设备队列提高吞吐,不等于改善单请求延迟。
  • 原子重命名保证命名切换原子,不自动保证新内容已经落盘。

自测

  1. 创建文件为何不是单个块的原子更新?
  2. 写前日志中的“write-ahead”具体要求什么顺序?
  3. 元数据日志与数据日志的故障保证有何差异?
  4. SCAN 为什么比 SSTF 更不易让远端请求饥饿?
  5. SSD 上为什么仍需要 I/O 调度和队列管理?
参考思路

创建涉及 inode、目录和位图。日志必须在最终位置更新被提交前持久可恢复。只记元数据保证结构,数据日志还覆盖文件内容事务。SCAN 有规则地扫过整个地址范围。SSD 仍有并行度、公平、写放大和队列延迟问题。