3、性能基准测试:基准测试方法论、常用基准测试工具、如何设计有效的基准测试用例。
聊到性能调优,有个问题我经常被问到:「老张,我的系统到底快不快?」
说实话,没有基准测试,你根本回答不了这个问题。你只能凭感觉说「好像还行」、「感觉有点慢」。但感觉这东西,在量化交易里是最不靠谱的。
基准测试,说白了就是给系统拍一张「体检照」。你得知道它现在什么水平,才能谈优化。不然你改了一堆代码,到底是变快了还是变慢了?没人知道。
基准测试方法论
我个人习惯把基准测试分成三类。每一类解决不同的问题,千万别混着用。
- 微基准测试(Microbenchmark):测单个函数或方法的性能。比如「一个订单簿插入操作要多少纳秒」。
- 宏基准测试(Macrobenchmark):测完整业务流程。比如「从行情接收到策略发出信号,整个链路耗时」。
- 负载测试(Load Test):测系统在压力下的表现。比如「每秒处理10万笔订单时,延迟和吞吐量如何」。
核心原则:控制变量法
每次只改一个参数。我见过太多人同时改了JVM参数、代码逻辑、硬件配置,然后说「系统快了20%」。你问他到底是哪个改动起了作用?他答不上来。
嗯,这里要注意一个坑:预热(Warm-up)。JVM有JIT编译,第一次跑和第十次跑,性能可能差10倍。我曾经在项目中吃过这个亏——第一次测试结果惨不忍睹,差点把刚写的代码全删了。后来发现是没做预热。
我的习惯做法:
- 先跑3轮预热,数据丢弃
- 再跑5轮正式测试,取中位数
- 不要取平均值,异常值会污染结果
常用基准测试工具
工具这东西,选对了一个顶十个。我这些年用下来,真正靠谱的就这几个:
| 工具 | 适用场景 | 我个人评价 |
|---|---|---|
| JMH (Java Microbenchmark Harness) | Java微基准测试 | 业界标准,没有之一。我所有Java性能测试都用它 |
| Google Benchmark | C++微基准测试 | Google出品,靠谱。但学习曲线略陡 |
| wrk / wrk2 | HTTP接口负载测试 | 轻量、快速。我压测REST API首选 |
| Locust | Python负载测试 | 写脚本方便,适合复杂场景 |
| sysbench | 数据库/系统基准测试 | 测MySQL、CPU、内存都行,老牌工具 |
避坑指南:我曾经用JMH测一个简单的字符串拼接,结果发现StringBuilder比String.concat快了3倍。但后来发现,这个优化在整体系统中只占0.1%的耗时。你想想看,为了这0.1%去重构代码,值吗?
所以我的建议是:先做宏基准测试找到瓶颈,再对瓶颈做微基准测试。别上来就微优化。
如何设计有效的基准测试用例
设计用例这件事,我踩过的坑比走过的路还多。给你几个实战经验:
1. 场景要真实
别用「Hello World」级别的测试。你的系统在真实环境中处理的是什么样的数据?就用什么样的数据来测。
举个例子:测订单簿匹配性能,别只用10个订单。真实场景下可能有10万个订单在排队。我见过有人用100个订单测出「性能优异」,结果上线第一天就崩了。
2. 数据要有代表性
行情数据有高峰有低谷。你只测低峰期的性能,那高峰期怎么办?
我的做法是:从生产环境抓一段真实数据,包含极端行情(比如闪电暴跌、开盘瞬间)。然后用这段数据回放测试。
3. 边界条件要覆盖
空队列、满队列、单线程、多线程、缓存命中、缓存未命中……这些边界条件都要测。
我曾经在项目中遇到一个bug:当订单簿为空时,查询最优买卖价的函数返回了错误值。基准测试时没测这个场景,结果上线后第一个空订单簿就把系统搞崩了。
4. 统计指标要全面
别只看平均延迟。平均延迟会骗人。
我习惯看这几个指标:
- P50(中位数延迟)
- P99(99分位延迟)
- P999(99.9分位延迟)
- 吞吐量(TPS/QPS)
- 最大延迟(最坏情况)
一个真实案例:
有一次我优化套利系统的信号生成模块。平均延迟从500微秒降到了200微秒,看起来很棒对吧?但一看P99,从1毫秒飙到了10毫秒。原来优化引入了偶发的GC停顿。如果只看平均值,这个优化就是「成功的」。但实际效果是灾难性的——那10毫秒的尾部延迟,让系统错过了好几次套利机会。
基准测试的完整流程
我画了一张图,把整个流程串起来。你照着这个做,基本不会出大问题。
一个完整的JMH示例
说了这么多理论,来点实际的。这是我写套利系统时常用的JMH测试模板:
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ArbitrageBenchmark {
private OrderBook orderBook;
private List<Order> testOrders;
@Setup
public void setup() {
orderBook = new OrderBook();
testOrders = generateRealisticOrders(10000);
// 预热:先跑一遍,让JIT编译
for (Order order : testOrders) {
orderBook.addOrder(order);
}
}
@Benchmark
public void testOrderInsertion() {
Order order = new Order("BTC-USDT", Side.BUY, 50000.0, 1.0);
orderBook.addOrder(order);
}
@Benchmark
public void testBestPriceQuery() {
orderBook.getBestBid();
orderBook.getBestAsk();
}
public static void main(String[] args) throws Exception {
Options opt = new OptionsBuilder()
.include(ArbitrageBenchmark.class.getSimpleName())
.warmupIterations(3)
.measurementIterations(5)
.forks(1)
.build();
new Runner(opt).run();
}
}
一个小技巧:JMH的@Setup注解里,我习惯把预热逻辑也放进去。这样每次测试前数据状态都是一致的,不会出现「第一次测试数据少,后面测试数据多」的问题。
基准测试这件事,说白了就是「先测量,后优化」。没有数据支撑的优化,都是瞎折腾。我见过太多团队,花了两周优化一个函数,结果发现这个函数在整个链路里只占1%的耗时。
嗯,记住一句话:不要优化你不知道的东西。基准测试就是让你「知道」的工具。
公众号:蓝海资料掘金营,微信deep3321