🐧 第七章 文件系统
📄 一句话概括

1000 次元数据更新:直接改要写 3.9 MB、1000 次随机写、14.1 毫秒;先写日志再改要写 7.8 MB、2000 次顺序写、只要 6.9 毫秒。多写了一倍的数据,反而快了 2 倍。

📝 本节和前面几节的关系

7.4 讲完【账做了一半】的三种残局,这一节讲第一种解法:不阻止断电,而是让断电之后能判断【这笔账到底做了没有】。

一、思路:先记账,再动手

日志(journal,也叫预写日志 WAL)的思路只有一句话:改之前,先在一个专门的地方写下【我要改哪儿、改成什么】。 写完这一笔并且确认落盘,再回头去改真正的元数据。改完之后,在日志里标记【这一笔已完成】。 这样断电之后就有确定的行为:日志里有完整记录、但没标完成 → 重做一遍(redo);记录不完整 → 当作没发生,丢掉(undo)。 关键在于【日志里那一笔要么完整、要么不完整】——它是一个连续的块,一次写入就能原子完成。这就是把【三个地方的修改】压缩成【一个地方的写入】。 一句话:日志不阻止断电,它只让断电之后【有据可查】。

二、日志为什么反而更快

这是最反直觉的一点:多写一遍数据,总耗时反而更短。 因为第 6 章量过:顺序写 1132 MB/秒,随机写只有 278 MB/秒——差 4 倍。 直接改元数据是【满地乱跳】:1000 个 inode 散在盘的各处,每次都要重新定位。而日志区是【连续往前写】的,磁头不用来回跑。 算下来:直接改 3.9 MB 随机写 ≈ 14.1 毫秒;写日志 7.8 MB 顺序写 ≈ 6.9 毫秒。多写一倍,还快了一倍。 所以很多文件系统默认就开着日志——不是因为怕丢数据,是因为它本身划算。这个结论在机械盘上最明显;SSD 上差距小一些,但方向一样。

三、日志保证什么、不保证什么

保证的:元数据自洽——不会再出现【inode 说占 1 块、位图说占 2 块】这种矛盾。也就是 7.4 里说的【把错变成漏】。 不保证的:① 你的数据本身有没有写到盘上。日志默认只管元数据,数据如果还在缓存里就断电,文件长度对、内容是旧垃圾(7.8 的 writeback 模式)。 ② 数据块有没有被磁盘悄悄写坏。日志记录的是【我要写什么】,不是【写完以后对不对】。校验和才能查这个(7.7)。 ③ 你的业务逻辑对不对。一次转账改了两个文件,两个文件各自都自洽,总钱数却变了——文件系统根本不知道这两个文件有关系。 所以日志是【文件系统的内部一致】,不是【你的应用的一致】。

四、日志区里到底写什么

写的不是【数据的新内容】,而是【我要怎么改元数据】。比如【把 inode 1234 的大小改成 8192、把第 55 块加进它的块列表】。 这类记录很小,一条往往只有几十字节,所以日志区的写入量并不大。但如果开成【连数据也记】的模式,就会翻倍(7.8)。 日志区是【循环使用】的(circular):写满一圈又从头覆盖。所以它的大小是有上限的——默认往往只有 128 MB 左右(tune2fs -l 里的 Journal size)。 这也带来一个实际影响:如果短时间写入太猛,日志区满了,内核就得停下来等回写——这类卡顿在 iostat 上看得到,是【写放大】的一种表现。

五、动手 Demo:日志——用【多写一遍】换【不会记错账】

这一节不算实际磁盘,只做一笔账: 同样 1000 次元数据更新,直接用第 6 章实测的【顺序写 / 随机写】速度,估算两种做法的耗时。

KB = 1024
MB = 1024 * 1024

n = 1000            # 1000 次元数据更新
blk = 4 * KB        # 每次改一个 4 KB 块
seq = 1132.0        # 第 6 章实测:顺序写 MB/秒
rnd = 278.0         # 第 6 章实测:随机写 MB/秒

