崩溃一致性:journaling 协议与 fsync 语义
崩溃一致性(crash consistency)研究"写多个数据块的中间任意时刻断电,如何保证可恢复":文件系统用日志协议实现元数据原子性,应用则必须用 fsync 精确地向内核声明"何时需要真正落盘"。
一句话定义
崩溃一致性(crash consistency)研究"写多个数据块的中间任意时刻断电,如何保证可恢复":文件系统用日志协议实现元数据原子性,应用则必须用 fsync 精确地向内核声明"何时需要真正落盘"。
为什么重要
数据库的 ACID、消息队列的不丢消息、配置文件的原子更新,全部依赖这条纪律。而它几乎是 UNIX 上被误解最多的一组语义:write 返回不等于落盘、rename 不保证顺序、数据库损坏的根因十有八九是应用与内核在"持久化责任"上的错位。这是工程师与系统对话的"合同条款",读错一条就要付生产事故的学费。
前置知识
kp-023(日志与页缓存);kp-027(写回缓存的脏页机制)。
核心概念
- 崩溃一致性问题:一次逻辑更新涉及盘上多个块,崩溃可能停在任意两写之间。
- write-back 页缓存:write 只写入内存页缓存即返回,落盘由内核异步刷写。
- fsync(fd):阻塞到该文件的数据(及必要的元数据)到达介质;代价是毫秒级等待。
- fdatasync:只刷数据与必需元数据(如大小),跳过 mtime 等,更快。
- O_SYNC/O_DSYNC:每次 write 自动同步,语义等价于隐式 fsync。
- 原子替换惯用法:写临时文件 → fsync 临时文件 → rename → fsync 所在目录。
- WAL(预写日志):数据库"先追加日志再改数据页"的通用解,与文件系统日志同构。
原理与机制
为什么"写入+重命名"不是原子的(ext4 data=ordered 下):
期望: write(new) -> rename(new, final) -> final 内容是新的
现实: rename 的元数据可能先于数据落盘(或二者顺序不受用户控制)
崩溃后: final 存在, 但内容为空或旧页缓存残留 -> 经典的"配置文件变0字节"
正确: fd=open(tmp); write(fd); fsync(fd); close;
rename(tmp, final); fsync(dirfd); <-- 目录项也需持久化
三层持久化对象常常被混淆:数据块(文件内容)、文件元数据(大小、inode)、目录项(名字所在)——fsync 只管前两者,目录项要 fsync 目录 fd。理解这一点,才能读懂 PostgreSQL(fsync 全家桶)与 SQLite(WAL + 同步模式分级)的可靠性配置。
图示
页缓存视角:
write() : 应用缓冲 -> 内核页缓存(标脏) -> 返回 (微秒级, 易失)
fsync() : 内核页缓存 -> 块层 -> 磁盘缓存 -> 介质 (毫秒级, 持久)
磁盘写缓存 : 有些盘的易失缓存使"到达磁盘"仍不等于持久 -> 需 FLUSH CACHE 命令
直观类比
write 像把便签贴到自己工位白板(快,但断电全没);fsync 像把便签誊进保险柜并锁上(慢,但可靠);rename 像换门牌——门牌换得再快,柜子里的便签没誊完,新门牌下就是空的。
实例或案例
strace -e trace=fsync,fdatasync,openat,rename sqlite3 test.db "CREATE TABLE t(a)" # 看 WAL 的同步序列
dd if=/dev/zero of=f bs=1M count=100 oflag=dsync # 测"每次写都同步"的真实吞吐(往往个位数 MB/s)
经典事故复盘范式:应用日志显示 write 成功、断电后文件缺失——不是内核丢数据,是应用从未声明需要持久。检查方法即上面的 strace:看不到 fsync,就不要谈持久化。
常见误区
- "write 返回了就是写进硬盘了":默认仅进页缓存,进程崩溃不影响、主机断电即失。
- "rename 是原子的所以不用 fsync":rename 原子性只针对名字映射的元数据层面,且新旧文件内容的落盘顺序不受应用控制。
- 关闭 fsync 换性能(MySQL 常见误配置 innodb_flush_log_at_trx_commit=2):换来的是断电窗口内已提交事务丢失。
与其他知识点的关系
脏页刷写的策略与节奏是 kp-027;日志协议的工程变体(LSM 的 WAL、COW 的影子页)见 kp-025;本节纪律是数据库课程"事务与恢复"章节的文件系统地基。
延伸阅读
《操作系统导论》crash consistency 章对 journaling 协议的逐步推演;《数据密集型应用系统设计》第 3 章存储与检索。
自测题
- 原子更新一个配置文件的完整序列是什么?
答:写临时文件 → fsync(临时文件) → close → rename(临时, 正式) → fsync(正式文件所在目录),数据、元数据、目录项三层全部持久化。
- fdatasync 与 fsync 的差别与取舍?
答:fdatasync 只刷数据与读取所必需的元数据(如文件大小),跳过时间戳等,代价更小;需要 mtime 严格持久时才用 fsync。
- 为什么数据库普遍自建 WAL 而不依赖文件系统日志?
答:文件系统日志只保证元数据一致性且粒度粗;数据库需要"已提交事务可恢复"的语义与顺序追加的高吞吐,WAL 把持久化控制权收回到数据库自身。