(八)JMH的詳細使用,附帶壓測dubbo案例+程式碼
1、JMH簡介
JMH
即Java 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
- 缺點就是程式碼入侵
靈感參考:
- dubbo壓測的官方程式碼:https://github.com/apache/dubbo-benchmark
- 主流RPC框架壓測程式碼:https://github.com/hank-whu/rpc-benchmark
-
壓測dubbo的原始碼已上傳到github:https://github.com/DogerRain/dubbo-samples-test
-
Java資源分享:Java學習路線思維導圖+Java學習視訊+簡歷模板+Java電子書