(八)JMH的詳細使用,附帶壓測dubbo案例+程式碼

醋酸菌HaC發表於2022-03-11

1、JMH簡介

JMHJava Microbenchmark Harness,是Java用來做基準測試的一個工具,該工具由OpenJDK提供並維護,測試結果可信度高。

相對於 Jmeter、ab ,它通過編寫程式碼的方式進行壓測,在特定場景下會更能評估某項效能。

本次通過使用JMH來壓測Dubbo的效能(官方也是使用JMH壓測)

2、使用

只需要引用兩個jar即可:

<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。

@State

只能用在類上,有三個取值:

Scope.Thread:預設的State,每個測試執行緒分配一個例項;
Scope.Benchmark:所有測試執行緒共享一個例項,用於測試有狀態例項在多執行緒共享下的效能;
Scope.Group:每個執行緒組共享一個例項;

@OutputTimeUnit

時間單位,如毫秒 TimeUnit.MILLISECONDS、秒 TimeUnit.SECONDS

@Benchmark

宣告一個public方法為基準測試方法。該類下的所有被@Benchmark註解的方法都會執行。

相當於類的main方法

@BenchmarkMode

指定測試某個介面的指標,如吞吐量、平均執行時間,一般我都是選擇 ALL

Mode有:

  • Throughput: 整體吞吐量,例如“1秒內可以執行多少次呼叫” (thrpt,參加第5點)

  • AverageTime: 呼叫的平均時間,例如“每次呼叫平均耗時xxx毫秒”。(avgt)

  • SampleTime: 隨機取樣,最後輸出取樣結果的分佈,例如“99%的呼叫在xxx毫秒以內,99.99%的呼叫在xxx毫秒以內”(simple)

  • SingleShotTime: 以上模式都是預設一次 iteration 是 1s,唯有 SingleShotTime 是隻執行一次。往往同時把 warmup 次數設為0,用於測試冷啟動時的效能。(ss)

@BenchmarkMode({Mode.Throughput,Mode.All})
public class StressTestProvider {

}

@Measurement

用於控制壓測的次數

//測量2次,每次測量的持續時間為20秒
@Measurement(iterations = 2, time = 20 , timeUnit = TimeUnit.SECONDS)

@Warmup

預熱,預熱可以避免首次因為一些其他因素,如CPU波動、類載入耗時這些情況的影響。

@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)

引數解釋同上。

@Fork

@Fork用於指定fork出多少個子程式來執行同一基準測試方法。

@Threads

@Threads註解用於指定使用多少個執行緒來執行基準測試方法,如果使用@Threads指定執行緒數為2,那麼每次測量都會建立兩個執行緒來執行基準測試方法。

3、執行

我這裡的例子是壓測dubbo,原始碼連結在文末

完整例子:

@BenchmarkMode({Mode.All})
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
//測量次數,每次測量的持續時間
@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.SECONDS)
@Threads(32)
@Fork(1)
@State(Scope.Benchmark)
@OutputTimeUnit(TimeUnit.SECONDS)
@Slf4j
public class StressTestProvider {

    private final AnnotationConfigApplicationContext annotationConfigApplicationContext;
    private final StressTestController stressTestController;

    public StressTestProvider() {
        annotationConfigApplicationContext = new AnnotationConfigApplicationContext(AnnotationConfig.class);
        annotationConfigApplicationContext.start();
        stressTestController = annotationConfigApplicationContext.getBean("stressTestController", StressTestController.class);
    }


    @TearDown
    public void close() throws IOException {
        annotationConfigApplicationContext.close();
    }

    @Benchmark
    public void string1k() {
        stressTestController.string1k();
    }

    @Benchmark
    public void string100k() {
        stressTestController.string100k();
    }

    public static void main(String[] args) throws RunnerException {

        log.info("測試開始");
        Options opt = new OptionsBuilder()
                .include(StressTestProvider.class.getSimpleName())
            //可以通過註解注入
//                .warmupIterations(3)
//                .warmupTime(TimeValue.seconds(10))
            //報告輸出
                .result("result.json")
            //報告格式
                .resultFormat(ResultFormatType.JSON).build();
        new Runner(opt).run();
    }
}

有兩種執行的方式,一般採用打成jar這種。

3.1、main方法執行

如上,只需要 配置Options,執行main方法即可,注意要使用 run模式啟動,不要使用debug模式啟動。

否則會報錯:

transport error 202: connect failed: Connection refused ERROR

3.2、打成jar執行

有時候需要放在伺服器上執行,就需要打成一個jar,需要使用單獨的jar打包外掛:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>2.2</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <finalName>jmh-demo</finalName>
                        <transformers>
                            <transformer
                                         implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <mainClass>org.openjdk.jmh.Main</mainClass>
                            </transformer>
                            <transformer
                                         implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

如果不想要這種打包方式,打成jar的時候一定要宣告main方法入口對應的類,也就是上面StressTestProvider

還有就是,因為我的是springboot專案,我測試了一下想同時打包springboot和 jmh:

但是執行 jhm-demo.jar 發現報錯:not match main class,還是老老實實通過 profile 節點打包吧。

打完包後,通過以下命令即可執行:

java -jar jmh-demo.jar  -rf json -rff result.json

-rf json 是輸出 json的格式

-rff /data/result.json 是輸出檔案位置和名稱

4、結果

執行後,會生成一個彙總結果:

