- 实战Alibaba Sentinel:深度解析微服务高并发流量治理
- 吴就业
- 2780字
- 2022-05-06 15:33:14
1.3.1 JMH基准测试
基准测试(Benchmark)是测量、评估软件性能指标的一种测试,用来对某个特定目标场景的某项性能指标进行定量的和可对比的测试。
JMH即Java Microbenchmark Harness,是Java用来做基准测试的一个工具。该工具由OpenJDK提供并维护,其测试结果的可信度高。
我们可以将JMH用于需要进行基准测试的项目中,以单元测试方式使用。
1. 添加依赖
在需要进行基准测试的项目中引入JMH的jar包,依赖配置如下。
注意:1.23版本是JMH目前最新的版本。
2. 使用注解方式
JMH提供了一系列注解,用于简化代码的编写。在运行时,注解配置被用于解析生成BenchmarkListEntry配置类实例。被@Benchmark注解注释的方法为基准测试方法,而注释在类上的注解或注释在类的字段上的注解,则是类中所有基准测试方法共用的配置。
1)@Benchmark
@Benchmark注解用于声明一个public方法为基准测试方法,@Benchmark注解的使用如下述代码所示。
2)@BenchmarkMode
使用JMH可以轻松地测试出某个接口的吞吐量、平均执行时间等指标的数据。
如果需要测试某个方法的平均耗时,那么可以使用@BenchmarkMode注解并指定基准测试的模式为AverageTime,代码如下。
常用的基准测试模式如下。
AverageTime:测量平均耗时。
Throughput:测量吞吐量。
3)@Measurement
@Measurement注解用于指定测量次数及每次测量的持续时间。测量次数越多且每次测量的持续时间越长,测试结果的可信度就越高。
@Measurement注解有如下3个配置项。
iterations:测量次数。
time与timeUnit:测量一次的持续时间,timeUnit用于指定时间单位。
@Measurement注解的使用如下述代码所示。
在本例中,我们配置总的测量次数为5,每次测量的持续时间为1秒。1秒内执行testFunction方法的次数是不固定的,由方法执行耗时和time决定。
每个线程实际执行基准测试方法的次数等于time除以基准测试方法单次执行的耗时。假设基准测试方法单次执行的耗时为1秒,并使用@Measurement注解指定iterations为100次、time为10秒,那么一次测量最多只能执行10(即10秒/1秒)次基准测试方法,而iterations为100次指的是测量100次。
注意:iterations指的是测量总次数,而不是执行基准测试方法的次数。
4)@Warmup
采用一定的预热次数可以提升测试结果的准确度,而使用@Warmup注解可以声明需要预热的次数及每次预热的持续时间。
@Warmup注解有如下3个配置项。
iterations:预热次数。
time与timeUnit:预热一次的持续时间,timeUnit用于指定时间单位。
@Warmup注解的使用如下述代码所示。
在本例中,我们配置总的预热次数为5,每次预热持续时间为1秒。
5)@OutputTimeUnit
@OutputTimeUnit注解用于指定输出方法执行耗时的单位。
如果方法执行耗时为毫秒级别,则为了便于观察结果,可以使用@OutputTimeUnit注解指定输出的耗时时间单位为毫秒,代码如下。
6)@Fork
@Fork注解用于指定Fork多少个子进程来执行同一基准测试方法。
如果不需要Fork多个子进程,则可以使用@Fork注解指定进程数为1,代码如下。
7)@Threads
@Threads注解用于指定使用多少个线程来执行基准测试方法。
如果使用@Threads注解指定线程数为2,则每次测量都会创建两个线程来执行基准测试方法,代码如下。
假设@Measurement注解指定time为1秒,则基准测试方法单次执行的耗时为1秒。若只使用单个线程测量,则一次测量将只会执行一次基准测试方法,而若使用10个线程测量,则一次测量将能执行10次基准测试方法。
@Threads注解可以用来模拟高并发,一般用于测量基准测试方法的吞吐量。
8)公共注解
如果需要在MyTestBenchmark类中创建两个基准测试方法——testFunction1和testFunction2,并且这两个方法分别调用不同的支付接口,用于对比两个接口的性能,那么可以将除@Benchmark注解外的其他注解都声明到类上,让两个基准测试方法都使用同样的配置,代码如下。
我们可以使用JMH对JSON序列化框架进行基准测试。以测量分别使用Gson、Jackson反序列化同一JSON字符串的平均耗时为例,编写基准测试用例,代码如下。
在本例中,使用@Threads注解声明创建两个线程来执行基准测试方法,使用@State注解指定gsonParser、jacksonParser这两个字段的共享域为Scope.Thread,即在不同线程中,gsonParser、jacksonParser这两个字段都是不同的实例。
以testGson方法为例,我们可以认为JMH会为每个线程克隆出一个gsonParser对象。如果在testGson方法中打印gsonParser对象的hashCode,就会发现相同线程打印的结果相同,不同线程打印的结果不同,代码如下。
执行testGson方法输出的结果如下。
9)@Param
使用@Param注解可以指定基准测试方法的执行参数,并且只能指定String类型的值。该参数值可以是一个数组,将在程序运行期间按给定顺序被遍历。
如果使用@Param注解指定了多个参数值,则JMH会为每个参数值执行一次基准测试。
例如,测试不同复杂度的JSON字符串使用Gson框架与使用Jackson框架解析的性能对比,代码如下。
测试结果输出如下。
3. 使用非注解方式
使用注解与不使用注解在本质上并没有区别,只是使用注解更加方便。在运行时,注解配置被用于解析生成BenchmarkListEntry配置类实例,而不使用注解最终也需要构造BenchmarkListEntry配置类实例。每一个基准测试方法对应一个BenchmarkListEntry配置类实例。
Options实例用于配置基准测试参数,Runner实例用于执行基准测试方法。首先使用OptionsBuilder方法构造一个Options实例,并指定基准测试方法及执行基准测试方法的参数,然后创建Runner实例并调用Runner实例的run方法,即可执行基准测试。
使用非注解方式实现上面的例子,代码如下。
OptionsBuilder类提供的方法如下。
• include:导入一个基准测试类,参数为类的简单名称,JMH默认会把include导入的类的每个public方法都当作基准测试方法。
• exclude:排除不需要参与基准测试的方法。
• forks:对应@Fork注解,指定Fork多少个子进程来执行同一基准测试方法。
• threads:对应@Threads注解,指定使用多少个线程来执行基准测试方法。
• timeUnit:对应@OutputTimeUnit注解,指定输出方法执行耗时的单位。
• warmupIterations:对应@Warmup注解,指定预热次数。
• warmupTime:对应@Warmup注解,指定每次预热的持续时间。
• measurementIterations:对应@Measurement注解,指定测量次数。
• measurementTime:对应@Measurement注解,指定每次测量的持续时间。
• mode:对应@BenchmarkMode注解,指定测量模式。
4. 打包成jar包放到服务器上执行
我们可以使用单元测试方式对JSON解析框架进行性能对比。若想要测试Web服务的某个接口性能,则需要对接口进行压测,而不能使用简单的单元测试方式进行测试。因此我们可以独立创建一个接口测试项目,将基准测试代码写在该项目中,然后将写好的基准测试项目打包成jar包放到Linux服务器上执行,这样测试结果会更加准确。
使用java命令即可运行一个基准测试应用,代码如下。
5. 在IDEA中执行
在IDEA中,我们可以编写一个单元测试方法,并在单元测试方法中创建一个Runner实例,然后调用Runner实例的run方法执行基准测试。但JMH不会扫描包,不会执行每个基准测试方法,这就需要我们通过配置项来告知JMH需要执行哪些基准测试方法,代码如下。
完整代码如下。
由于本例中的JsonBenchmark类已经使用了注解,因此Options实例只需要配置需要执行基准测试的类即可。
如果需要执行多个基准测试类,则可以多次调用include方法;如果需要将测试结果输出到文件中,则可以调用output方法配置文件路径,否则输出到控制台中。
6. 在IDEA中使用JMH Plugin插件执行
安装JMH Plugin插件:在IDEA中搜索JMH Plugin,安装后重启即可使用。
1)只执行单个基准测试方法
在方法名称所在行中,IDEA会有一个▶️执行符号,右击该符号并在弹出的快捷菜单中选择“运行”命令即可。
2)执行一个类中的所有基准测试方法
在类名所在行中,IDEA会有一个▶️执行符号,右击该符号并在弹出的快捷菜单中选择“运行”命令即可,该类下的所有被@Benchmark注解注释的方法都会被执行。
注意:如果写的是单元测试方法,则IDEA会提示选择执行单元测试还是基准测试。