第11章 基准测试
方法论、工具与陷阱
章节导读
基准测试(benchmarking)是性能工作的"实验室科学":生产观测告诉你系统现在怎么样,基准测试告诉你系统在受控条件下能怎么样、两个方案哪个更好、这次改动是进步还是退步。但基准测试也是最容易自欺欺人的性能活动——Jim Gray 早在 1990 年代就总结过"基准测试的十二宗罪",Gregg 在本章开篇直言:错误的基准测试比没有基准测试更危险,因为它给错误决策披上了数据的外衣。一个"数据库 A 比 B 快 3 倍"的结论可能只是 A 的测试恰好命中了缓存、B 的测试没预热 JIT;一个"优化提升 20%"的声明可能只是两次运行的自然波动。本章的目标是让你具备设计、执行、判读基准测试的科学素养。
先明确基准测试的四大目的,不同目的对应不同设计。(1)容量规划:回答"单实例能扛多少 QPS(在 SLO 约束下)",需要逼近真实负载模型的宏基准,输出是"拐点容量"(P99 达 SLO 上限时的吞吐)而非极限吞吐。(2)方案对比:回答"A 与 B 哪个好",需要控制变量的对照实验——同硬件、同负载、同预热策略,只变被测因素。(3)回归检测:回答"新版本是否变慢",需要 CI 化的自动基准:固定环境、固定负载、统计显著性判断,性能回归超阈值即阻断发布。(4)理解系统:回答"瓶颈在哪、各配置的敏感度如何",需要参数扫描(sweep)——改变一个维度(队列深度、并发数、块大小)观察响应曲线,绘制系统的"性能地形图"。
统计学基础是基准测试的第一课。性能测量是随机变量:同一配置多次运行的结果有波动(CPU 频率漂移、缓存状态、后台干扰、云环境噪声)。核心概念:(a)多次重复——单次运行无意义,至少 5-10 次取分布;(b)中心趋势与离散度——均值受长尾影响,中位数更稳健;标准差/变异系数(CV = σ/μ)量化波动,CV >5% 说明环境噪声大,先治噪声再谈对比;(c)置信区间——"A 比 B 快 12% ± 3%(95% CI)"才是完整结论,CI 的计算用 t 分布(小样本);(d)显著性检验——两方案对比用 t 检验(正态假设)或 Mann-Whitney U(非参数,更稳健),p < 0.05 才声称差异存在;(e)效应量——统计显著不等于实际重要,1% 的差异即使显著也可能无业务意义,先定义"最小有意义差异"(MDE)再设计样本量;(f)分布形态——延迟数据几乎从不是正态的(右偏长尾),用百分位数(P50/P99/P99.9)而非均值描述,协调直方图(HdrHistogram)合并多轮数据。
测量系统的陷阱清单(本章的核心价值)。预热问题:JIT 编译(JVM 前几秒是解释执行,性能差 10 倍——预热到稳态再测量,或丢弃前 N 轮)、页缓存(首轮读盘、次轮读缓存——明确测"冷"还是"热",冷测要 drop_caches,热测要预热到命中率稳定)、CPU 频率(idle 降频,负载初期爬频——预热期同时让频率稳定)、连接池与线程池(懒初始化——预热期建立满池)。SSD 稳态问题:新鲜盘(FOB)性能虚高(SLC 缓存、无 GC 压力),必须先预处理(preconditioning:顺序写满 2 遍 + 目标负载写到稳态——SNIA 标准),稳态判定用滑动窗口的 IOPS 波动 <20%。测量偏差:观察者效应(测量工具本身的开销——wrk 的 Lua 脚本、抓包、剖析都会吃掉施压端或被测端 CPU);协调遗漏(coordinated omission,Gil Tene 的著名批判:闭环负载生成器在被测系统卡顿时"暂停发压",导致长尾延迟不被记录——P99.9 严重失真;开环负载(恒定到达率)或修正算法(HdrHistogram 的 CO 修正)是解药);时钟与计时粒度(低分辨率计时器量化误差);施压端瓶颈(压不动了以为是被测端极限——施压端 CPU/网络/端口要先验证有余量,分布式施压是大规模测试的标配)。
微基准(microbenchmark)与宏基准(macrobenchmark)的分工。微基准测单个组件/函数/机制:一次 malloc 多少 ns、一次系统调用多少 ns、一个数据结构的吞吐——工具如 Google Benchmark(C++)、JMH(Java,处理 JIT 陷阱的专业框架:@Warmup、@Fork 隔离 profile 效应、Blackhole 防死代码消除)、Go 的内建 testing.B、hyperfine(命令行程序)。微基准的陷阱:编译器优化掉被测代码(结果必须被"使用")、JIT 的内联与逃逸分析改变语义(JMH 的 -XX:+PrintAssembly 验证)、缓存状态与生产不符(微基准的数据集常小到全在 L1)。宏基准测端到端系统:完整应用 + 真实协议 + 负载模型——工具如 wrk/wrk2(HTTP,wrk2 支持恒定速率开环)、k6/Gatling/JMeter(脚本化场景)、sysbench(数据库/文件/CPU 的通用负载)、fio(存储)、iperf3/netperf(网络)、TPC 系列(工业标准但争议大)。宏基准的关键是负载模型的真实性:到达过程(泊松 vs 恒定 vs 突发)、请求分布(大小、类型、热点)、数据分布(倾斜度)——"均匀随机访问"的测试结果对热点负载毫无预测力。
可复现性是基准测试的工程纪律。环境固化:同一硬件/实例类型、同一内核与驱动版本、同一编译选项、记录全部 sysctl 与 BIOS 设置(电源策略、turbo、C-states——BIOS 差异可造成 30% 性能差);系统净化:关闭无关服务、停止自动更新、隔离测试机(无其他租户/负载)、绑核与中断隔离(排除调度噪声);流程脚本化:从环境检查 → 预热 → 测量 → 数据收集 → 统计分析全自动(人工步骤是误差源);数据完整保留:原始每请求延迟(而非只有汇总)、系统侧指标(测试期间的 CPU/内存/I/O/网络时序——解释异常轮次)、环境快照;报告规范:结论 + 置信区间 + 环境描述 + 复现步骤 + 原始数据链接。CI 性能回归检测的工程化:专用基准机(物理机优于云实例——噪声低)、A/B 交替运行(消除时间漂移:ABABAB 而非 AAAABB)、变化点检测算法(如 MongoDB 的 E-Divisive 用于自动识别性能变化的提交)。
负载生成器的选择细节。HTTP:wrk(多线程 + epoll,单机可打数十万 RPS,Lua 脚本定制请求——闭环模式,注意 CO 问题)、wrk2(开环恒定速率,延迟测量更真实)、hey/vegeta(Go 实现,vegeta 支持恒定速率与自定义到达过程)、k6(JS 脚本,场景编排强,云原生 CI 友好)、Gatling(Scala DSL,报告精美)、JMeter(GUI 老牌,线程模型重,高并发下施压端自身成瓶颈——分布式部署)。数据库:sysbench oltp_read_write(MySQL 生态标准)、pgbench(PostgreSQL 内建,注意其 -T 时长与 vacuum 前置)、BenchmarkSQL(TPC-C 实现)。存储与网络见前述 fio/iperf3/netperf(netperf 的 TCP_RR 测请求-响应延迟,比 iperf 的带宽视角更贴近 RPC 负载)。选择原则:施压端能力必须 >> 被测端极限(验证方法:被测端换成空响应,施压端能否打出目标速率的 2 倍);延迟测量用开环或 CO 修正工具;场景真实性优先于工具便利性。
基准测试结果的判读方法。永远同时看两侧:负载侧(吞吐、延迟分布、错误率)与系统侧(被测端的 CPU/内存/I/O/网络时序 + 施压端资源)——系统侧数据解释"为什么":吞吐拐点处哪个资源先饱和(USE 方法在基准测试中的应用);延迟毛刺对应什么系统事件(GC?限流?后台任务?);错误率上升的机制(超时?拒绝?队列溢出?)。曲线形态学:吞吐-并发曲线的三阶段(线性区 → 拐点 → 饱和/退化区——退化区说明争用,USL 模型拟合可外推极限);延迟-利用率曲线的曲棍球杆形态(排队论的必然);参数扫描的热力图(如块大小 × 队列深度的 IOPS 地形)。对比测试的判读纪律:差异超出 CI 才下结论;差异方向与机制假设一致才可信("A 快 15%"且火焰图显示 A 的锁路径消失 = 可信;"A 快 15%"但两侧火焰图无差异 = 疑噪声,重测);负面结果同样记录(避免发表偏倚——只报成功的优化会让团队高估自己的优化能力)。
最后是基准测试的伦理与常见滥用。厂商基准的批判性阅读:TPC 结果的可比性(不同 schema、不同硬件配置的"世界纪录"意义有限)、"最高性能"与"性价比"的口径切换、测试条件披露不全(稳态还是 FOB?预热多久?)。内部基准的常见错误:用基准结果直接外推生产(负载模型差异)、对比测试只调优了一方("我们花了三天调 A,B 用默认配置"——不公平对比)、把相关当因果(见第 2 章)。Gregg 的建议:基准测试是"受控实验",遵守实验科学的一切纪律——假设明确、变量控制、重复测量、统计检验、完整披露。做到这些,基准测试就是性能工程最强大的工具;做不到,它就是数字化的占卜。
核心概念
- 基准测试
- 统计学
- 微基准
- 可复现性
要点精析
协调遗漏(Coordinated Omission):延迟测量的头号陷阱。闭环负载生成器(wrk、JMeter 默认模式)的行为是"发完一个请求等响应再发下一个"——当被测系统卡顿 2 秒,生成器也停 2 秒,这 2 秒内"本应发出"的请求从未存在,它们的"延迟"(本应是 2 秒+)不被记录。后果:系统越卡,测得的延迟分布越"好看"(卡顿期间的请求消失了)——P99.9 可以被低估 10-100 倍。Gil Tene(HdrHistogram 作者)的著名演讲"How NOT to Measure Latency"用实机演示了这个骗局。解法:(1)开环负载(open-model):按预定到达率发请求不等响应(wrk2 的 -R 参数、vegeta 的恒定速率、Gatling 的 open injection)——请求按计划存在,延迟 = 响应时刻 - 计划发出时刻,卡顿被如实记录;(2)CO 修正:HdrHistogram 的记录修正功能(已知预期间隔,把长延迟"回填"到缺失的槽位);(3)至少报告负载模型(闭环/开环)让读者自行判断。判断工具是否闭环:看它的"目标速率"语义——达到速率靠加并发(闭环)还是靠定时发送(开环)。生产流量天然是开环的(用户不会因为你慢就不发请求),所以开环测试才有预测力。
预热(warmup)的完整清单与验证方法。JIT 预热:JVM 的 C2 编译在方法调用约 1 万次后触发,完全预热需分钟级——验证方法:观察吞吐时序曲线进入平台期(每 10 秒采样,连续 3 个采样点波动 <2% 即稳态),或用 -XX:+PrintCompilation 确认热点方法编译完成;JMH 的 @Warmup(iterations=5, time=1) 是标准化实现。缓存预热:页缓存(数据文件预读至命中率稳定——vmtouch 验证)、应用缓存(Redis 类:预热流量跑到命中率平台)、CPU 缓存(微基准的数据集触碰)。连接预热:连接池填满(数据库、HTTP 客户端)、TCP 慢启动(长肥管道的 cwnd 爬升——测试前先用大流量把窗口撑开,或接受前 N 秒的爬升期)。频率预热:CPU 从 idle 频率爬到 turbo 需百 ms 级——预热负载同时解决。GC 预热:JVM 的堆分代大小是自适应的(前几次 GC 后趋于稳定)——预热期让 GC 进入稳态节奏。纪律:预热时长与轮次写入测试脚本(不靠人工判断),预热数据明确丢弃(标记 warmup 轮),报告披露预热策略——"预热 5 分钟,丢弃前 3 轮,测量后 7 轮"是可复现性的最低要求。
SSD 与云盘的稳态测试(preconditioning)。SSD 的性能是状态的函数:新鲜盘(FOB)所有块已擦除,写路径无 GC——性能是标称峰值;随着写入,无效页积累,GC 开始与主机写争抢闪存带宽,性能下降到"稳态"(steady state)——随机写稳态性能可能只有 FOB 的 1/3-1/2。SNIA SSS PTS 标准的预处理流程:(1)Purge(NVMe format 或 ATA secure erase——恢复 FOB,云盘不可做);(2)Preconditioning:顺序写全盘 2 遍(填满映射表);(3)目标负载预处理:用测试负载(如 4K 随机写)写到稳态——判定标准:连续 5 个测量窗口(每窗口如 1 分钟)的 IOPS 均值波动 <20% 且无持续下降趋势。云盘的对应问题:突发额度(burst balance)——测试前确认额度满(或明确测"额度耗尽后的基线性能",两种都是有效测试但语义不同,必须披露);gp3 类恒定配额盘无此问题。fio 的 --ss 参数族内建稳态判定(--ss=iops:20% --ss_dur=300 自动检测并停止)。不做预处理的 SSD 基准是"测了个寂寞"——厂商标称值与你的生产体验的差异大半在此。
统计方法的实操规范。重复次数:环境噪声决定——先跑 3 次算 CV(变异系数),CV <2% 时 5 次够用,CV 5-10% 需 10+ 次或先治噪声(云环境 CV 常 >5%,物理机 <2%)。对比检验:两组各 n≥5 的样本,先做正态性检验(Shapiro-Wilk)与方差齐性(Levene),满足用 Student's t 检验,方差不齐用 Welch's t,非正态用 Mann-Whitney U;Python 的 scipy.stats 一行实现。报告格式:均值/中位数 ± 95% CI(CI = t(0.975, n-1) × σ/√n),延迟用百分位数族(P50/P90/P99/P99.9 + max)而非单一均值。多方案对比(>2 组):ANOVA + 事后检验(Tukey HSD)控制多重比较的假阳性。效应量:Cohen's d((μA-μB)/σ_pooled)——d>0.8 大效应,配合业务 MDE(最小有意义差异,如"延迟改善 <5% 不值得上线风险")。时序数据的特殊处理:性能时序有自相关与漂移(温度、碎片积累),ABAB 交替设计消除时间趋势,变化点检测(E-Divisive、Bayesian online changepoint)用于 CI 中自动定位回归提交。工具:hyperfine 内建统计对比(--export-json + 显著性提示)、pytest-benchmark、CodSpeed 类 CI 基准服务。
微基准框架的反优化对抗。编译器与 JIT 会"看穿"微基准并优化掉被测代码:死代码消除(结果没被使用——整个循环被删,测出 0.3ns 的"神速")、常量折叠(参数是编译期常量——计算被预执行)、循环不变量外提、JIT 的激进内联与逃逸分析(对象分配被消除——测不出 malloc 成本)。对抗手段:Google Benchmark 的 benchmark::DoNotOptimize(value)(内联汇编屏障强制"使用"结果)与 ClobberMemory();JMH 的 Blackhole.consume() 与 @State 对象的生命周期管理、返回结果作为方法返回值(JMH 自动消费);Go 的 runtime.KeepAlive 与 b.N 循环规范。JMH 的额外机制:@Fork 多 JVM 进程隔离(避免 profile 污染——前一个基准的 JIT 决策影响后一个)、@Measurement 与 @Warmup 的迭代控制、-prof 集成 perf/async-profiler(基准运行时同步剖析——验证"测的确实是被测代码":火焰图上被测函数应占主导,否则基准无效)。验证纪律:任何微基准结果先问"编译器有没有把它优化掉"——用反汇编(-XX:+PrintAssembly、objdump)或剖析确认被测代码真的在执行。
负载模型的真实性设计。到达过程:生产流量是泊松到达(独立随机间隔)+ 周期波动(早晚高峰)+ 突发(秒杀、推送)——恒定速率(wrk2 -R)测稳态容量,泊松到达(k6 的 arrival-rate executor、Gatling 的 PoissonUsers)测排队行为,突发模型(阶跃 + 尖峰)测弹性与恢复。请求分布:大小分布(对数正态而非均匀——真实 API 的 payload 长尾)、类型混合(读写比、接口比例按生产流量画像配置)、数据访问分布(Zipf 倾斜——热点 key 的缓存命中率与均匀分布完全不同,YCSB 的 workload A-F 是数据库负载模型的标准库:A 是 50/50 读写 Zipfian,B 是 95/5 读多,E 是扫描)。并发模型:闭环(固定并发数,think time 模拟用户思考)vs 开环(固定到达率)——容量规划用开环找拐点,用户体验模拟用闭环 + think time。数据准备:数据集大小 >> 缓存(测真实 I/O)或明确分层(热集在缓存、冷集在盘的两段测试);数据内容影响压缩与分支预测(真实数据脱敏后使用,合成数据注意熵——全零数据被压缩/去重优化欺骗)。负载模型披露:报告必须包含到达过程、请求分布、数据分布、并发模型四要素,否则结果不可比。
施压端(load generator)的容量验证。基准测试的隐形失败模式:施压端先于被测端饱和——测出的"极限"是施压端的极限。验证方法:(1)空载校准——被测端换成最小响应(echo 服务/空 handler),施压端全力打压,确认能达到目标测试速率的 2 倍以上;(2)施压端监控——测试期间采集施压端 CPU(wrk 打满一个核就接近其单线程极限)、网络带宽(对比网卡规格)、本地端口(短连接测试的 TIME_WAIT 与端口耗尽——第 9 章)、fd 限制;(3)分布式施压——单机不够时多施压端聚合(vegeta/k6/Gatling 原生支持,wrk 手动多机),聚合时注意时钟同步(延迟统计的时间基准)与网络路径一致(施压端到被测端的 RTT 应远小于被测延迟,否则测的是网络)。施压端工具的性能排序(HTTP 场景单机能力):wrk/wrk2(C + epoll,最强)> vegeta/k6(Go,强)> Gatling(JVM,中强)> JMeter(线程模型,弱——万级并发需分布式)。云端施压注意:跨 AZ/Region 施压引入网络延迟与带宽费,同 AZ 施压 + 独立子网是标准做法。
系统级基准工具的正确用法。sysbench:cpu(素数计算——纯 CPU 对比,受频率与编译影响)、memory(连续读写带宽——对内存带宽干扰敏感,云环境噪声检测器)、threads(线程调度器压力——mutex 争用与上下文切换的综合)、fileio(文件 I/O——prepare 阶段生成文件,测前 drop_caches 明确冷测)、oltp_*(MySQL 负载——注意 --tables 与 --table-size 决定数据集大小,--rand-type=zipf 启用倾斜)。fio:见第 8 章(QD 扫描、预处理、稳态判定)。iperf3:单流 TCP 受 cwnd/单线程限制(测单流性能),-P 8 多流测带宽上限,--bidir 双向,UDP 模式(-u -b)测丢包与 pps;注意 iperf3 单进程单线程——高带宽(25G+)测试要多进程多端口。netperf:TCP_RR(请求-响应往返——RPC 类负载的延迟基准,输出 transactions/s 与 P50/P99 延迟分布)、TCP_CRR(每事务建连——连接建立成本)、TCP_STREAM(带宽);netperf 的测试语义比 iperf 更贴近应用负载。stress-ng:不是基准而是负载发生器(--metrics-brief 输出 bogo ops 可做粗略对比,但 bogo ops 的口径不严谨,不建议用于严肃对比)。
应用级基准的场景化设计。真实应用的基准不是"打压一个 URL"而是"场景剧本":用户旅程(浏览 → 搜索 → 加购 → 下单的序列,各步骤比例按生产漏斗)、混合负载(读接口 80% + 写接口 15% + 重接口 5% 的加权混合——k6/Gatling 的 scenario 权重)、数据依赖(下单前必须先创建用户与商品——setup 阶段的数据准备)、状态积累(长时间运行中缓存增长、碎片积累、日志膨胀——soak test 浸泡测试 4-24 小时观察性能漂移)。压测类型学:负载测试(预期流量验证 SLO)、压力测试(超载找拐点与失效模式——优雅降级还是雪崩)、浸泡测试(长时间稳定性——泄漏与漂移)、尖峰测试(突发流量的弹性——扩容时延验证,第 10 章)、破坏测试(chaos:杀实例/断网络下的性能韧性)。报告维度:每场景的吞吐-延迟曲线(并发扫描)、SLO 达标容量、拐点资源画像(USE 数据)、错误模式分类。这套设计把"基准测试"升级为"性能验收工程",是上线前性能评审的标准内容。
CI 性能回归检测的工程实现。挑战:CI 环境噪声大(共享 runner 的性能波动 10-30%),朴素阈值("慢 5% 即报警")误报泛滥。工程解法:(1)专用基准硬件——物理机或独占云实例(金属实例),关闭 turbo 波动(固定频率)、绑核隔离、无其他负载;(2)A/B 交替测量——同一 runner 上交替运行 base 与 candidate(ABABAB...),配对比较消除时间漂移(温度、碎片);(3)统计方法——变化点检测(E-Divisive with Medians:MongoDB 性能 CI 的开源实现,对时序自动定位变化点)或贝叶斯比较(后验概率 P(candidate 更慢) > 阈值才报警);(4)分层基准——微基准(函数级,JMH/Google Benchmark,敏感但可能与端到端脱节)+ 宏基准(关键用户旅程,稳定但慢)两层互补,微基准定位、宏基准把关;(5)回归处置流程——报警附火焰图差分与 profile 对比(自动采集),bisect 定位提交,性能预算(performance budget:关键路径的 CPU 时间/内存/延迟上限写入 CI 断言)。开源参考:MongoDB 的 signal-processing 库、CodSpeed、pytest-benchmark 的 compare 模式、Go 的 benchstat(官方统计对比工具,配对 t 检验)。
TPC 与工业标准基准的批判性使用。TPC-C(OLTP 事务处理)、TPC-H/DS(分析查询)、SPEC CPU(计算)、MLPerf(机器学习)是行业通用语言,价值在方法论的标准化(schema、负载、审计流程都有严格规范,结果可审计)。但使用时的批判点:(1)可比性边界——TPC-C 结果依赖硬件配置与调优深度,"世界纪录"是特定配置的产物,与你的环境无关;(2)口径游戏——price/performance 的分母(含不含软件许可、3 年维保)与分子(tpmC 的口径)可被包装;(3)负载模型的时代性——TPC-C 的 warehouse 模型是 1990 年代的订单录入,与现代互联网负载(读多写少、热点倾斜、水平扩展)差异大;(4)调优针对性——厂商为基准特调(专用 schema 设计、参数固化),不代表通用负载表现。正确使用方式:把标准基准当"参考负载"而非"真理"——用 TPC-H 对比数据库的分析能力时,同时跑你自己的查询画像;引用厂商 TPC 结果时检查 Full Disclosure Report(审计披露的完整配置)。内部基准的黄金标准始终是:贴近自己生产负载的自定义基准 + 严格统计纪律。
基准测试报告与复现包的标准结构。一份合格的基准报告包含:(1)摘要——结论 + 关键数字 + 置信区间(一页内);(2)测试目的与假设——"验证 X 优化对 P99 的影响,预期改善 >10%";(3)环境——硬件(型号/规格/BIOS 设置)、软件(OS/内核/驱动/运行时版本、关键 sysctl)、网络拓扑(施压端与被测端的位置关系);(4)负载模型——工具与版本、到达过程、请求/数据分布、并发模型、预热策略;(5)方法——重复次数、轮次结构(ABAB)、测量指标、统计方法;(6)结果——吞吐/延迟分布/错误率 + 系统侧资源时序(USE 数据)+ 曲线图;(7)分析——差异的机制解释(火焰图差分、profile 对比);(8)威胁与局限——已知偏差(CO、噪声源)、外推边界;(9)复现包——脚本、配置、原始数据(每请求延迟文件)、环境快照,一键复现命令。复现包是报告的灵魂:不可复现的基准结果在工程上等于不存在。团队实践:基准脚本入库(版本控制)、原始数据归档(对象存储 + 索引)、报告模板化——让基准测试成为可积累的组织资产而非一次性活动。
代码示例
wrk -t4 -c100 -d30s --latency http://target/api
/* 闭环压测:4 线程 100 连接 30 秒,输出延迟分布 */
wrk2 -t4 -c100 -d60s -R2000 --latency http://target/api
/* 开环压测:恒定 2000 RPS 到达率——延迟测量更真实 */
vegeta attack -duration=60s -rate=2000 -targets=targets.txt | vegeta report -type=hist[0,10ms,50ms,100ms,500ms,1s]
/* 恒定速率 + 直方图报告 */
hey -z 30s -q 50 -c 100 http://target/api
/* hey:每 worker 50 QPS × 100 worker */HTTP 负载生成器的四种形态与选择。wrk 是闭环标杆:-c 是并发连接数(不是速率!),实际 RPS = 并发/平均延迟——系统变慢时 RPS 自动下降(闭环特性),--latency 输出 P50/P90/P99 分布(注意其 P99.9+ 受 CO 影响失真)。wrk2 是 Tene 参与改造的开环版:-R2000 指定恒定到达率,内部用 CO 修正的直方图——测延迟分布(尤其长尾)必须用开环,wrk2 或 vegeta 是正确工具。vegeta 的设计哲学是"恒定速率的攻击":targets.txt 描述请求集(可加权混合多接口),report 的 -type=hist 自定义桶边界(桶设计要覆盖 SLO 阈值——100ms SLO 就在 100ms 处设桶边界),还能输出 HDR 直方图与时序图(vegeta plot)。hey 适合快速验证(Go 单二进制,-q 是每 worker 速率)。选择决策:容量拐点测试(找最大稳定 RPS)用 wrk2/vegeta 做速率扫描(500→1000→2000→4000 RPS 逐档,每档记录延迟分布与错误率,P99 超 SLO 的前一档即容量);SLO 验证用开环恒定速率跑生产峰值的 1.5 倍;快速冒烟用 hey/wrk。共同纪律:施压端与被测端分离部署、施压端资源监控(CPU <70% 才可信)、测试前空载校准施压端上限。sysbench oltp_read_write --mysql-host=DB --mysql-user=test --mysql-password=test --mysql-db=sbtest --tables=8 --table-size=1000000 --threads=64 --time=300 --report-interval=10 --rand-type=zipfian run
/* 8 表 × 100 万行,64 线程,Zipfian 访问,每 10 秒输出中间结果 */
sysbench oltp_read_write ... prepare
/* 测试前数据准备 */
sysbench oltp_read_write ... cleanup
pgbench -i -s 100 postgres
/* PostgreSQL:初始化 scale 100(约 1.5GB) */
pgbench -c 32 -j 8 -T 300 -P 10 --latency-limit=100 postgres
/* 32 客户端 8 线程 300 秒,超 100ms 计为 late */数据库基准的标准工具与参数语义。sysbench 的关键参数:--tables × --table-size 决定数据集总量(8 × 100 万行 ≈ 2GB——必须远大于 buffer pool 才测到 I/O,或明确小于以测纯内存性能,两种语义都要披露);--rand-type=zipfian 启用倾斜访问(默认 special 也是热点分布,uniform 均匀——均匀分布下缓存命中率虚高,与生产不符);--report-interval=10 每 10 秒输出 tps/qps/延迟百分位(时序数据用于观察稳态与毛刺,比只有最终汇总信息量大得多);--threads 扫描(16/32/64/128/256 逐档跑,绘制吞吐-并发曲线找拐点);prepare/run/cleanup 三阶段生命周期。pgbench 的 -s(scale factor:1 ≈ 15MB,100 ≈ 1.5GB)决定数据量;-c 客户端数与 -j 工作线程(-c 128 -j 8 表示 8 个线程各管 16 个客户端——施压端效率);--latency-limit 把超限事务单独计数(late 列——SLO 视角);-r 报告每语句延迟(哪条 SQL 慢);注意 pgbench 默认脚本含 UPDATE(写负载,测前 checkpoint 与 autovacuum 状态要记录)。方法论:数据库基准的系统侧同步采集(iostat 看 IOPS 与延迟、perf 看数据库进程火焰图、buffer 命中率从数据库统计视图取)——吞吐数字必须配资源画像才能解释拐点机制(I/O 饱和?锁争用?CPU 上限?)。hyperfine --warmup 3 --runs 10 --export-json results.json "command_a" "command_b"
/* 内建预热与多轮统计,自动对比两个命令 */
hyperfine --prepare "sync; echo 3 > /proc/sys/vm/drop_caches" --runs 5 "grep -r pattern /usr/share/doc"
/* 每轮前清缓存:冷缓存基准 */
go test -bench=. -benchmem -count=10 ./pkg > old.txt
/* 修改代码后 */
go test -bench=. -benchmem -count=10 ./pkg > new.txt
benchstat old.txt new.txt
/* 官方统计对比:中位数、区间、显著性 p 值 */微基准的统计化工作流。hyperfine 是命令行程序的基准框架:--warmup 3 预热轮(丢弃)、--runs 10 测量轮、自动计算均值/标准差/相对倍数("command_b ran 1.35 ± 0.08 times faster"——自带置信区间!)、--prepare 每轮前的清场命令(drop_caches 冷测、rm 临时文件)、--export-json 原始数据导出(自定义统计分析)。它把"跑一次 time 就下结论"的坏习惯升级为统计对比。benchstat 是 Go 生态的统计对比标准:输入两组 -count=10 的基准输出(同机交替运行更佳:go test -bench=. -count=10 分别跑 old/new 代码,或用 git worktree 交替),输出每基准的中位数变化、置信区间与 Mann-Whitney p 值——"time/op: 152ns ± 5% → 131ns ± 3%, -13.8% (p=0.001 n=10+10)"是完整可信的结论格式;-delta-test 可换检验方法。JMH 的对应流程:mvn archetype 生成骨架,@Benchmark 方法 + @OutputTimeUnit + Blackhole,运行后 jmh-result.json 用脚本做 t 检验(JMH 自带 -rf json 与 visualizer 工具)。共同纪律:微基准的每个结果都要过"统计显著 + 机制可解释"双关——显著但解释不了的差异要怀疑测量伪影(频率漂移、缓存状态、编译器版本差异)。fio --name=precondition --ioengine=libaio --direct=1 --rw=write --bs=128k --iodepth=32 --filename=/dev/nvme1n1 --loops=2
/* 预处理第一步:顺序写满 2 遍 */
fio --name=steadystate --ioengine=libaio --direct=1 --rw=randwrite --bs=4k --iodepth=32 --numjobs=4 --filename=/dev/nvme1n1 --time_based --runtime=3600 --ss=iops:10% --ss_dur=600 --ss_ramp=600 --log_avg_msec=10000 --write_iops_log=ss
/* 稳态测试:--ss 自动判定(600 秒窗口内 IOPS 波动 <10% 即停) */
fio --name=lat --ioengine=io_uring --direct=1 --rw=randread --bs=4k --iodepth=1 --filename=/dev/nvme1n1 --runtime=60 --time_based --percentile_list=50:90:99:99.9:99.99存储基准的 SNIA 式流程用 fio 实现。第一条是预处理(preconditioning):对裸设备(--filename=/dev/nvme1n1,危险操作——确认是测试盘!文件系统测试用文件路径)顺序写满 2 遍,把 FTL 映射表填满、消除 FOB 状态。第二条的 --ss 参数族是稳态判定自动化:--ss=iops:10% 定义稳态标准(滑动窗口 IOPS 波动 <10%)、--ss_dur=600 窗口时长、--ss_ramp=600 判定前的稳定期(丢弃);达到稳态即自动停止并输出 ss 报告(含稳态 IOPS 与波动)——这是"稳态随机写性能"的标准测法,厂商标称与生产体验的桥梁。--write_iops_log 每 10 秒记录 IOPS 时序(画出从 FOB 到稳态的下降曲线——SSD 性能状态机的可视化)。第三条测延迟分布:QD1 随机读的 percentile_list 输出 P50 到 P99.99(NVMe 的 P50 约 10-20μs,P99.99 可能 100μs+——长尾来自设备内部 GC 与磨损均衡)。完整报告应含:预处理确认、稳态判定数据、QD 扫描曲线(第 8 章实验)、延迟百分位族、测试期间的 smartctl 快照(温度节流检查——nvme smart-log 的 thermal management 计数)。k6 run --vus 0 --duration 5m scenario.js
/* scenario.js 内容:
export const options = {
scenarios: {
browsing: { executor: "constant-arrival-rate", rate: 800, timeUnit: "1s", duration: "5m", preAllocatedVUs: 200 },
ordering: { executor: "ramping-arrival-rate", startRate: 10, timeUnit: "1s", stages: [{ duration: "2m", target: 100 }, { duration: "3m", target: 100 }], preAllocatedVUs: 50 },
},
thresholds: { http_req_duration: ["p(99)<200"], http_req_failed: ["rate<0.01"] },
};
*/
k6 run --out json=results.json scenario.jsk6 的场景化基准脚本展示现代负载测试的三个关键能力。(1)开环到达率执行器:constant-arrival-rate 按恒定速率(800 请求/秒)发起,preAllocatedVUs 是执行池(VU 不够时 k6 报 dropped iterations——施压端容量不足的信号,等价于"施压端饱和"检测);ramping-arrival-rate 做速率爬坡(找拐点的自动化版本)。(2)多场景混合:browsing 与 ordering 两个场景并行,权重即生产流量画像(读 8:写 1 的混合负载)——单接口打压的结果对混合负载没有预测力(缓存与锁的交互不同)。(3)阈值断言:thresholds 把 SLO 写进脚本(p99<200ms、错误率<1%),超限则 k6 退出码非零——CI 集成的性能门禁,性能回归检测的最后一道闸。结果输出:内建汇总(http_req_duration 的 p90/p95/p99、每秒速率、VU 数)+ --out json 原始数据(每请求记录,供 HdrHistogram 式分析与 CO 检查)。k6 的 JS 脚本还能做完整用户旅程(setup 造数据 → 场景步骤带 check 断言 → teardown 清理),把"基准测试"扩展为"性能验收自动化"。同类工具 Gatling(Scala DSL + 精美 HTML 报告)与 JMeter(GUI + 插件生态)能力相近,选择看团队技术栈——方法论(开环、混合场景、阈值门禁)是通用的。实验笔记
实验十一:设计并执行一次符合科学规范的基准测试。
实验目标:(1)完整经历"目的 → 设计 → 预热 → 测量 → 统计 → 报告"的基准测试生命周期;(2)亲手复现协调遗漏、预热缺失、施压端饱和三大陷阱并量化其失真程度;(3)建立可复现的基准测试脚本与报告模板。
环境准备:两台 Linux 虚拟机(被测端 server、施压端 client,同宿主机或同 VPC),被测端部署一个简单 HTTP 服务(nginx 静态文件,或一个故意含 sleep 与 CPU 工作的 Python/Go 应用——后者更能暴露延迟问题),施压端安装 wrk、wrk2(源码编译)、vegeta、hyperfine、k6。记录环境:两端规格、内核版本、RTT 基线(ping)。
步骤一(陷阱复现:协调遗漏):给被测应用注入一个"偶发卡顿"(如每 100 个请求 sleep 2 秒,或用 tc netem 周期性加延迟)。分别用 wrk(闭环,-c 50)与 wrk2(开环,-R 1000)各测 60 秒,对比 P99 与 P99.9:预期 wrk 的长尾显著低于 wrk2(卡顿期间 wrk 停发请求,长尾被抹平)。计算失真倍数(wrk2 的 P99.9 / wrk 的 P99.9)。写一段 200 字结论解释机制——这个实验让你终身对闭环工具的延迟数据保持警惕。
步骤二(陷阱复现:预热缺失):被测端换成 JVM 应用(如一个 Spring Boot 的 hello 服务,或任何 Java HTTP 服务)。重启后立即用 wrk2 -R 500 测 60 秒(每秒记录 RPS 与延迟),观察前 10-20 秒的低吞吐高延迟(解释执行期)与之后的稳态(JIT 完成)。对比"丢弃前 30 秒"与"全量统计"两种口径的 P99 差异。再用 -XX:+PrintCompilation 或 JFR 确认热点方法的编译时刻与性能拐点的时间对应。结论:预热策略必须写入测试脚本。
步骤三(陷阱复现:施压端饱和):在施压端用 wrk -t1 -c10000 打一个极快响应(nginx 空 200),观察 RPS 到达某值后不再增长且施压端 CPU 100%——这是施压端极限不是被测端极限。验证:加第二个施压端(或 -t4)后总 RPS 上升。空载校准流程实践:被测端换 echo 服务,确认施压端能打出目标速率 2 倍以上,记录施压端的能力上限表(每线程 RPS 上限、CPU 占用曲线)。
步骤四(正式基准:容量拐点扫描):选定被测应用(建议含真实工作的版本:一次 JSON 序列化 + 一次内存计算 + 可选的磁盘/DB 访问),用 wrk2 做速率扫描:从 200 RPS 开始每档 ×1.5(200/300/450/675/1000/1500...),每档 60 秒,记录:实际达成速率、错误率、P50/P99/P99.9、被测端 CPU/内存(mpstat/free 时序)。绘制"到达率-达成率"曲线(达成率开始低于到达率即饱和点)与"到达率-P99"曲线(曲棍球杆拐点)。确定 SLO 容量:P99 < 100ms 且错误率 <0.1% 的最大速率。同步记录被测端 USE 数据,确认拐点处的饱和资源(CPU?锁?I/O?)。
步骤五(对比测试与统计):对被测应用做一个"优化"(如加缓存、换序列化库、或故意做一个伪优化),设计 A/B 对比:ABAB 交替各 5 轮(每轮 60 秒,速率固定在拐点的 70%),用 Mann-Whitney U 检验(Python scipy 或手算)对比两组 P99,报告效应量与置信区间。同时采集两组的火焰图做差分(第 4 章工具),验证"统计差异"与"机制解释"的一致性。若差异不显著,如实报告——负面结果也是结果。
步骤六(报告与复现包):按本章要点的报告结构写完整报告:摘要、环境、负载模型、方法、结果(曲线图 + 统计表)、分析(火焰图差分)、局限(虚拟机噪声、CO 处理)、复现包(全部脚本 + 原始数据 + 一键运行说明)。
预期结果:三大陷阱的量化复现数据、容量拐点曲线与 SLO 容量结论、A/B 对比的统计报告、一份可复现的基准测试包。
进阶挑战:(1)把步骤四改造成 k6 的 ramping-arrival-rate 自动化扫描 + thresholds 门禁,集成进一个 shell 流水线;(2)给步骤五加 benchstat 式的时序漂移控制(测试期间记录 CPU 温度/频率,分析其对结果的影响);(3)设计 Zipfian 数据分布的负载(k6 脚本按权重选 key),对比均匀分布下的缓存命中率与 P99 差异;(4)用 hdr-heatmap 或 HdrHistogram 的可视化工具绘制两轮测试的延迟分布热力图,观察长尾形态随负载的变化。
延伸阅读
- 《Systems Performance》第 2 版第 12 章 Benchmarking 原文。Gregg 的基准测试方法论:四大目的(容量规划/对比/回归/理解)、统计学基础(均值/中位数/百分位/多模态分布)、观测整合(基准测试必须同步采集系统侧数据)、以及著名的"基准测试陷阱"清单(预热、稳态、施压端、协调遗漏)。原书的"benchmarking checklist"可直接作为实验步骤四到六的执行清单。
- Gil Tene 的演讲《How NOT to Measure Latency》(YouTube,多版本)与《Coordinated Omission》相关博文(highscalability.com 与 InfoQ 的文字版)。延迟测量方法论的震撼教育:用实机演示闭环工具的 CO 失真(同一个系统测出 P99.9 差 100 倍)、HdrHistogram 的设计动机、"延迟是分布不是数字"的完整论述。每个做性能测试的工程师都应该看这个演讲——它会永久改变你对延迟数据的怀疑程度。
- Jim Gray 主编《The Benchmark Handbook》(第 2 版, Morgan Kaufmann)。基准测试领域的奠基文献:Gray 总结的基准测试十二宗罪(如"报告不完整""价格未更新""未审计")、TPC 的设计哲学与历史、可审计性与公平性原则。虽出版于 1990 年代(数据库基准的黄金时代),其方法论批判对今天的云基准、AI 基准(MLPerf 的争议)依然适用。
- SNIA Solid State Storage Performance Test Specification(SSS PTS,snia.org 免费下载)。存储基准的工业标准:预处理(purge/precondition)、稳态判定(五窗口 20% 规则)、测试环境要求、报告规范的完整定义。fio 的 --ss 参数族就是按此标准实现的。做任何 SSD/云盘性能对比前,按此规范执行预处理——否则数据无效。附录的企业级与客户端测试差异说明也很有价值。
- JMH 官方文档与 Aleksey Shipilëv 的《JMH: Java Microbenchmark Harness》系列文章(shipilev.net 的 benchmarking checklist 一文尤其经典)。JVM 微基准的全部陷阱与对抗:死代码消除、常量折叠、循环展开、profile 污染、安全点偏差,每个陷阱附"错误示例 → 正确写法"。Shipilëv 的 checklist("你的基准测的是你想测的东西吗"十问)适用于一切语言的微基准。
- Google Benchmark 文档(github.com/google/benchmark)与 C++ 性能社区的基准实践(CppCon 的 benchmarking 主题演讲,如 Chandler Carruth《Efficiency with Algorithms, Performance with Data Structures》中的测量方法部分)。C++ 微基准的 DoNotOptimize/ClobberMemory 机制、自定义计数器、复杂度估计(ComplexityBigO)的用法。原生语言基准还要控制编译器版本与优化级别——报告必须披露完整编译命令。
- MongoDB 性能测试基础设施的公开资料:《Continuous Performance Testing》系列博客与 signal-processing 开源库(github.com/mongodb/signal-processing,E-Divisive 变化点检测的 Python 实现)。工业级 CI 性能回归检测的完整方案:专用硬件、ABAB 设计、变化点检测算法、报警与 bisect 流程。对想在 CI 中落地性能门禁的团队,这是最完整的参考实现(另见 PostgreSQL 社区的 buildfarm 与 CFbot 性能跟踪实践)。
- k6、Gatling、vegeta 的官方文档与负载测试模式指南(k6.io/docs 的 executors 与 scenarios 章节尤其系统:每种执行器的语义、开环 vs 闭环的选择、分布式压测)。现代负载测试工具的文档本身就是方法论教材——k6 文档对 arrival-rate 与 VU 模型的辨析、Gatling 对 open/closed injection 的图示,都是本章负载模型要点的工具化落地。