Write 返回了,数据就落盘了吗?不一定:它只是进了操作系统的页缓存。这篇在 macOS 上量 Write、fsync、Go 的 Sync 各要多久,比较不同的同步策略,并读源码里为什么 Go 在 macOS 上不直接用 fsync。
追加写 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 的数据可能丢。
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 里的实现。