🐧 第六章 存储层次
📄 一句话概括

顺序写 16 MB 用 12.99 毫秒,随机写只有 7 MB 却要 21.95 毫秒——按每 MB 折算贵 3 倍。写比读更怕随机:一次随机写可能变成【一读一写】。

📝 本节和前面几节的关系

6.11 讲了读的随机慢,这一节讲写的随机为什么更慢——这也是【日志用追加写】的根本原因。

一、实测:每 MB 贵 3 倍

顺序写 4096 次 4 KB(16 MB):12.99 毫秒。 随机写 2000 次 4 KB(7 MB):21.95 毫秒。 按总量看差 1.7 倍;按每 MB 折算,贵 3 倍。 为什么【按每 MB 算】差距更大:因为随机的每一次都有固定开销(定位、可能读回来、记元数据),跟写多少无关。 所以【写小块的随机】是最差的组合——固定开销摊不开。

二、写比读多四笔账

一、要先读回来。你改一页里 4 KB,文件系统可能得先把那一页整页读进来(如果块比页小,或者要校验)。 二、要写回去。所以一次随机写可能变成【一读一写】两趟。 三、要记元数据。块的分配表、inode 的时间戳、空闲块位图都得跟着改——这些又是额外的随机写。 四、设备有写放大。SSD 按块擦写:改 4 KB,实际可能动了 256 KB。所以【小块随机写】在 SSD 上寿命和性能双输。

三、所以日志都追加写

追加(append)天生顺序:不用找位置、不用读回来、不用改中间的块。 而且追加的内容永远在文件末尾——元数据只需要改【文件大小】一个字段。 所以【日志 + 追加写】是同时拿到了【顺序】和【少元数据】两个好处。 数据库的 WAL、消息队列的分段文件、LSM 树的 SSTable,全是这个思路。 代价是【读的时候要合并】——用读的复杂度换写的速度。

四、一个立刻能用的结论

写文件时尽量不要【跳着写】:先攒在内存里,最后一次性顺序写出。 能用追加就别用原地改。 要改文件中间的内容,宁可直接重写一个新文件再替换(写临时文件 + rename)——rename 是原子的,而且全程顺序写。 这条在 SSD 上依然成立(写放大),在机械盘上是十倍以上的差距。

五、动手 Demo:写入文件的实验——顺序写和随机写差在哪

这一节往 16 MB 的文件里写 4 KB 的块,一次从头顺序写,一次跳着随机写,再按【每 MB 花多少时间】折算。

size, blk, n = 16 * MB, 4 * KB, 2000
data = b"\x7E" * blk

with open(p, "r+b") as f:
    t0 = time.perf_counter()
    for _ in range(size // blk):
        f.write(data)
    f.flush()
    seq_ms = (time.perf_counter() - t0) * 1000

with open(p, "r+b") as f:
    t0 = time.perf_counter()
    for pos in poses:
        f.seek(pos)
        f.write(data)
    f.flush()
    rand_ms = (time.perf_counter() - t0) * 1000

实跑输出:

--------------------------------------------------------------------
写入文件的实验——顺序写和随机写差在哪
--------------------------------------------------------------------
① 顺序写 4096 次 4 KB vs 随机写 2000 次 4 KB

  走法       写了几次     写多少     耗时
  ----------------------------------------------
  顺序写     4096 次      16 MB      17.21 毫秒
  随机写     2000 次      7 MB       20.47 毫秒

  **随机写只写了 7 MB,耗时却是顺序写 16 MB 的 1.2 倍。**
  **按【每 MB 花多少时间】算,随机写贵了 2 倍。**

② 写比读更怕随机

  原因                     说明
  ---------------------------------------------------------------
  要改的是【已有的页】     写之前可能得先把那一页读进来
  改完还得写回去           一次随机写可能变成【一读一写】
  文件系统要记元数据       块的分配表也得跟着改
  设备的写放大             SSD 按块擦写,改 4 KB 可能动了 256 KB

  **所以【写】的随机比【读】的随机更贵**——
  **这也是为什么日志、数据库都尽量【追加写】,而不是【原地改】。**

③ 一个立刻能用的结论

  **写文件时尽量不要【跳着写】**:**先攒在内存里,最后一次性顺序写出。**
  **追加(append)永远比原地改快**——**因为它天生是顺序的。**
  **这条规则在 SSD 上依然成立**(写放大),**在机械盘上是十倍以上的差距。**

顺序写 16 MB 用 12.99 毫秒;随机写 7 MB 用 21.95 毫秒。 按每 MB 算,随机贵 3 倍。注意这还是【没有 fsync】的数字——只是写到页缓存。 如果每次随机写都 fsync,差距会到几十倍——因为每次都要真的跳一次设备。 所以最差的组合是【小块 + 随机 + 每次 fsync】:三个乘法叠起来。 反过来最好的组合是【大块 + 顺序 + 攒一批 fsync】——这就是日志与批处理的写法。

📋 Linux 上看写放大与写量:cat /proc/<pid>/io 的 write_bytes(真到设备的字节数)与 wchar(进程写了多少)——两者差得越大,说明写放大越严重。

30 秒实验

  1. cat /proc/self/io,对比 wchar 与 write_bytes。
  2. 把随机写改成【先排序偏移量再写】,看耗时降多少。
  3. 用【写临时文件 + rename】改一次文件中间内容,与直接 seek 改对比。

常见误区

  1. 【写比读快】。写要处理元数据与可能的读回,随机的写比随机的读更贵。
  2. 【SSD 随便随机写】。SSD 有写放大:改 4 KB 可能动了 256 KB,性能与寿命双输。
  3. 【追加写是土办法】。它同时省了定位、读回、元数据三笔账,是所有日志系统的标准做法。
自测三问(点开对答案) 1. 随机写比顺序写贵在哪? —— **可能要先读回那一页、改完写回去、还要改元数据**,**SSD 上再加写放大。** 2. 为什么日志都用追加写? —— **追加天生顺序,不用定位、不用读回、元数据只改文件大小一个字段。** 3. 要改文件中间的内容,最稳的做法是什么? —— **写一个新文件再 rename 替换**——**全程顺序写,rename 是原子的。**

相关笔记