RocksDB Sync Write on HDD 性能优化

2025.11.11
SF-Zhou

1. 背景

业务需求,需要在 HDD 上使用 RocksDB 并且每一笔写入都需要确保落盘,即写入返回成功后立即断电,重新加载也能完整恢复数据。每次写入实际上只有 100B 左右,需要尽可能地降低延迟。发现在 HDD 上 RocksDB 的 sync write 性能非常差,平均在 30ms 以上。而业务上又很难将多笔写入合并为大的 write batch 一次落盘,故有了本文中阐述的方案。

2. 分析

RocksDB 的 sync write 性能差,核心原因是使用 buffer IO 模式写完 WAL 文件后,fdatasync 的性能太差了。如果替换成 direct IO,应该可以规避 fdatasync。跟 AI 简单聊了两句,发现 RocksDB 上 SST 文件已经支持了 direct IO 读写了,讲道理让 WAL 支持 direct IO 应该不会太困难。

此时我又在忙别的活,就让 Copilot 帮我写一版,PR 在这里。Copilot 改的非常简单,第一眼我看着以为是瞎改。跟它掰扯了几下,又看了下代码,发现它做的是对的。一来 RocksDB 本身支持使用 direct IO 写文件,会在内存里维护最后一部分非对齐的内容,每次追加时会覆盖写最后的 512B,padding 部分会填充 0;二来 WAL 是支持跳过全 0 的记录的,意外断电恢复也就不是事了。

简单过了下单元测试,就开始将这个修改集成当我们的系统中,意外地发现性能非常好。启用 direct IO + sync,单次写入平均延迟降低到 4ms,直接变可用了!

本来以为这样就足够了,后面仔细阅读代码发现启用 direct IO 时,Sync 会直接跳过 fdatasync 操作。尝试改代码强制执行 fdatasync,性能直接回退到之前的状态。不得已还是得考虑完全去掉 fdatasync 的方案。

假定我们使用的企业级硬盘包含 PLDP 能力,即在 direct IO 写完成后,数据部分不会丢失,那么剩下的只需要保证文件元数据不丢失,即文件长度信息和 extent 状态信息。考虑到业务场景,一个巧妙的方法在 WAL 文件末尾预分配一块空间,写入全 0,并且使用 fdatasync 将元数据集中落盘;后续所有在这个区间的 write batch 写入都直接通过 direct IO 完成,因为文件长度和 extent 状态信息都不会发生变化,只要写下去了就可以认为它不会丢失了。预分配的过程可以在后台异步完成,不阻塞正常 write batch 写入。并且在 direct IO 模式下实现对应的 Sync 函数逻辑:即等待预分配的 fdatasync 确认落盘的长度大于 WAL 逻辑数据的长度即可。

使用该思路实现一版代码。中间还发现 RocksDB 中的一些 bug,给官方提了 PR 一并提交了:https://github.com/facebook/rocksdb/pull/14116

我也跑了一下 RocksDB 自带的 db_bench,能看到新的 direct IO + sync 远超 buffer IO + sync:

# disable use_direct_io_for_wal.
./db_bench --benchmarks=fillseq --db=/storage/data1/rocksdb --num=10000 --value_size=128 --sync=true --use_direct_io_for_wal=false
#> fillseq      :   25555.892 micros/op 39 ops/sec 255.559 seconds 10000 operations;    0.0 MB/s

# enable use_direct_io_for_wal.
./db_bench --benchmarks=fillseq --db=/storage/data1/rocksdb --num=10000 --value_size=128 --sync=true --use_direct_io_for_wal=true
#> fillseq      :     147.164 micros/op 6795 ops/sec 1.472 seconds 10000 operations;    0.9 MB/s

3. 总结

AI 确实越来越好用了,它可以成为你额外的并行生产力。但逻辑能力总还是差一点,需要非常耐心的引导,有些精细的活还是得自己动手。