改动 1 个字节,8 个块里精确查出第 3 块不对——但坏掉的那几个字节已经找不回来了。校验和能【发现】,修不了;要能修,得有第二份副本。
7.5 与 7.6 给了两道防线,这一节把它们的能力边界画清楚:哪些故障它们根本管不着,以及为什么【应用层的一致性】永远是应用自己的事。
一、三道防线各管一段
日志:管【元数据改了一半】。靠重做或丢弃,把矛盾抹平。 写时复制:也管【元数据改了一半】。靠永不覆盖,让旧树永远完整。 校验和:管【数据块被写坏了】。靠每块存一个 CRC,读的时候对一遍。 三者的边界很清楚:前两个是【一致性】,第三个是【完整性】。 一致性 = 【账本自己说得通】。完整性 = 【账上的东西还在,而且是对的】。这是两件不同的事,很多人会混在一起。 日志与 COW 保证一致性,完全不保证完整性。一块数据被磁盘悄悄写错了,它们两个一丝一毫都不知道。
二、校验和只能发现
做法:每写一个数据块,顺手算一个 CRC32,把校验和存在【另一个地方】。Btrfs 把它存在块树里,ZFS 存在父块里。 读的时候重算一遍,对不上就报错。实测:8 个块里改 1 个字节,能精确指出是第 3 块。 但请注意:它告诉你【这块坏了】,却不能把坏掉的那几个字节变回来。信息已经丢了。 要真能修,必须有【第二份】:Btrfs 的 RAID1 就是【校验和 + 副本】——读到坏块时自动从另一份重取,并把坏的那份重写一遍。 这也解释了一件事:单盘上的校验和只能提高【发现率】,不能提高【存活率】。它最多让你知道【备份该从哪份取】。
三、应用层的不一致,文件系统看不见
举一个具体的例子:一次转账要改两个文件——A 减 100,B 加 100。
写完 A、还没写 B 时断电:两个文件各自都自洽(都没坏),但总钱数变了。
这件事任何文件系统都管不了——因为它根本不知道这两个文件有关系。在它眼里,这是两次独立的写。
解法只有两个。
① 应用自己写一张【待办清单】:做之前先记下【我要做这两笔】,做完再划掉。恢复时看清单,没划掉的重做或回滚。这就是数据库的 WAL,和第 7.5 节的日志是同一个思路,只是层次不同。
② 把两笔写进同一个文件,用一次原子替换。比如整个账户表就一个文件,写新的临时文件再 rename 覆盖——rename 在同一个文件系统内是原子的。
一句话:文件系统保证【单次操作的原子性】,不保证【你的一串操作也用一个原子性】。
四、那到底该怎么分层
把防线叠起来,每层管自己的事。 ① 文件系统:管元数据自洽。日志或 COW,二选一。 ② 校验和:管数据完整性。Btrfs/ZFS 自带;ext4 只管元数据(metadata_csum),数据不管。 ③ 副本或 RAID:管【坏了还能读】。校验和只能发现,副本才能修。 ④ 应用自己的事务:管业务一致性。数据库、消息队列都有;自己写的脚本往往没有。 ⑤ 备份:管【上面全都失效】。而且备份要在另一块盘、另一台机器上,最好再加一份离线。 最常见的错误是【拿快照当备份】:快照与源在同一块盘上,盘坏了两个一起没。快照解决的是【改错了想回去】,不是【盘坏了想恢复】。
五、动手 Demo:防止不了的情况——三道防线各管到哪儿
这一节先摆一张【五类故障 × 三道防线】的对照表,再用 CRC32 真做一次【改一个字节、查出来】的实验。
import os
import zlib
blocks = [os.urandom(4096) for _ in range(8)] # 8 个块
csum = [zlib.crc32(b) & 0xFFFFFFFF for b in blocks] # 各存一个校验和
# 模拟【第 3 块的第 0 个字节被写坏】
bad = list(blocks)
raw = bytearray(bad[3])
raw[0] ^= 0xFF
bad[3] = bytes(raw)
hit = []
for i in range(8):
if (zlib.crc32(bad[i]) & 0xFFFFFFFF) != csum[i]:
hit.append(i)
print("查出不对的块:", hit)
实跑输出:
--------------------------------------------------------------------
防止不了的情况——三道防线各管到哪儿
--------------------------------------------------------------------
① 五类故障,三道防线
故障 日志 写时复制 校验和
------------------------------------------------------------
元数据改一半就断电 能 能 能发现
块被标占却没人引用 能 能 能发现
数据块被写坏(坏道) 不能 不能 只能发现
静默错误(读回旧数据) 不能 不能 能发现
跨两个文件的业务事务 不能 不能 不能
**前两行是【元数据自洽】,后三行都不是。**
② 校验和能发现,却修不了
**做法**:**每写一个数据块,顺手算一个 CRC32,**
**把校验和存在【另一个地方】**(Btrfs 存在块树里)。
**读的时候重算一遍,对不上就报错。**
**改了 1 个字节,8 个块里查出 1 个不对:第 3 块。**
**请注意:它告诉你【这块坏了】,但【坏掉的那几个字节已经找不回来了】。**
**要真能修,得有第二份副本**——
**Btrfs 的 RAID1 就是拿校验和 + 副本,读到坏块时自动从另一份重取。**
③ 最容易被忽略的一类:应用层的不一致
**举一个具体的例子**:**一个转账,要改两个文件**——**A 减 100,B 加 100。**
**写完 A、还没写 B 时断电**:
**文件系统两本账各自都自洽(两个文件都没坏),但【总钱数变了】。**
**这件事任何文件系统都管不了**——
**因为它根本不知道这两个文件有关系。**
**解法只有两个。**
**① 应用自己写一张【待办清单】**:
**做之前先记下【我要做这两笔】,做完再划掉**(这就是数据库的 WAL)。
**② 把两笔写进同一个文件,用一次原子替换。**
**一句话**:**文件系统保证【单次操作的原子性】,**
**不保证【你的一串操作也用一个原子性】。**
改 1 个字节,8 个块里精确查出 1 个不对:第 3 块。 它能【发现】,但坏掉的那几个字节已经找不回来了。要能修,得有第二份副本。 CRC32 的保证很具体:任意【单个比特】翻转一定查得出;多个比特同时翻,有小概率漏掉(约 40 亿分之一的量级)。要更强就用更强的校验和(Btrfs 可选 xxhash、sha256)。 注意这里只做了一次:真实文件系统是对【每一个数据块】都算,所以空间上要多花一点(几十字节一块),CPU 上要多算一遍。
📋 Linux 上看校验和:Btrfs 上 btrfs scrub start / 会全盘重算一遍校验和;dmesg | grep -i csum 看有没有报错。
30 秒实验
- 把上面的
raw[0] ^= 0xFF改成同时改两个字节,看还能不能查出来。 - 想一想:你的备份在哪?如果今天这块盘坏了,你要多久能恢复?
mount | grep btrfs,看有没有能跑 scrub 的文件系统。
常见误区
- 【有日志就不会丢数据】。日志管元数据自洽,不管数据落没落盘,也不管数据对不对。
- 【校验和能修复坏块】。它只能发现;修复必须有第二份副本。
- 【快照就是备份】。快照在同一块盘上,盘坏了两个一起没。
自测三问(点开对答案)
1. 一致性与完整性的区别是什么? —— **一致性 = 账本自己说得通(inode 与位图不矛盾);****完整性 = 账上的东西还在、而且是对的。****日志与 COW 只保证前者。** 2. 校验和为什么修不了坏块? —— **因为信息已经丢了**——**它只能告诉你【这块不对】**,**没法把错的字节算回原来的值。** 3. 转账这种事应该由谁保证? —— **应用自己**:**写一张待办清单(WAL),或把相关数据放进同一个文件,用一次原子 `rename` 替换。****文件系统不知道这两个文件有关系。**- ☐ 7.8 文件系统不一致的对策
相关笔记
- 日志 — 一致性的第一种解法
- 文件系统不一致的对策 — 三种日志模式怎么选
- Btrfs — 校验和 + 副本的实战
- 🐧 Linux是怎样工作的 · 索引