批量化是性能的关键
ext3 将多个系统调用批量化到一个事务中,分摊固定成本并启用写入吸收。
MIT 6.1810 · 第 19 课 · rtm
以 Linux ext3 文件系统为案例,深入研究日志在真实系统中的高性能实现。
| 规则 | 含义 |
|---|---|
| 预写规则 | 在所有写入提交到日志之前,不能开始写入 FS 的实际位置 |
| 释放规则 | 在所有实际位置写入完成之前,不能擦除/覆盖日志 |
| 方面 | xv6 | ext3 |
|---|---|---|
| 提交频率 | 每个系统调用通常提交一次 | 每几秒提交一次(批量化) |
| 系统调用等待 | 等待日志写入和提交 | 通常不需要等待(异步) |
| 数据模式 | 日志记录所有块(包括文件内容) | ordered data 模式:只记录元数据 |
| 并发 | 有限 | 多个事务在不同阶段并行 |
|SB: offset+seq#| ... |D4|...blocks...|C4|D5|...
↑ ↑ ↑ ↑
日志超级块 描述符 数据块 提交记录
日志超级块:日志偏移和最早有效事务的序列号
描述符块:magic、seq、块号列表
提交块:magic、seq
|SB| T4 | T5 | T2 | T3 |
↑
SB 指向最早的有效事务
ext3 将多个系统调用的更新批量化到一个事务中:
应用层注意:数据库、邮件系统、编辑器需要调用 fsync() 确保写入顺序。
sys_unlink() {
h = start() // 通知日志系统有新系统调用
get(h, block #) ... // 告诉日志系统要修改的块
在缓存中修改块
stop(h) // 结束系统调用(不触发提交)
}
start(): // 可能关闭当前事务,开始新事务
get(): // 将块添加到当前事务的日志列表中
stop(): // 只有当所有包含的 system call 都 stop() 后,事务才能提交
示例问题:
T1: |-create(x)-|
T2: |-unlink(y)-|
X 崩溃
如果允许 T2 的 unlink 在 T1 的 create 执行期间发生:
解决方案:屏障——不开始 T2 的系统调用直到 T1 的系统调用完成
T1: |--create(d/f)--| ... T1 的日志写入 ...
T2: |--create(d/g)--|
两个 create() 修改 d 的目录内容块
如果 T1 在 T2 的 create() 之后写入日志
→ T1 可能包含 T2 的部分写入
→ 崩溃+恢复可能重放 T2 的部分操作
解决方案:给 T1 一个缓存的私有副本(写时复制),T1 从这个快照提交
每个数据块写两次到磁盘。数据通常比元数据大得多。
文件内容不写入日志。
挑战 1:rmdir 释放目录内容块 #27,同一事务中的文件写入分配块 #27,写入 #27,崩溃在 rmdir 提交之前。恢复后 rmdir 未发生,但 #27 已被覆盖!
修复:在释放系统调用提交之前,不重用已释放的块
挑战 2:释放目录块后重用,但日志重放覆盖了文件数据
修复:释放目录块时添加 "revoke" 日志记录,阻止先前的日志记录写入该块
SB T8 xT5 T6 T7
↑
SB 指向 T6(T8 覆盖了 T5 的开始)
1. 在日志超级块中查找开始位置
2. 扫描有效事务找到日志末尾
3. 将所有块写入实际位置(按日志顺序)
4. 开始正常文件系统操作
确保较新的块版本覆盖较旧的版本。
风险:磁盘通常有写缓存并重排序写入以提高性能。磁盘可能"撒谎"说写入已完成。
解决方案:提交块包含整个事务的校验和。恢复时计算校验和,匹配则安装,不匹配则不安装。
注意:ext4 有日志校验和,ext3 没有。
ext3 将多个系统调用批量化到一个事务中,分摊固定成本并启用写入吸收。
系统调用只写入缓存并返回,不等待日志写入或提交。
文件内容先写入 FS,元数据后提交,避免数据块的双重写入。
系统调用之间的屏障防止事务间的相互干扰。
Q1:ext3 的批量化如何提高性能?
Q2:ext3 ordered data 模式的核心思想是什么?
Q3:为什么 ext3 需要在系统调用之间设置屏障?