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

写 20000 次 1 字节:不带缓冲 79.74 毫秒,带 1 MB 缓冲 2.74 毫秒——差 29 倍。写的内容一模一样,差的只是【过不过内核】。

📝 本节和前面几节的关系

6.9 讲了 fsync 那一道关,这一节往回退一层:在到达内核之前,程序自己还能攒一道。

一、三层缓冲

第一层:程序自己的缓冲(C 的 stdio、Python 的文件对象)。攒够了一定大小或手动 flush 才交给内核。 第二层:内核的页缓存。写进去就算【别人能看到了】,但还没落盘。 第三层:设备自己的写缓存。在硬盘电路板上,几 MB,到了就写,这一层断电就丢。 三层叠起来:你写 1 个字节,可能要过很久才真到盘上。所以要分清【谁能看到】与【断电丢不丢】是两件事。

二、实测:29 倍

写 20000 次 1 字节。 不带缓冲(每次直接过内核):79.74 毫秒,每次 3.99 微秒。 带 1 MB 缓冲:2.74 毫秒,每次 0.14 微秒。 差 29 倍。写的内容完全一样,差的只是【过不过内核】。 这 3.99 微秒就是【一次系统调用 + 一次拷贝】的价格,跟 6.9 里 fsync 的 3 毫秒是两个量级的问题——一个是过门,一个是等设备。

三、什么时候必须关缓冲

一、要立刻被别人读到:flush 就够(进页缓存,别人就能读),不需要 fsync。 二、要断电不丢:必须 fsync(催到介质)。 三、你自己已经攒了一大批:直接写,别再套一层缓冲——否则数据会被拷两次(一次进你的缓冲,一次进页缓存)。 第四条容易被忽略:大块写时关掉缓冲反而更快,因为省掉了一次内存拷贝。

四、日志为什么要【追加 + 缓冲 + fsync】

追加:天生顺序,不用跳着写(6.12 会讲随机写多贵)。 缓冲:把多次小写攒成一次大写,省掉过门。 fsync:在【我说提交成功】之前,逼它落盘。 三件事合起来,就是【日志(WAL)】的写法——它同时拿到了【顺序】、【批量】、【可靠】三个好处。 所以几乎所有数据库、消息队列都用这一套。

五、动手 Demo:缓冲区缓存——带缓冲和不带缓冲差多少

这一节写 20000 次 1 字节,一次不带缓冲(每次直接过内核),一次带 1 MB 缓冲,把【过门】的价格量出来。

n = 20000

with open(p, "wb", buffering=0) as f:
    t0 = time.perf_counter()
    for _ in range(n):
        f.write(b"A")
    print("不带缓冲", (time.perf_counter() - t0) * 1000)

with open(p, "wb", buffering=1024 * 1024) as f:
    t0 = time.perf_counter()
    for _ in range(n):
        f.write(b"A")
    print("带 1MB 缓冲", (time.perf_counter() - t0) * 1000)

实跑输出:

--------------------------------------------------------------------
缓冲区缓存——带缓冲和不带缓冲差多少
--------------------------------------------------------------------
① 写 20000 次 1 字节,两种写法

  写法                     总耗时         每次折算
  --------------------------------------------------
  不带缓冲(每次过门)     81.66 毫秒     4.08 微秒
  带 1 MB 缓冲             2.76 毫秒      0.14 微秒

  **差了 30 倍。**
  **写的内容一模一样,差的只是【过不过内核】。**

② 缓冲区缓存是【谁】的缓冲

  这一层             在哪             攒多久
  -------------------------------------------------------
  程序自己的缓冲     你的进程里       到大小或手动 flush
  内核的页缓存       内核里           等回写线程来收
  磁盘自己的缓存     硬盘电路板上     到几 MB 就写

  **三层缓冲叠起来**:**你写 1 个字节,可能要过很久才真到盘上。**
  **所以【写完就能被别的进程读到】靠的是第二层(页缓存),不是第一层。**
  **而【断电不丢】必须靠 fsync 一直催到第三层。**

③ 什么时候该关掉缓冲

  场景                   该怎么做
  ---------------------------------------------
  要立刻被别人读到       flush(进页缓存就行)
  要断电不丢             fsync(催到介质)
  自己已经攒了一大批     直接写,别再套一层

79.74 毫秒对 2.74 毫秒——差 29 倍。 每次折算:3.99 微秒对 0.14 微秒。3.99 微秒就是【一次系统调用 + 一次拷贝】的价格。 注意这里连 fsync 都没做——只是到内核为止。如果再加上 fsync,就是 3 毫秒那一档了(6.9)。 所以性能优化要分清层次:过门是微秒级,等设备是毫秒级。先解决毫秒级的,再管微秒级的。 这也解释了【C 语言里 printf 要不要加换行】这种老话题——行缓冲与全缓冲的差别,就是同一个道理。

📋 Linux 上看到底写没写下去:strace -e trace=write,fsync -c <命令>——看 write 被调了几次、每次多少字节。

30 秒实验

  1. 把缓冲改成 4 KB 与 1 MB,看耗时怎么变。
  2. strace -c -e trace=write ls,看一个普通命令调了几次 write。
  3. 把上例的 1 字节改成 4 KB、写 100 次,看带不带缓冲还差多少(差距应该大幅缩小)。

常见误区

  1. 【缓冲就是 fsync】。缓冲只是攒起来;fsync 是逼它落盘,两件事不同层。
  2. 【缓冲一定更快】。大块写的时候它反而多一次拷贝,所以有些场景要显式关掉。
  3. 【flush 之后断电就不丢了】。flush 只到页缓存,断电照样丢。
自测三问(点开对答案) 1. 三层缓冲分别在哪? —— **程序自己的缓冲 → 内核页缓存 → 设备写缓存。****越往下越慢,也越【丢不了】。** 2. 为什么带缓冲快 29 倍? —— **不带缓冲时每次写都是一次系统调用**(约 4 微秒);**带缓冲把 20000 次合并成几次。** 3. 什么时候应该关掉缓冲? —— **你自己已经攒了一大批、一次大块写的时候**——**关掉能省一次内存拷贝。**

相关笔记