【性能测试JMH】SpringBoot结合 JMH进行性能测试 调优

高性能问题


代码单元的性能测试、代码优化


按照软件工程的思想,要想完成一个项目,只要做好准备工作,编码的工作量是很小的,比如搭建一个复杂的web项目,直接分模块,比如Cfeng的预上线网站cfeng.net目前提供的几个服务就是不同的几个模块,借助JPA、Spring Boot就可以迅速完成项目的构建,个人项目麻烦的反而是前端的页面

在初期阶段,we可能为了完成任务,比如完成一个身份鉴定,可以使用多层if, 同时也可以选择使用switch,但是二者的性能和速度you 真的考虑过吗?

这里提几个ms的问题: 简单的代码的性能问题

if 和 switch 谁的性能更好?

FastJSON和 GSON谁性能更好?

程序使用Spring 框架 和不使用Spring 框架 哪个性能更好? 【虽然现在几乎不可能绕开Spring了,servlet,手动new】

HashMap 初始化的时候需要指定 初始的空间大小吗? 【安全不是这里讨论的】

HashMap 获取容器大小 需要更多的延时吗?

JDK的 炫技的Lambda表达式 是否需要消耗更多的性能?

MyBatis-plus 和 Spring-Data-JPA 哪个框架的性能更好?

Spring-Data-Redis和分布式的Rdission哪个效率高?

… 就各种比较性能【MinIO和FastDFS】… 怎么回答? 回答的思路?

性能测试代码全部放在GITEE的cfeng-test-demo

JMH java microbenchmark harness java单元性能测试

在编写项目的时候,作为决策者选择框架的时候就难免需要考虑性能问题,多个框架都可以解决问题,这里就面临选择,而编代码过程更会遇到更细节的问题,也会涉及到性能的比较 : 比如 基于STOMP的聊天服务,存储当前用户endpoint,是使用Set还是Map? 如何Choose? Set中使用泛型是Set《JSONObject》和Set《JavaBean》数据大小同时谁更快?

分别对两个单元使用JMH进行性能测试之后,得到的结果包含每s或者每ns的最大执行数, 得出谁的性能更好

JMH是OpenJDK开发的基准测试工具,【之前的Junit是测试结果的】,一般用于代码的性能调优,精度达到ns,适用于Java和其余基于JVM的language,是JDK9之后自带的(貌似还是需要),和web性能测试JMeter不同,JMH测试对象可以为任何方法,不仅限于REST API

<dependency>
	<groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-core</artifactId>
    <version>1.23</version>
</dependency>

<dependency>
	<groupId>org.openjdk.jmh</groupId>
    <artifactId>jmh-generator-annprocess</artifactId>
    <version>1.23</version>
</dependency>

JMH只要基于方法层面进行性能测试,类似与Junit单元测试,测试的目的不同,为代码优化过程;

JMH的使用场景:

  • 准确知道某方法执行的时间,执行时间和输入之间的相关性
  • 对比接口的不同实现在给定条件下的吞吐量【性能】
  • 查看多少百分比的请求在多少时间内完成

JMH使用

首先贴一段实践的代码

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

/**
 * @author Cfeng
 * @date 2022/9/1
 */

public class JMHExampleTest {

    /**
     * 这里基准函数
     */
    @Benchmark
    public void jmhMethod() {

    }

    public static void main(String[] args) throws RunnerException {
        Options opt = new OptionsBuilder()
                        .include(JMHExampleTest.class.getSimpleName())  //基准类
                        .forks(1)
                        .build();
        new Runner(opt).run();
    }
}

JMH编译期间生成基准代码,在基准列表中将待测性能的方法注册为基准数值,该待测方法应该是公开的且可以抛出异常; 抛出异常之后JMH的Runner就会执行错误,直接结束,进入下一个基准测试; 衡量的基准,将空函数作为基准进行参考【空函数没有内容,执行速度最快】

JMH 的运行依赖于org.openjdk.jmh.runner.options.Options的配置, 配置Options之后,就可以通过org.openjdk.jmh.runner.Runner进行运行

  • include: 运行时包含的基准类
  • forks: 指定方法重复执行的次数

执行后可以看到大量的迭代,大量的吞吐量,每隔函数的开销

# Warmup: 5 iterations, 10 s each
# Measurement: 5 iterations, 10 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Throughput, ops/time
# Benchmark: com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod

