存储基准测试笔记:不要只看平均延迟

借助 redis-benchmark、memtier_benchmark 和 sysbench,说明测试负载、预热、数据规模、p99、性能拐点和资源隔离为什么比平均值更重要。

Engineering notesStorage systemsPerformance

很多基准测试报告的问题不是数字错了,而是数字回答了一个没人关心的问题。

比如一份 Redis 压测结果写着:

text
吞吐:120k ops/s
平均延迟:1.8 ms

看起来很漂亮。但如果同一组数据里还有:

text
p99 延迟:80 ms
p999 延迟:1.2 s
超时率:0.3%

那这个系统对在线请求来说已经很危险。平均值没有撒谎,它只是把问题藏起来了。

我现在看 Redis、MySQL 或存储引擎的基准测试,第一反应不是问“快不快”,而是问:这个数字是在什么负载、什么数据规模、什么预热状态、什么并发压力和什么资源隔离条件下得到的。回答不上来,数字越漂亮越可疑。

工具只能产生数字,不能定义问题

常用工具本身没有问题。

Redis 侧常见的是:

  • redis-benchmark:Redis 自带,适合快速确认 GET/SET、流水线和连接数变化下的基础吞吐。
  • memtier_benchmark:更适合构造接近真实场景的 KV 负载,可以配置键分布、值大小、流水线、读写比例、线程数和连接数。
  • YCSB:适合把 Redis 放进更通用的 KV 负载中,与 RocksDB、MongoDB、Cassandra 等系统比较。

MySQL 侧常见的是:

  • sysbench:最常用,oltp_read_writeoltp_read_onlyoltp_write_only 都可以快速构造事务负载。
  • mysqlslap:MySQL 自带,适合简单 SQL 并发验证,但描述负载的能力有限。
  • tpcc-mysqlHammerDBgo-tpcBenchmarkSQL:更接近 TPC-C / TPC-H / TPC-DS 这类事务或分析型场景。

这些工具只是入口。真正决定基准测试有没有意义的是问题定义。

text
要回答什么问题?
  -> 单请求延迟?
  -> 峰值吞吐?
  -> 尾延迟是否稳定?
  -> 容量规划的性能拐点在哪里?
  -> 压缩、检查点和刷盘进入后台后还能不能稳定?

如果目标是线上容量规划,却只跑了一个短时间极限吞吐测试,工具再专业也没用。它测出来的是“系统在一小段时间内最多能被打到多高”,不是“系统能长期稳定服务到哪里”。

redis-benchmark 很容易测到一个过于简单的世界

redis-benchmark 很适合快速冒烟:

bash
redis-benchmark -h 127.0.0.1 -p 6379 -n 100000 -c 100 -t get,set

这条命令能告诉你服务能不能扛住基础 GET/SET 压力,但它默认构造出来的世界通常太干净:

  • 键值模型简单。
  • 请求类型少。
  • 客户端和服务端可能在同一台机器上互相抢 CPU。
  • 开启流水线后,吞吐数字会很好看,但单请求排队和尾延迟可能被掩盖。
  • 数据集如果太小,测到的更像热缓存路径。

所以 redis-benchmark 的结果适合作为冒烟测试,不适合直接用来证明生产负载很快。

如果报告里只有:

text
SET: 150000 requests per second
GET: 180000 requests per second

我会继续问:

text
值有多大?
键空间有多大?
是否使用流水线?
客户端和 Redis 是否隔离?
是否启用了持久化?
有没有慢查询、fork、AOF rewrite 或 RDB save?
有没有 p95 / p99 / p999?

这些问题不是吹毛求疵。Redis 的尾延迟很容易受到 fork、AOF fsync、内存碎片、网络队列和客户端流水线深度影响。只看每秒请求数,等于只看了最会讲故事的一列。

memtier_benchmark 更适合构造 Redis 负载

如果要认真测试 Redis,我更愿意从 memtier_benchmark 开始,因为它能把负载描述清楚:

bash
memtier_benchmark \
  --server=127.0.0.1 \
  --port=6379 \
  --threads=4 \
  --clients=50 \
  --ratio=1:9 \
  --data-size=1024 \
  --key-pattern=R:R \
  --key-maximum=10000000 \
  --test-time=300 \
  --pipeline=1

