写 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 秒实验
- 把次数改成 20 与 2000,看总耗时是不是线性的(fsync 版本应该是,因为每次代价固定)。
- 用
os.fdatasync替换os.fsync,比一比差多少。 cat /proc/meminfo | grep Dirty,看写完还没落盘的脏页有多少。
常见误区
- 【write 返回就是写完了】。它只到了进程缓冲区;连内核都还没到,更别说磁盘。
- 【fsync 只影响性能,不影响正确性】。它决定的正是断电会不会丢数据。
- 【fsync 越多越安全】。它只保证【这一份】落盘;如果上层逻辑有 bug,fsync 也救不了。
自测三问(点开对答案)
1. write、flush、fsync 各把数据推到哪一层? —— **write:进程缓冲区;flush:内核页缓存;fsync:存储介质。****断电安全只在最后一层成立。** 2. 为什么单次 fsync 要 3 毫秒,而且跟写多少无关? —— **等的是设备的确认信号,不是数据传输**,**所以写 4 KB 和写 4 MB 代价一样。** 3. 什么是组提交?代价是什么? —— **多个写入攒在一起,一次 fsync 全部落盘。**代价是**最后一条要等别人,延迟变大**。- ☐ 6.10 缓冲区缓存
相关笔记
- 页面缓存 — 读的那一侧
- 缓冲区缓存 — 缓冲到底有几层
- 日志 — fsync 在文件系统里的用法
- 🐧 Linux是怎样工作的 · 索引