site logo

Marico's space

存储引擎调优:我从 p99 踩坑中学到的那些事

服务器技术 2026-04-21 11:31:34 13
存储引擎调优:我从 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。 收藏之前记得点个赞。