这类命令比一个裸 redis-benchmark 更有解释力,因为它至少把几个关键维度摊开了:

  • --ratio=1:9:读写比例。
  • --data-size=1024:值大小。
  • --key-maximum=10000000:键空间,影响缓存和内存访问路径。
  • --key-pattern=R:R:访问分布,决定热点形态。
  • --threads / --clients:客户端压力模型。
  • --pipeline:吞吐和排队行为会被它显著改变。

但即使使用 memtier_benchmark,也不能只拿最终汇总行当结论,尤其要小心流水线设置。

流水线能提高吞吐,因为客户端可以一次发出多个请求,减少往返等待。但这也会改变请求排队形态。对于批量写入或异步处理,流水线可能符合业务;对于用户同步请求,过深的流水线可能只是把延迟塞进队列,然后把吞吐数字擦得很亮。

所以报告里应该把流水线深度视为负载的一部分,而不是藏在命令参数中。

sysbench 的小表测试经常只是在测缓冲池

MySQL 基准测试最常用的工具是 sysbench

bash
sysbench oltp_read_write \
  --mysql-host=127.0.0.1 \
  --mysql-port=3306 \
  --mysql-user=root \
  --mysql-password=pass \
  --mysql-db=bench \
  --tables=16 \
  --table-size=1000000 \
  --threads=64 \
  --time=300 \
  prepare

正式运行:

bash
sysbench oltp_read_write \
  --mysql-host=127.0.0.1 \
  --mysql-port=3306 \
  --mysql-user=root \
  --mysql-password=pass \
  --mysql-db=bench \
  --tables=16 \
  --table-size=1000000 \
  --threads=64 \
  --time=300 \
  --report-interval=10 \
  run

这里最容易骗人的地方是数据集大小。

如果 tables * table-size * row-size 明显小于 InnoDB 缓冲池,很多查询会变成内存命中路径。这个结果并非没有价值,但它回答的是“热数据在内存里时 MySQL 表现如何”,而不是“数据集超过内存后 MySQL 表现如何”。

我看 sysbench 报告时会先找这些信息:

text
缓冲池大小
表数量
每张表的数据量
行大小
数据和索引总大小
prepare 后是否重启 MySQL
测试前是否预热
正式统计窗口多长

如果数据和索引总量只有 5GB,缓冲池有 32GB,那这个基准测试不能用来证明磁盘路径、页读取、检查点压力或 I/O 尾延迟。

它只能证明:这个负载在热内存中表现不错。

数据集大小决定你到底测到了什么

存储系统的基准测试一定要先讲清楚数据是否超过缓存层。

text
缓存命中负载
缓存未命中负载
混合负载
工作集缓慢增长
工作集大于内存

这些不是细节,而是不同问题。

在 Redis 场景中,键空间太小可能全是热键;在 MySQL 场景中,表和索引太小可能全部位于缓冲池;在文件系统场景中,如果没有说明页缓存,fio 或自写压测可能测到的是内存缓存,而不是设备路径。

这不代表缓存命中测试没有意义。很多线上系统就是依靠缓存工作,但报告必须诚实说明自己测的是哪一层。

我更喜欢把结果拆成几组:

text
热数据集:工作集完全装入内存
温数据集:大多数请求命中缓存,少数未命中
冷数据集:数据大于内存,持续出现未命中
增长数据集:写入路径持续扩大工作集

这样读者才知道数字应该怎么用。

预热和正式窗口必须分开

没有预热,刚开始的数据可能很差;预热过度,结果又会过于理想。

Redis 读取负载需要让热点分布进入稳定状态。MySQL 需要考虑缓冲池、自适应哈希索引、重做日志和检查点状态。LSM 系统还要等待刷盘、压缩、墓碑标记和 L0 文件数量进入稳定区间。

所以一次像样的基准测试至少要分成两段:

text
预热窗口
  -> 不计入最终结果
  -> 让缓存、缓冲区和后台任务进入目标状态

正式测量窗口
  -> 固定时长
  -> 记录吞吐、延迟分位、错误率和资源曲线

如果是写入型存储系统,还要警惕“蜜月期”。

刚启动或刚加载完数据时,后台债务可能还没积起来。运行 30 秒很快,运行 30 分钟后压缩开始追不上,这两个结果都是真的,但只有后者更接近长期服务能力。

平均延迟不够,p99 也不是终点

我至少会看这些指标:

text
吞吐
平均延迟
p50
p95
p99
p999
max
错误率
超时数量

平均值说明总体效率,分位数说明用户会不会撞上长尾。

但 p99 也不是魔法数字。对于高 QPS 服务,1% 已经是很多请求。p999、max、timeout 和错误率能帮你看到更糟糕的尾部。

