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

写 200 次 4 KB:不 fsync 1.56 毫秒,每次 fsync 602.94 毫秒——单次差 3007 微秒,慢了 380 倍。write 返回不等于数据落地,这是所有数据库都要写日志的原因。

📝 本节和前面几节的关系

6.8 讲了【读】为什么快——因为缓存在挡。这一节讲【写】的另一面:缓存在挡的时候,你怎么知道数据真的落到了盘上。

一、write 成功,数据在哪

write 返回时,数据在你的进程缓冲区里(Python 的文件对象那一层)。 flush 之后,数据到了内核的页缓存里——这时候别的进程能读到了。 fsync 之后,数据才真的落到存储介质上——这时候断电也不丢。 三层,三种承诺。所以【写成功了】这句话,必须先问【成功到哪一层】。 这是本章最容易搞错的一件事:很多人以为 write 返回就是【落盘了】。

二、实测差多少

写 200 次 4 KB:不 fsync 1.56 毫秒(每次 7.8 微秒);每次 fsync 602.94 毫秒(每次 3014.7 微秒)。 单次差 3007 微秒,整体慢 380 倍。 这 3 毫秒不是 CPU 在算,是【在等设备确认】。所以 fsync 的代价跟你的 CPU 快慢无关,跟设备有关。 注意这还是 SSD/有写缓存的情况——机械盘上还要更差。

三、fsync 与 fdatasync

fsync:数据 + 元数据(文件大小、修改时间、块位置)都落盘。 fdatasync:只保证数据落盘,元数据可能还在缓存里。 所以 fdatasync 快一点——只要文件大小没变,元数据丢了可以重建。 数据库一般用 fdatasync(预分配好文件,大小不变),普通应用用 fsync 更稳。

四、所以要攒着用

一次 fsync 写 4 KB 和写 4 MB,代价几乎一样——大头是等设备。 所以正确做法是【攒一批再一次 fsync】,而不是【每写一条就 fsync 一次】。 这个取舍在数据库里叫【组提交(group commit)】:多个事务的日志攒在一起,一次 fsync 全部落盘。 代价是【延迟变大一点】——你得多等几毫秒,等别人一起走。这是吞吐与延迟的又一次权衡。

五、动手 Demo:同步写入——fsync 一次多少钱

这一节写 200 次 4 KB,一次只写不 fsync,一次每次写完都 fsync,把【等设备确认】的价格量出来。

n, blk = 200, 4 * 1024
data = b"\x5A" * blk

with open(p, "wb") as f:
    t0 = time.perf_counter()
    for _ in range(n):
        f.write(data)
    f.flush()
    print("不 fsync", (time.perf_counter() - t0) * 1000)

with open(p, "wb") as f:
    t0 = time.perf_counter()
    for _ in range(n):
        f.write(data)
        f.flush()
        os.fsync(f.fileno())
    print("每次 fsync", (time.perf_counter() - t0) * 1000)

实跑输出:

--------------------------------------------------------------------
同步写入——fsync 一次多少钱
--------------------------------------------------------------------
① 写 200 次 4 KB,两种写法

  写法                 总耗时          每次折算
  -------------------------------------------------
  只写,不 fsync       1.17 毫秒       5.9 微秒
  每次写完都 fsync     605.75 毫秒     3028.8 微秒

  **每次 fsync 要多花 3023 微秒**——**这就是【等磁盘确认】的价格。**
  **一次 fsync 就是毫秒级**——**比不 fsync 慢了三四个数量级。**
  **机械盘上还要更差**,而且这个数跟设备有没有写缓存关系很大。

② fsync 保证了什么

  动作               数据到哪了           断电会丢吗
  -------------------------------------------------------
  write 返回         进程的缓冲区         会
  flush              内核的页缓存         会
  fsync 返回         存储设备的介质上     不会(这一份)
  fdatasync 返回     数据部分落到介质     元数据可能丢

  **看清一件事**:**write 成功不等于数据落地**。
  **这是所有数据库都要【写日志 + fsync】的原因**——
  **不 fsync 就敢说【提交成功】,断电就丢。**

③ 所以 fsync 要【攒着用】

  **一次 fsync 写 4 KB 和写 4 MB,代价几乎一样**——**大头是等设备确认。**
  **所以正确的做法是【攒一批再一次 fsync】**,
  **而不是【每写一条就 fsync 一次】**(除非你要的是【每条都不能丢】)。
  **这个取舍在数据库里有个名字**:**组提交(group commit)。**

1.56 毫秒对 602.94 毫秒——差 380 倍。 单次折算:7.8 微秒对 3014.7 微秒。3 毫秒就是这台机器【等一次设备确认】的价格。 注意这 3 毫秒完全不随【写多少】变化:写 4 KB 和写 4 MB 都是这个数——因为等的是设备的确认信号,不是数据传输。 所以【批量 + 一次 fsync】几乎免费地提高了 380 倍吞吐。这就是组提交的全部秘密。 但代价是真的:最后那一条要等别人,延迟变大。

📋 Linux 上看设备的写缓存有没有开:hdparm -W /dev/sdX(机械盘);SSD 上要看 nvme id-ctrl 与厂商工具——有写缓存时 fsync 快,但断电可能丢缓存里的数据。

30 秒实验

  1. 把次数改成 20 与 2000,看总耗时是不是线性的(fsync 版本应该是,因为每次代价固定)。
  2. 用 os.fdatasync 替换 os.fsync,比一比差多少。
  3. cat /proc/meminfo | grep Dirty,看写完还没落盘的脏页有多少。

常见误区

  1. 【write 返回就是写完了】。它只到了进程缓冲区;连内核都还没到,更别说磁盘。
  2. 【fsync 只影响性能,不影响正确性】。它决定的正是断电会不会丢数据。
  3. 【fsync 越多越安全】。它只保证【这一份】落盘;如果上层逻辑有 bug,fsync 也救不了。
自测三问(点开对答案) 1. write、flush、fsync 各把数据推到哪一层? —— **write:进程缓冲区;flush:内核页缓存;fsync:存储介质。****断电安全只在最后一层成立。** 2. 为什么单次 fsync 要 3 毫秒,而且跟写多少无关? —— **等的是设备的确认信号,不是数据传输**,**所以写 4 KB 和写 4 MB 代价一样。** 3. 什么是组提交?代价是什么? —— **多个写入攒在一起,一次 fsync 全部落盘。**代价是**最后一条要等别人,延迟变大**。

相关笔记