存储引擎调优:我从 p99 踩坑中学到的那些事
做数据库/存储相关工作的,谁没被 p99 延迟折磨过?明明测试环境跑得飞起,一上生产就这里抖那里卡。踩过几次坑之后我发现,问题往往不在"磁盘慢",而是你根本没测对东西。
这篇文章来自 beefed.ai 的工程师,写得很实战,正好把我踩过的坑都串起来了。强烈建议先收藏,再往下看。
先说个核心观点:基准测试不是为了跑出好看的数字,而是为了找出 SLO 和现实之间的差距。测错了,跑分再高也是幻觉。
设计有代表性的工作负载
很多人在测存储的时候喜欢用"裸测"——直接跑顺序读写,然后拿个很高的 IOPS 数字到处炫耀。但生产环境哪有那么理想?
关键是要问自己:真实流量长什么样?
- 读/写/扫描的比例是多少?
- key 和 value 大小分布是怎样的?别只告诉平均值,直方图更靠谱。
- 访问是否倾斜?热数据集中在哪些前缀?
- 并发是多少?峰值并发又是多少?
把这些东西摸清楚之后,用 YCSB 或者 RocksDB 自带的 db_bench 做合成工作负载,比"裸测"有意义一百倍。
举个例子:生产环境 90% 点查、10% 写入,key 16B,value 中位 512B,并发平均 24 峰值 240。
那映射到 YCSB 呢?workloada 配合 zipfian 分布,偏斜 0.9,线程数从 24 逐步拉到 240 模拟峰值。RocksDB 则是 fillrandom → readrandom → readwhilewriting 这样的流程。
还有一点——**预热很重要**。LSM 引擎有 compaction 瞬态,冷启动直接测会把稳态数据完全掩盖掉。
测试工具:fio + iostat + RocksDB stats
工具本身不难搭,难在**同步收集**。
工作负载生成器:fio 测块设备,db_bench 测 RocksDB,YCSB 跑应用级流量。
系统收集器:iostat -x -m 1 捕获设备级指标,vmstat/top 看 CPU/内存,perf/eBPF 找热点。
引擎遥测:RocksDB 的 --statistics、--histogram、--stats_per_interval 打开,关键日志也要抓。
fio 最佳实践(拿来直接用):
fio --name=randrw-4k-q64 \
--ioengine=libaio --direct=1 \
--rw=randrw --rwmixread=70 \
--bs=4k --numjobs=4 --iodepth=64 \
--time_based --runtime=120 --group_reporting \
--output=fio.json --output-format=json+
json+ 输出可以直接拿来做自动化回归对比,比看日志爽多了。
iostat 并行跑,收集 util、avgqu-sz、await。如果 %util 接近 100% 且 await 在上升——设备瓶颈到了。

关键指标:p99、吞吐量、IOPS、波动性
指标是信号,不是目标。选对指标才能问对问题。
| 指标 | 测量内容 | 为什么重要 |
|------|----------|------------|
| p99 延迟 | 99% 请求完成时间 | 尾部行为直接映射到 SLO,用户感知的是最慢的那些请求 |
| 吞吐量 MB/s | 聚合数据速率 | 大文件顺序读写场景 |
| IOPS | 每秒 I/O 操作数 | 小块随机读写,通过 Little's Law 与队列深度/延迟关联 |
| 波动性/直方图 | 分布形状 | 判断抖动是偶发异常还是确定性的规律 |
| 设备 %util | 设备繁忙程度 | 高 util + 上升 await = 设备饱和 |
关于 p99 我多说一句:在分布式调用链里,最慢的那一步决定整体延迟。中位数再好看,p99 拉胯的话用户体验还是崩的。
Little's Law 很好用:queue_depth ≈ IOPS × avg_latency_seconds。比如目标 50k IOPS、延迟 1ms,那 QD 至少要到 50 才能跑满。如果应用只能驱动 QD 4,就是瓶颈在应用侧,加并行度吧。
系统性瓶颈分析:测量 → 假设 → 改一个变量 → 重新测
调优要按顺序来,乱了就是白调。
先跑基线:预热 DB,跑 10-30 分钟测量窗口,把 fio/db_bench 输出、iostat、RocksDB stats 全存下来。环境信息也要记(CPU 型号、内核版本、NVMe 型号、文件系统挂载选项),不然下次复现的时候哭都来不及。
隔离原始设备能力:裸块设备跑 fio,direct=1,单线程开始,逐步加 numjobs/iodepth 找拐点。%util 到 100% 或 await 突然上升——找到瓶颈了。
缩小范围:
- CPU 瓶颈:top 里 sys/user 高,perf top 看到 compaction 线程占满
- I/O 瓶颈:%util 90-100%,await 上升
- RocksDB 内部:--stats_per_interval 看 compaction 写放大和 stall
RocksDB 调优顺序(别乱):
1. 先拿 --disable_wal 在临时 DB 上摸 WAL 的代价基线
2. 调 write_buffer_size 和 max_write_buffer_number,增加 memtable flush 大小
3. 增加 max_background_compactions 加快 L0→L1,但别把前台 CPU/I/O 抢走太多
4. 调整 level0_file_num_compaction_trigger、level0_slowdown_writes_trigger 控制写入 stall
5. 读延迟敏感?看 use_plain_table、mmap_reads、pin_l0_filter_and_index_blocks_in_cache
设备级注意:NVMe 确认用对了驱动,调度器别选太重的(mq-deadline 或 noop 常比 cfq 好),挂载选项 noatime 加上,文件系统选对。
有个反直觉的体会——**不要只看 IOPS 数字**。加 compaction 线程或者扩大队列深度,往往能拉高吞吐量,但同时也会拉大延迟分布。除非你先确认 CPU、I/O、内存都有余量,否则别动这些参数。
CI 自动化与测试套件
基准测试必须是代码,不是"我今天跑了一下感觉还行"。
测试套件结构:
- 01-sanity:裸设备 fio 单线程,检查设备健康
- 02-db-warmup:db_bench 填充确定性 keyset
- 03-read-heavy:匹配生产读比例的工作负载
- 04-write-heavy:锻炼 compaction 路径
- 05-spike-tests:突发并发模拟尾部行为
GitHub Actions 示例(拿来改改就能用):
name: storage-bench
on: [workflow_dispatch]
jobs:
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install fio
run: sudo apt-get update && sudo apt-get install -y fio
- name: Run benchmarks
run: ./bench/run_all.sh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: bench-results
path: results/**
CI 跑的是回归检测,不是最终验收。baseline 产物要存到持久存储里,最终批准要在专用硬件上跑。
报告的要点:存 json+ 原始数据,用 fiologparser_hist.py 转 CSV 绘图,计算 p50/p95/p99/吞吐量的变化量。自动化回归检查:p99 增幅超过阈值就告警。
最后,测量是为了学习,不是为了验证你心里那个"我调的参数肯定是对的"的假设。每次改一个变量,跑 ≥3 次取中位数,存好原始产物。这是铁律。
这篇文章的原文来自 beefed.ai,标题是 "Benchmarking & Performance Tuning for Storage Engines",译自 dev.to。
收藏之前记得点个赞。