更重要的是分位延迟要和时间线放在一起看。

text
每 10 秒记录:
  吞吐
  p95 延迟
  p99 延迟
  错误与超时
  CPU / 内存 / 磁盘 I/O / 网络
  后台任务状态

如果 p99 每隔一分钟尖刺一次,平均 p99 可能不难看,但线上用户会周期性卡顿。这种模式常常来自检查点、AOF fsync、GC、压缩、云盘额度或调度抖动。

吞吐峰值不是容量规划点

很多系统在低负载下延迟很低。随着并发增加,吞吐上升,p99 慢慢抬头。到某个点后,吞吐只增加一点,尾延迟却突然恶化。

这个点比峰值吞吐更重要。

text
输入负载 / 并发
  -> 吞吐
  -> p99 延迟
  -> 超时

我会按梯度扫描并发或输入负载,而不是只压一个最大值。

text
clients: 8   throughput: 30k/s   p99: 3 ms
clients: 16  throughput: 58k/s   p99: 5 ms
clients: 32  throughput: 96k/s   p99: 18 ms
clients: 64  throughput: 118k/s  p99: 95 ms
clients: 96  throughput: 121k/s  p99: 480 ms

真正可用的容量点不是 121k/s,而可能是 96k/s 之前。因为后面已经进入排队失控区间。

线上系统不应该长期运行在接近失控的边缘。基准测试报告如果只写峰值吞吐,就像只告诉你车能开到多快,却不告诉你什么时候刹不住。

客户端也可能先成为瓶颈

压测器不是透明的。

在 Redis 基准测试中,客户端线程数、连接数、流水线和网络栈都会影响结果。在 MySQL sysbench 测试中,Lua 负载、客户端 CPU、连接池、SQL 生成和结果处理也可能先达到瓶颈。

如果客户端和服务端位于同一台机器上,问题会更麻烦:

  • 客户端会抢占服务端 CPU。
  • 环回网络路径与真实网络不同。
  • NUMA、调度和中断路径不代表生产环境。
  • 客户端自己先到瓶颈,服务端还没被打满。

所以报告至少要说明:

text
客户端与服务端是否分机
客户端 CPU 使用率
客户端网络带宽
客户端线程和连接模型
压测器是否有错误或超时

压测器先达到瓶颈时,服务端的基准测试数字就没有太大解释力。

后台状态要和结果放在一起

存储系统的长尾通常不是凭空出现的。

Redis 要看:

  • AOF fsync。
  • AOF rewrite。
  • RDB save / fork。
  • 内存碎片和 eviction。
  • slowlog。
  • 网络输入输出缓冲区。

MySQL 要看:

  • 缓冲池命中率。
  • 重做日志和检查点年龄。
  • 脏页刷盘。
  • 锁等待。
  • transaction rollback。
  • temporary table。
  • I/O queue。

LSM / RocksDB / LevelDB 要看:

  • 内存表刷盘。
  • 压缩字节数。
  • L0 file count。
  • 待压缩字节数。
  • 写入停顿。
  • 读放大。
  • 写放大。

如果 p99 尖刺和后台任务时间线对得上,基准测试才开始有解释力。否则只能得出一个结论:某段时间慢了,但不知道为什么。

我会怎样写基准测试报告

一份能被相信的报告,至少应该包含:

  1. 目标:这次基准测试要回答什么问题。
  2. 工具和版本:redis-benchmarkmemtier_benchmarksysbench 或其它工具的版本。
  3. 环境:客户端与服务端拓扑、CPU、NUMA、内存、磁盘、文件系统、网络、云盘规格。
  4. 配置:Redis 持久化、MySQL 缓冲池、重做日志、存储引擎参数。
  5. 负载:读写比例、键分布、值大小、表规模、数据集是否大于内存。
  6. 方法:数据准备、预热、正式测量、并发梯度、采样间隔。
  7. 结果:吞吐、p50/p95/p99/p999、最大值、错误率、超时。
  8. 资源:CPU、内存、I/O、网络、客户端侧瓶颈。
  9. 后台状态:AOF、检查点、刷盘、压缩、GC、队列。
  10. 结论边界:这些数字适用于什么条件,不适用于什么条件。

最后一项最重要。

好的基准测试不是为了证明系统赢了,而是为了说明系统在哪些条件下仍然可信。平均延迟只是证据之一,真正有用的是解释:系统在什么负载下稳定,什么时候开始失控,以及为什么失控。