本文转载自微信公众号「稀饭下雪」,作者帅气的小饭饭 。转载本文请联系稀饭下雪公众号。
「主管小肥肥:」 最近我们这边引入了mongodb,不过没有实际上测试过性能如何,只是听说读写比mysql快,你今天没有什么排期,测试一下,然后今天内给我个答案吧
「小饭饭:」 好的,接下来就是测试性能的一天了。
到了这里,可能大部分人的第一想法应该是直接用这种方式:
- public void test() {
- long start = System.currentTimeMillis();
- // 执行逻辑
- long end = System.currentTimeMillis();
- System.out.println(end - start);
- }
no,我这次使用的是JMH
无论出自何种原因需要进行性能评估,量化指标总是必要的,那么如何量化呢?
这就需要我们的主角 JMH 登场了!
先给你们看个效果图
性能对比图
什么是JMH
JMH(Java Microbenchmark Harness)是用于代码微基准测试的工具套件,主要是基于方法层面的基准测试,精度可以达到纳秒级。
该工具是由 Oracle 内部实现 JIT 的大牛们编写的,他们应该比任何人都了解 JIT 以及 JVM 对于基准测试的影响。
当你定位到热点方法,希望进一步优化方法性能的时候,就可以使用 JMH 对优化的结果进行量化的分析。
JMH 比较典型的应用场景如下:
- 想准确地知道某个方法需要执行多长时间,以及执行时间和输入之间的相关性
- 对比接口不同实现在给定条件下的吞吐量
- 查看多少百分比的请求在多长时间内完成
下面我们以mongodb、hibernate、jdbc数据加载性能对比为例,使用 JMH 做基准测试。
怎么做JMH基准测试?
- 加入依赖
因为 JMH 是 JDK9 自带的,如果是 JDK9 之前的版本需要加入如下依赖:
- <dependency>
- <groupId>org.openjdk.jmh</groupId>
- <artifactId>jmh-core</artifactId>
- <version>1.29</version>
- </dependency>
- <dependency>
- <groupId>org.openjdk.jmh</groupId>
- <artifactId>jmh-generator-annprocess</artifactId>
- <version>1.29</version>
- </dependency>
- 编写基准测试
接下来,创建一个 JMH 测试类,具体代码如下所示:
- @BenchmarkMode({Mode.AverageTime})
- @Warmup(iterations = 1, time = 5)
- @Measurement(iterations = 3, time = 5)
- @Threads(1)
- @Fork(1)
- @OutputTimeUnit(TimeUnit.MILLISECONDS)
- @State(Scope.Benchmark)
- public class ReadBenchMarks {
- @Benchmark
- public void loadMongoTemplate(){
- // mongoTemplate数据加载
- }
- @Benchmark
- public void loadMongoDriver(){
- // mongoDriver数据加载
- }
- @Benchmark
- public void loadHibernate(){
- // hibernate数据加载
- }
- @Benchmark
- public void loadJdbc(){
- // jdbc数据加载
- }
- public static void main(String[] args) throws RunnerException {
- Options options = new OptionsBuilder()
- .include(ReadBenchMarks.class.getSimpleName())
- .output("db.log")
- .build();
- new Runner(options).run();
- }
- }
「核心关注点:」
类上加了注解
- 需要测试的方法用 @Benchmark 注解标识
- 启动的方式
这些注解的具体含义将在下面介绍。
大家有兴趣可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
- 执行基准测试
准备工作做好了,接下来,运行代码,等待片刻,测试结果就出来了
- # JMH version: 1.29
- # VM version: JDK 1.8.0_251, Java HotSpot(TM) Client VM, 25.251-b08
- # VM invoker: C:\soft\Java\jdk1.8.0_251\jre\bin\java.exe
- # VM options: -javaagent:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\lib\idea_rt.jar=53895:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\bin -Dfile.encoding=UTF-8
- # Blackhole mode: full + dont-inline hint
- # Warmup: 2 iterations, 5 s each
- # Measurement: 10 iterations, 5 s each
- # Timeout: 10 min per iteration
- # Threads: 1 thread, will synchronize iterations
- # Benchmark mode: Average time, time/op
- # Benchmark: com.db.jmh.write.WriteBenchMarks.writeHibernate
- # Parameters: (info = 10031,1,5)
- # Run progress: 0.00% complete, ETA 00:06:00
- # Fork: 1 of 1
- # Warmup Iteration 1: 7.743 ms/op
- # Warmup Iteration 2: 9.433 ms/op
- Iteration 1: 7.854 ms/op
- Iteration 2: 8.638 ms/op
- Iteration 3: 8.579 ms/op
- Iteration 4: 8.213 ms/op
- Iteration 5: 8.843 ms/op
- Iteration 6: 9.178 ms/op
- Iteration 7: 7.739 ms/op
- Iteration 8: 9.608 ms/op
- Iteration 9: 10.152 ms/op
- Iteration 10: 9.461 ms/op
- Result "com.db.jmh.write.WriteBenchMarks.writeHibernate":
- 8.827 ±(99.9%) 1.182 ms/op [Average]
- (min, avg, max) = (7.739, 8.827, 10.152), stdev = 0.782
- CI (99.9%): [7.645, 10.008] (assumes normal distribution)
- # Run complete. Total time: 00:06:38
- REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
- why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
- experiments, perform baseline and negative tests that provide experimental control, make sure
- the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
- Do not assume the numbers tell you what you want them to tell.
- Benchmark (info) Mode Cnt Score Error Units
- WriteBenchMarks.writeHibernate 10031,1,5 avgt 10 8.827 ± 1.182 ms/op
- WriteBenchMarks.writeHibernate 10032,5,6 avgt 10 8.783 ± 1.478 ms/op
- WriteBenchMarks.writeHibernate 10033,5,20 avgt 10 12.574 ± 0.928 ms/op
- WriteBenchMarks.writeMongo 10031,1,5 avgt 10 5.057 ± 0.358 ms/op
- WriteBenchMarks.writeMongo 10032,5,6 avgt 10 7.392 ± 0.651 ms/op
- WriteBenchMarks.writeMongo 10033,5,20 avgt 10 12.590 ± 0.795 ms/op
下面对结果做下简单说明:
- # JMH version: 1.29
- # VM version: JDK 1.8.0_251, Java HotSpot(TM) Client VM, 25.251-b08
- # VM invoker: C:\soft\Java\jdk1.8.0_251\jre\bin\java.exe
- # VM options: -javaagent:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\lib\idea_rt.jar=53895:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\bin -Dfile.encoding=UTF-8
- # Blackhole mode: full + dont-inline hint
- # Warmup: 2 iterations, 5 s each
- # Measurement: 10 iterations, 5 s each
- # Timeout: 10 min per iteration
- # Threads: 1 thread, will synchronize iterations
- # Benchmark mode: Average time, time/op
- # Benchmark: com.db.jmh.write.WriteBenchMarks.writeHibernate
- # Parameters: (info = 10031,1,5)
该部分为「测试的基本信息」,比如使用的 Java 路径,预热代码的迭代次数,测量代码的迭代次数,使用的线程数量,测试的统计单位等。
- # Warmup Iteration 1: 7.743 ms/op
- # Warmup Iteration 2: 9.433 ms/op
该部分为每一次热身中的性能指标,预热测试不会作为最终的统计结果。预热的目的是「让 JVM 对被测代码进行足够多的优化」,比如,在预热后,被测代码应该得到了充分的 JIT 编译和优化。
- Iteration 1: 7.854 ms/op
- Iteration 2: 8.638 ms/op
- Iteration 3: 8.579 ms/op
- Iteration 4: 8.213 ms/op
- Iteration 5: 8.843 ms/op
- Iteration 6: 9.178 ms/op
- Iteration 7: 7.739 ms/op
- Iteration 8: 9.608 ms/op
- Iteration 9: 10.152 ms/op
- Iteration 10: 9.461 ms/op
- Result "com.db.jmh.write.WriteBenchMarks.writeHibernate":
- 8.827 ±(99.9%) 1.182 ms/op [Average]
- (min, avg, max) = (7.739, 8.827, 10.152), stdev = 0.782
- CI (99.9%): [7.645, 10.008] (assumes normal distribution)
该部分显示测量迭代的情况,每一次迭代都显示了当前的执行速率,即一个操作所花费的时,在进行 10 次迭代后,进行统计。
最后的测试结果如下所示:
- Benchmark (info) Mode Cnt Score Error Units
- WriteBenchMarks.writeHibernate 10031,1,5 avgt 10 8.827 ± 1.182 ms/op
- WriteBenchMarks.writeHibernate 10032,5,6 avgt 10 8.783 ± 1.478 ms/op
- WriteBenchMarks.writeHibernate 10033,5,20 avgt 10 12.574 ± 0.928 ms/op
- WriteBenchMarks.writeMongo 10031,1,5 avgt 10 5.057 ± 0.358 ms/op
- WriteBenchMarks.writeMongo 10032,5,6 avgt 10 7.392 ± 0.651 ms/op
- WriteBenchMarks.writeMongo 10033,5,20 avgt 10 12.590 ± 0.795 ms/op
看这些数据也能看出个大概,不过我不大可能直接将这个数据扔给老大, 因此用了以下两个网站
- JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
- JMH Visualizer:https://jmh.morethan.io/
生成了一开始看到的那张图形化界面。
补充下,JMH 基础
为了能够更好地使用 JMH 的各项功能,下面对 JMH 的基本概念进行讲解:
- @BenchmarkMode
用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。
- Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
- AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
- SampleTime:随机取样,最后输出取样结果的分布
- SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
- All:上面的所有模式都执行一次
- @State
通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
- Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
- Scope.Group:同一个线程在同一个 group 里共享实例
- Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit
为统计结果的时间单位,可用于类或者方法注解
- @Warmup
预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
- iterations:预热的次数
- time:每次预热的时间
- timeUnit:时间的单位,默认秒
- batchSize:批处理大小,每次操作调用几次方法
- @Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup 相同。
- @Threads
每个进程中的测试线程,可用于类或者方法上。
- @Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
- @Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
在介绍完常用的注解后,让我们来看下 JMH 有哪些陷阱。
回答个疑问,为什么需要预热?
因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
如何将测试结果 可视化
其实很简单,将main函数改成
- public static void main(String[] args) throws RunnerException {
- Options opt = new OptionsBuilder()
- .include(WriteBenchMarks.class.getSimpleName())
- .result("db_read.json")
- .resultFormat(ResultFormatType.JSON).build();
- new Runner(opt).run();
- }
就可以了,再将生成的json格式文件扔进以下网站:
- JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
- JMH Visualizer:https://jmh.morethan.io/
就可以了啦。
「小饭饭:」 我测完啦,还生成了柱形图给你看看
「主管小肥肥:」 不错,mongodb的性能确实ok,你做的也不错,还以为你会用System.currentTimeMillis()这种low的手段呢,没想到用上了JMH,做的不错,快调薪了,必须给你加一笔。
原文链接:https://mp.weixin.qq.com/s/hTRa-eOSvSns0sm2P2BMVg