# Run progress: 0.00% complete, ETA 00:01:40
# Fork: 1 of 1
# Warmup Iteration   1: 2432867074.037 ops/s
# Warmup Iteration   2: 2560473580.920 ops/s
# Warmup Iteration   3: 2602717289.592 ops/s
# Warmup Iteration   4: 2585859810.812 ops/s
# Warmup Iteration   5: 2588578460.594 ops/s
Iteration   1: 2606056999.216 ops/s
Iteration   2: 2469027629.800 ops/s
Iteration   3: 2595788786.098 ops/s
Iteration   4: 2591829783.488 ops/s
Iteration   5: 2600675478.647 ops/s

Result "com.Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod":
  2572675735.450 ±(99.9%) 224052512.720 ops/s [Average]
  (min, avg, max) = (2469027629.800, 2572675735.450, 2606056999.216), stdev = 58185726.044
  CI (99.9%): [2348623222.730, 2796728248.170] (assumes normal distribution)

Benchmark                                              Mode  Cnt           Score           Error  Units
Jning.cfengtestdemo.jmhTest.JMHExampleTest.jmhMethod  thrpt    5  2572675735.450 ± 224052512.720  ops/s

ops 为Operations Per Second 每秒的操作数; 可以看到空函数的速度非常快, 可以看到空函数的执行为每s 执行25亿次

@BenchMarkMode 设置基准测试的模式 【方法或者类】

设置运行基准测试的模式,可以选择放在方法上面,只对该方法生效,在BenchMarkMode内部设置Mode的模式

  • Mode.Throughput : 吞吐量模式,获得单位时间的操作数量,连续运行@BenchMark的方法,计算所有的工作线程的总吞吐量
  • Mode.AverageTime: 平均时间模式, 获得每次操作的平均时间,计算所有工作线程的平均时间
  • Mode.SimpleTime: 时间采样模式, 对每一个操作函数的时间进行采样,连续运行@BenchMark的函数,随机抽取运行所需要的时间
  • Mode.SingleShotTime: 单次触发模式, 测试单次操作的时间,连续运行@BenchMark函数,只运行一次并计算时间: 该模式只是运行一次@BenchMark函数,所以需要预热, 如果基准数值小,使用SimpleTime模式采样
  • Mode.All : 无模式,采用所有的基准模式,效果最好

@OutPutTimeUnit 报告结果的默认时间单位【类、方法】

可以放在类或者方法上面,设置测试的结果的显示的时间的单位,可以是哦那个java.util.current.TimeUnit进行设置

@OutPutTimeUnit(TimeUnit.MILLSECOUNDS)

@Warmup 预热,设置具体的配置参数如次数,时间等

JVM进程启动时,类加载器将所需要的所有类加载入内存,Bootstrap Class 核心类库,比如JRE、lib等; Extension Class 由相关的ExtClassLoader加载, Application Class 由AppClassLoader负责加载

类加载过程完毕后,所有类会进入JVM cache中,但是其他与JVM启动无关类没有加载、懒加载,当应用的第一个请求到来(比如controller的一个处理器),会触发相关类第一次加载,这个过程比较耗时, 对于低延迟应用必须要避免

采用特定的策略处理加载逻辑,保证第一次请求的快速响应,称为JVM预热

设置具体的预热的参数

  • iterations: 预热的迭代次数
  • Time: 预热的时间
  • timeUnit: 预热的时间单位
  • batchSize: 每个操作的基准方法的调用次数 (batch 一批)

@Measurement 类似预热,但是设置的是测量时的

测量的参数和上面的预热的参数相同

@Fork 整体测试几次

就像之前的在main函数中设置Options中设置fork的参数,之前通过new Runner 配置参数进行run

@State 设置配置对象的作用域,定义线程之间的共享程度

可以设置测试状态对象的多线程的共享程度

  • Scope.Benchmark: 基准状态范围, 基准作用域: 相同类型的所有实例在所有工作线程之间共享; ---- Spring多为无状态单例Bean,可以直接所有的线程共享 此状态上面的对象的SetUp方法和TearDown方法都是一个工作线程执行,每个级别一次,没有其他线程可以操作状态对象
  • Scope.Group: 组状态范围、组作用域: 相同类型的所有实例在同一组中的所有的线程之间共享,每一个线程组都将提供自己的状态对象 【组内共享,组间隔离】 该状态对象上面的SetUp方法和TearDown由一个组线程执行
  • Scope.Thread: 线程状态范围,线程作用域: 相同类型的所有实例都不同(不是单例的),在同一个基准中注入了多个状态对象,此状态的SetUp和TearDown方法由单个工作线程独占执行

