JMH基准测试,看我怎么用它来测试mongodb的数据加载性能

700 阅读8分钟

不花时间的导读:这是新开的《技术扫盲》系列的第一篇文章,主要分享我日常遇见的觉得新奇的技术,后续会分享游戏外挂相关的技术,有兴趣的关注下。

主管小肥肥: 最近我们这边引入了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();
    }
}

核心关注点:

  • 类上加了注解

    image-20210707233133644

  • 需要测试的方法用 @Benchmark 注解标识

  • 启动的方式

image-20210707233152541

这些注解的具体含义将在下面介绍。

大家有兴趣可以看下官方提供的 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 基础

为了能够更好地使用 JMH 的各项功能,下面对 JMH 的基本概念进行讲解:

  • @BenchmarkMode

用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),还可以设置为 Mode.All,即全部执行一遍。

  1. Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
  2. AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
  3. SampleTime:随机取样,最后输出取样结果的分布
  4. SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
  5. All:上面的所有模式都执行一次
  • @State

通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:

  1. Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
  2. Scope.Group:同一个线程在同一个 group 里共享实例
  3. Scope.Thread:默认的 State,每个测试线程分配一个实例
  • @OutputTimeUnit

为统计结果的时间单位,可用于类或者方法注解

  • @Warmup

预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:

  1. iterations:预热的次数
  2. time:每次预热的时间
  3. timeUnit:时间的单位,默认秒
  4. 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格式文件扔进以下网站:

就可以了啦。

小饭饭: 我测完啦,还生成了柱形图给你看看

主管小肥肥: 不错,mongodb的性能确实ok,你做的也不错,还以为你会用System.currentTimeMillis()这种low的手段呢,没想到用上了JMH,做的不错,快调薪了,必须给你加一笔。

插眼=》 微信搜:稀饭下雪