direct_mb = n * blk / MB          # 直接改:一次
wal_mb = n * blk * 2 / MB         # 日志:写日志 + 回写

t_direct = direct_mb / rnd * 1000   # 毫秒
t_wal = wal_mb / seq * 1000
print("直接改 %.1f MB 随机 -> %.1f 毫秒" % (direct_mb, t_direct))
print("日志   %.1f MB 顺序 -> %.1f 毫秒" % (wal_mb, t_wal))

实跑输出:

--------------------------------------------------------------------
日志——用【多写一遍】换【不会记错账】
--------------------------------------------------------------------
① 日志的基本思路:把【记一笔】变成一次原子操作

  **改之前,先在一个专门的地方(日志区)写下【我要改哪儿、改成什么】。**
  **写完这一笔并且落盘,再回头去改真正的元数据。**
  **中途断电也没关系:重启后先看日志**——
  **有完整的记录就【重做一遍】,没写完的就【当作没发生】。**

② 算一笔账:多写一遍到底亏不亏

  方案             写多少     随机还是顺序      估算耗时
  ----------------------------------------------------------
  直接改元数据     3.9 MB     1000 次随机写     14.1 毫秒
  先写日志再改     7.8 MB     2000 次顺序写     6.9 毫秒

  **多写了一倍的数据,却快了 2.0 倍。**
  **因为第 6 章量过:顺序写 1132 MB/秒,随机写只有 278 MB/秒。**

  **日志真正的妙处不在快,在于【顺序】。**
  **日志区是连续往前写的,磁头不来回跑**;
  **而直接改元数据是满地乱跳。**
  **所以日志甚至常常比不写日志还快。**
  **很多文件系统默认就开着它,不是因为怕丢数据,是因为它本身划算。**

③ 日志保证的是什么、不保证什么

  问题                       日志能解决吗
  ----------------------------------------------------
  元数据改了一半就断电       能:重做或丢弃
  块被标占却没人引用         能:回滚到改之前
  用户数据本身没写到盘上     不能,要应用自己 fsync
  数据块被磁盘悄悄写坏       不能,要靠校验和

  **一句话**:**日志管的是【元数据这本账自洽】,不管【你的数据对不对】。**

多写了一倍的数据,却快了 2 倍。因为日志把【随机写】换成了【顺序写】。 注意这两个速度是第 6 章在真文件上量出来的(1132 与 278 MB/秒),这里只是拿来估算,不是重新测的。 所以这个结论有前提:在【顺序比随机快得多】的设备上,日志才明显划算。机械盘差 4 倍,SSD 差得少一些,但顺序仍然占优。 真正的收益还是【一致性】——速度只是顺带的红利。

📋 Linux 上看日志区多大:tune2fs -l /dev/sda1 | grep -i journal;dumpe2fs -h /dev/sda1 也能看到。

30 秒实验

  1. tune2fs -l /dev/sda1 | grep -i "journal size",看日志区多大。
  2. 想一想:如果日志区只有 128 MB,而你一次要写 10 GB,会发生什么?
  3. mount | grep " / ",看根分区用的是什么类型的日志。

常见误区

  1. 【日志让写入变慢】。在顺序比随机快的设备上,日志往往更快;它写两遍,但两遍都是顺序。
  2. 【开了日志就不会丢数据】。日志默认只管元数据;数据要不要落盘,得应用自己 fsync。
  3. 【日志能修复坏道】。日志防的是【写了一半】,不是【写坏了】。后者要靠校验和,而且只能发现、不能修。
自测三问(点开对答案) 1. 日志为什么能让断电后的结果变确定? —— **因为【日志里那一笔】是一个连续块,能原子写完**;**重启时看它是完整的还是不完整的,就知道该重做还是该丢弃。** 2. 多写一遍为什么反而更快? —— **因为日志把随机写变成了顺序写**,**而顺序写在这类设备上快好几倍**。 3. 日志能保证【转账两个文件都对】吗? —— **不能**。**文件系统不知道这两个文件有关系**;**这类一致性只能由应用自己用事务日志来保证。**

相关笔记