Result "com.dubbo.benchmark.StressTestProvider.string1k":
  N = 3
  mean =      0.016 ±(99.9%) 0.022 s/op

  Histogram, s/op:
    [0.014, 0.014) = 0 
    [0.014, 0.015) = 0 
    [0.015, 0.015) = 0 
    [0.015, 0.015) = 1 
    [0.015, 0.015) = 1 
    [0.015, 0.016) = 0 
    [0.016, 0.016) = 0 
    [0.016, 0.016) = 0 
    [0.016, 0.016) = 0 
    [0.016, 0.017) = 0 
    [0.017, 0.017) = 0 
    [0.017, 0.017) = 0 
    [0.017, 0.017) = 1 
    [0.017, 0.018) = 0 
    [0.018, 0.018) = 0 
    [0.018, 0.018) = 0 

  Percentiles, s/op:
      p(0.0000) =      0.015 s/op
     p(50.0000) =      0.015 s/op
     p(90.0000) =      0.017 s/op
     p(95.0000) =      0.017 s/op
     p(99.0000) =      0.017 s/op
     p(99.9000) =      0.017 s/op
     p(99.9900) =      0.017 s/op
     p(99.9990) =      0.017 s/op
     p(99.9999) =      0.017 s/op
    p(100.0000) =      0.017 s/op

# 第36行
# Run complete. Total time: 00:05:12

Benchmark                                           Mode     Cnt     Score      Error  Units
StressTestProvider.string100k                      thrpt       3   759.794 ±   66.300  ops/s
StressTestProvider.string1k                        thrpt       3  6798.005 ± 6992.093  ops/s
StressTestProvider.string100k                       avgt       3     0.042 ±    0.002   s/op
StressTestProvider.string1k                         avgt       3     0.005 ±    0.012   s/op
StressTestProvider.string100k                     sample   22982     0.042 ±    0.001   s/op
StressTestProvider.string100k:string100k·p0.00    sample             0.017              s/op
StressTestProvider.string100k:string100k·p0.50    sample             0.041              s/op
StressTestProvider.string100k:string100k·p0.90    sample             0.048              s/op
StressTestProvider.string100k:string100k·p0.95    sample             0.050              s/op
StressTestProvider.string100k:string100k·p0.99    sample             0.058              s/op
StressTestProvider.string100k:string100k·p0.999   sample             0.075              s/op
StressTestProvider.string100k:string100k·p0.9999  sample             0.088              s/op
StressTestProvider.string100k:string100k·p1.00    sample             0.092              s/op

StressTestProvider.string1k                       sample  186906     0.005 ±    0.001   s/op
StressTestProvider.string1k:string1k·p0.00        sample             0.001              s/op
StressTestProvider.string1k:string1k·p0.50        sample             0.005              s/op
StressTestProvider.string1k:string1k·p0.90        sample             0.007              s/op
StressTestProvider.string1k:string1k·p0.95        sample             0.008              s/op
StressTestProvider.string1k:string1k·p0.99        sample             0.011              s/op
StressTestProvider.string1k:string1k·p0.999       sample             0.030              s/op
StressTestProvider.string1k:string1k·p0.9999      sample             0.035              s/op
StressTestProvider.string1k:string1k·p1.00        sample             0.038              s/op
StressTestProvider.string100k                         ss       3     0.030 ±    0.181   s/op
StressTestProvider.string1k                           ss       3     0.016 ±    0.022   s/op
     
Benchmark result is saved to result.json

結果分析

簡單分析一下:

只需要從第36行開始看,我這裡一共壓測了2個方法

  • StressTestProvider.string100k
  • StressTestProvider.string1k

Mode

這一列表示測試的名稱,也就是 @BenchmarkMode你選擇的測試型別,原始碼在此:

public enum Mode {
    /**
     * <p>Throughput: operations per unit of time.</p>
     */
    Throughput("thrpt", "Throughput, ops/time"),

    /**
     * <p>Average time: average time per per operation.</p>
     *
     */
    AverageTime("avgt", "Average time, time/op"),

    /**
     * <p>Sample time: samples the time for each operation.</p>
     *
     */
    SampleTime("sample", "Sampling time"),

    /**
     * <p>Single shot time: measures the time for a single operation.</p>
     *
     */
    SingleShotTime("ss", "Single shot invocation time"),

thrpt:吞吐量,也可以理解為tps、ops

avgt:每次請求的平均耗時

sample:請求樣本數量,這次壓測一共發了多少個請求

ss:除去冷啟動,一共執行了多少輪

Cnt、Score、Units

單位

Error

誤差

如果你配置了輸出檔案,比如我上面的 resul.json ,但是你開啟是看不懂的,可以藉助兩個網站把檔案上傳進行分析:

彙總:

以上對dubbo進行了分別傳輸1k和100k的資料壓測。

provider機器:

2核4g

CentOS release 6.4 (Final)
model name      : QEMU Virtual CPU version 2.5+
stepping        : 3
cpu MHz         : 2099.998
cache size      : 4096 KB

JVM:

jdk1.8
-server -Xmx2g -Xms2g -XX:+UseG1GC 

dubbo:

版本:2.7.3
序列化:hessian2
使用預設dubbo執行緒數

壓測引數:

32併發

結果:

1k 100k
TPS 6700 760
RTT 95% 8ms 95% 50ms
AVGTime/OP 5ms 42ms
OOM

對比了 jmeter、Apache-Benmark(ab)、jmh 這三個壓測工具,個人比較推薦使用jmh,原因有:

  • jmh壓測簡單,只需要引入依賴,宣告註解
  • 準確性高,目前大多數效能壓測都是使用jmh
  • 缺點就是程式碼入侵

靈感參考:


相關文章