@Setup 线程执行前的配置函数、初始化

该注解只能在配置函数上面,Setup方法只由一个可以访问State对象的线程执行,一般就是一个特定的工作线程执行,如果状态共享,那么就可能由不同的线程执行(Thread 作用域)

@TearDown 测试后处理操作 【方法】

放置在方法上面进行测试后处理操作,一般测试后清理资源

@BenchMark 标记测试基准 【方法】

放在方法上面表明测试的基准

@OperationsPerInvocation 与基准进行多操作通信,运行JMH调整

这里就可以测试循环内部的代码

@BenchMark
@OperationsPerInvocation(10)
public void test() {
    for(int i = 0; i < 10; i ++) {
        //xxx
    }
}

SpringBoot中使用JMH

SpringBoot中使用的区别就是需要获取容器中的Bean,进行测试, 在测试中,使用@Setup进行初始化, 使用SpringApplication.run(XXXX) 获取容器,获取测试所需要的Bean

package com.Jning.cfengtestdemo.jmhTest;

import com.Jning.cfengtestdemo.DemoApplication;
import com.Jning.cfengtestdemo.controller.TestUserController;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author Cfeng
 * @date 2022/9/1
 */

@BenchmarkMode(Mode.AverageTime) //平均时间模式
@State(Scope.Benchmark) //使用的SpringBoot容器,都是无状态单例Bean,无安全问题,可以直接使用基准作用域BenchMark
@OutputTimeUnit(TimeUnit.NANOSECONDS) //这里是ns为单位
@Fork(1)  //整体平均执行1次
@Warmup(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //预热1s
@Measurement(iterations = 5,time = 1,timeUnit = TimeUnit.SECONDS) //测试也是1s、五遍
public class JMHExampleTest {

    //springBoot容器
    private ApplicationContext context;

    //待测试的TestUserController
    private TestUserController userController;

    /**
     * 初始化,获取springBoot容器,run即可,同时得到相关的测试对象
     */
    @Setup
    public void init() {
        //容器获取
        context = SpringApplication.run(DemoApplication.class);
        //获取对象
        userController = context.getBean(TestUserController.class);
    }

    @Benchmark
    public void getUserList() {
        List userList =  userController.queryAll();
    }


    @Benchmark
    public void testIncrement() {
        context.getBeanDefinitionCount();
    }

    /**
     * 测试的后处理操作,关闭容器,资源清理
     */
    @TearDown
    public void down() {
//        System.out.println("结束测试,后处理操作");
        //使用子类ConfigurableApplicationContext关闭
        ((ConfigurableApplicationContext)context).close();
    }


    public static void main(String[] args) throws RunnerException {
//        Options opt = new OptionsBuilder()
//                        .include(JMHExampleTest.class.getSimpleName())  //基准类
//                        .forks(1) //重复执行的次数
//                        .build();

        //使用注解之后只需要配置一下include即可,fork和warmup、measurement都是注解
        Options opt = new OptionsBuilder()
                        .include(".*" + JMHExampleTest.class.getSimpleName() + ".*")
                        .build();
        new Runner(opt).run();
    }
}

多使用注解代替在Options中进行配置

Benchmark                                                 Mode  Cnt       Score        Error  Units
Jning.cfengtestdemo.jmhTest.JMHExampleTest.getUserList    avgt    5  621247.176 ± 109697.833  ns/op
Jning.cfengtestdemo.jmhTest.JMHExampleTest.testIncrement  avgt    5       1.860 ±      0.237  ns/op

这里可以看到测试两个方法,第一个是controller接口的获取所有的用户, 第二个为获取容器bean的数量,第一个每次操作需要621247ns 相比getCount来说消耗庞大, 这是因为方法1需要进行数据库查询,需要进行数据库连接,2不需要

同理可以使用StringBuilder和StringBuffer,StringBuilder运行的速度更快【Buffer更安全】

其他的性能问题也可以进行比较得出,比如

Lambda表达式,一般情况下增强for循环比Stream更快, 但是预热后Lambda、stream可能更快

JMH建议在打包之后再使用,利用jar进行命令行方式的启动测试,因为IDEA本身也会消耗资源

界面化和日志输出都很消耗性能, 比如空函数和System.out.print(XXX)就差距巨大,在高并发的操作时就避免输出日志

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值