在spring boot中使用jmh进行性能测试

6,653 阅读4分钟

长期处于CRUD工作中的我突然有一天关心起自己项目的qps了.便用jmeter测试了访问量最大的接口,发现只有可怜的17r/s左右......看到网络上比比皆是的几百万qps,我无地自容啊.

于是就准备进行性能优化了,不过在优化前我们需要进行性能测试,这样才能知道优化的效果如何.比如我第一步就是用redis加了缓存,结果测试发现居然比不加之前还要慢???所以性能测试是非常重要的一环,而jmh就是非常适合的性能测试工具了.

准备工作

准备工作非常的简单,引入jmhmaven包就可以了.

        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-core</artifactId>
            <version>1.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.openjdk.jmh</groupId>
            <artifactId>jmh-generator-annprocess</artifactId>
            <version>1.22</version>
            <scope>provided</scope>
        </dependency>

第一个例子

package jmh;

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

/**
 * Benchmark
 *
 * @author wangpenglei
 * @since 2019/11/27 13:54
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class Benchmark {

    public static void main(String[] args) throws Exception {
        // 使用一个单独进程执行测试,执行5遍warmup,然后执行5遍测试
        Options opt = new OptionsBuilder().include(Benchmark.class.getSimpleName()).forks(1).warmupIterations(5)
                .measurementIterations(5).build();
        new Runner(opt).run();
    }

    @Setup
    public void init() {
   
    }

    @TearDown
    public void down() {

    }

    @org.openjdk.jmh.annotations.Benchmark
    public void test() {

    }

}

@BenchmarkMode

这个注解决定了测试模式,具体的内容网络上非常多,我这里采用的是计算平均运行时间

@OutputTimeUnit(TimeUnit.MILLISECONDS)

这个注解是最后输出结果时的单位.因为测试的是接口,所以我采用的是毫秒.如果是测试本地redis或者本地方法这种可以换更小的单位.

@State(Scope.Benchmark)

这个注解定义了给定类实例的可用范围,因为spring里的bean默认是单例,所以我这里采用的是运行相同测试的所有线程将共享实例。可以用来测试状态对象的多线程性能(或者仅标记该范围的基准).

@Setup @TearDown @Benchmark

非常简单的注解,平常测试都有的测试前初始化*测试后清理资源**测试方法*.

如何与spring共同使用

因为我们需要spring的环境才能测试容器里的bean,所以需要在初始化方法中手动创建一个.我查了一下资料没发现什么更好的方法,就先自己手动创建吧.

    private ConfigurableApplicationContext context;
    private AppGoodsController controller;

    @Setup
    public void init() {
        // 这里的WebApplication.class是项目里的spring boot启动类
        context = SpringApplication.run(WebApplication.class);
        // 获取需要测试的bean
        this.controller = context.getBean(AppGoodsController.class);
    }

    @TearDown
    public void down() {
        context.close();
    }

开始测试

写好测试方法后启动main方法就开始测试了,现在会报一些奇奇怪怪的错误,不过不影响结果我就没管了.运行完成后会输出结果,这时候可以对比下优化的效果.

Result "jmh.Benchmark.testGetGoodsList":
  65.969 ±(99.9%) 10.683 ms/op [Average]
  (min, avg, max) = (63.087, 65.969, 69.996), stdev = 2.774
  CI (99.9%): [55.286, 76.652] (assumes normal distribution)


# Run complete. Total time: 00:02:48

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                   Mode  Cnt   Score    Error  Units
Benchmark.testGetGoodsList  avgt    5  65.969 ± 10.683  ms/op

Process finished with exit code 0

开头的负优化

在文章开头我讲了一个负优化的例子,我用redis加了缓存后居然比直接数据库查询还要慢!其实原因很简单,我在本地电脑上测试,连接的redis却部署在服务器上.这样来回公网的网络延迟就已经很大了.不过数据库也是通过公网的,也不会比redis快才对.最后的原因是发现部署redis的服务器带宽只有1m也就是100kb/s,很容易就被占满了.最后优化是redis加缓存与使用内网连接redis.

优化结果

优化前速度:

Result "jmh.Benchmark.testGetGoodsList":
  102.419 ±(99.9%) 153.083 ms/op [Average]
  (min, avg, max) = (65.047, 102.419, 162.409), stdev = 39.755
  CI (99.9%): [≈ 0, 255.502] (assumes normal distribution)


# Run complete. Total time: 00:03:03

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                   Mode  Cnt    Score     Error  Units
Benchmark.testGetGoodsList  avgt    5  102.419 ± 153.083  ms/op

Process finished with exit code 0

优化后速度(为了模拟内网redis速度,连的是本地redis):

Result "jmh.Benchmark.testGetGoodsList":
  29.210 ±(99.9%) 2.947 ms/op [Average]
  (min, avg, max) = (28.479, 29.210, 30.380), stdev = 0.765
  CI (99.9%): [26.263, 32.157] (assumes normal distribution)


# Run complete. Total time: 00:02:49

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                   Mode  Cnt   Score   Error  Units
Benchmark.testGetGoodsList  avgt    5  29.210 ± 2.947  ms/op

Process finished with exit code 0

可以看到大约快了3.5倍,其实还有优化空间,全部数据库操作都通过redis缓存的话,大概1ms就处理完了.