bigywater.tech
Go · 96

文件写入与 fsync:Write 返回了数据就落盘了吗

Write 返回了,数据就落盘了吗?不一定:它只是进了操作系统的页缓存。这篇在 macOS 上量 Write、fsync、Go 的 Sync 各要多久,比较不同的同步策略,并读源码里为什么 Go 在 macOS 上不直接用 fsync。

27 µs vs 2876 µs
f.Write 与 f.Sync()(F_FULLFSYNC)的中位耗时
2405.6 vs 15.4 µs/条
追加日志:每条都 Sync 与每 100 条 Sync 一次
约 5.5 ms
“写临时文件→Sync→重命名→目录 Sync” 里两次 Sync 的合计耗时

Write 只是进了页缓存

实测 · 本机执行输出
  追加写 4 KiB(f.Write,只进内核页缓存):最小 14 µs / 中位 27 µs / 最大 219 µs(200 次)
  写完调用 syscall.Fsync(fd)(macOS 上的 fsync):最小 38 µs / 中位 63 µs / 最大 481 µs(200 次)
  写完调用 f.Sync()(Go 在 macOS 上用 F_FULLFSYNC):最小 990 µs / 中位 2876 µs / 最大 9112 µs(200 次)

Write 一次 4 KiB 中位 27 µs,而且另一个文件句柄立刻就能读到刚写的内容——对别的读者可见,不等于落盘。掉电或系统崩溃时,没有 Sync 的数据可能丢。

macOS 上的两种“同步”

实测 · 本机执行输出
  f.Write 之后没有 Sync、没有 Close,另一个文件句柄立刻读到:"hello"(5 字节)——数据已经对别的读者可见,但还没有保证落盘

syscall.Fsync 中位 63 µs,f.Sync() 中位 2876 µs,差约 45 倍。源码注释说明:Go 在 macOS 上让 Sync 调用 fcntl(F_FULLFSYNC),因为 macOS 的 fsync 并没有把内容完全刷到磁盘。Linux 上的数字会不同,我没有测。

同步策略的代价

实测 · 本机执行输出
  追加 300 条各 100 字节的记录:不 Sync                             总耗时      1.3 ms,平均每条      4.4 µs
  追加 300 条各 100 字节的记录:每条都 f.Sync()                       总耗时    721.7 ms,平均每条   2405.6 µs
  追加 300 条各 100 字节的记录:每 10 条 f.Sync() 一次(批量)             总耗时     73.6 ms,平均每条    245.4 µs
  追加 300 条各 100 字节的记录:每 100 条 f.Sync() 一次(批量)            总耗时      4.6 ms,平均每条     15.4 µs
  追加 300 条各 100 字节的记录:打开时加 os.O_SYNC(每次 Write 都同步)       总耗时     11.2 ms,平均每条     37.5 µs

300 条 100 字节的日志,每条都 Sync 要 721.7 ms,不 Sync 只要 1.3 ms;每 10 条一次 73.6 ms,每 100 条一次 4.6 ms。数据库的“组提交”就是这个道理:把多条记录合并成一次同步。代价是崩溃时可能丢掉最后一批。O_SYNC 的每条只要 37.5 µs,比 f.Sync() 便宜得多,我推测它在 macOS 上没有 F_FULLFSYNC 的力度,但没有验证。

安全替换文件

实测 · 本机执行输出
  安全替换文件的各步骤(50 次):创建 + 写入    最小 73 µs / 中位 160 µs / 最大 810 µs
  安全替换文件的各步骤(50 次):f.Sync()   最小 1103 µs / 中位 2840 µs / 最大 3670 µs
  安全替换文件的各步骤(50 次):os.Rename  最小 129 µs / 中位 215 µs / 最大 774 µs
  安全替换文件的各步骤(50 次):目录 Sync()  最小 1006 µs / 中位 2655 µs / 最大 4412 µs

写临时文件、Sync、重命名、再 Sync 目录,是常见的原子替换写法。实验里创建、写入、重命名合起来不到 0.4 ms,两次 Sync 约 5.5 ms。我没有模拟崩溃去验证每一步的必要性,这些步骤来自通用经验而不是本文的实验。

读文档

实测 · 本机执行输出
  // Fsync invokes SYS_FCNTL with SYS_FULLFSYNC because
  // on OS X, SYS_FSYNC doesn't fully flush contents to disk.
  // See Issue #26650 as well as the man page for fsync on OS X.
  Sync commits the current contents of the file to stable storage. Typically, this means flushing the file system's in-memory copy of recently written data to disk.

os.File.Sync 的文档说它把数据提交到稳定存储,通常意味着刷新文件系统缓存;具体平台上怎么做,要看 internal/poll 里的实现。

分步演示

文件写入与 fsync
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。机器是带内置 SSD 的 Mac(具体型号与文件系统没有记录);每个延迟项 200 次,日志策略各一次(300 条),原子替换流程 50 次;读过的源码与文档:internal/poll/fd_fsync_darwin.go,go doc os.File.Sync
    • 没有验证:真实的掉电/崩溃后数据是否丢失(无法在这里模拟)、Linux 上的 fsync/fdatasync 成本、不同文件系统的差别、O_SYNC 与 F_FULLFSYNC 在持久性上的真实差异、SSD 型号对延迟的影响、目录 Sync 的必要性
    京ICP备2026067502号-1京公网安备11010502063487号