Jmh基準測試,看我怎么用它來測試Mongodb的數(shù)據(jù)加載性能
本文轉載自微信公眾號「稀飯下雪」,作者帥氣的小飯飯 。轉載本文請聯(lián)系稀飯下雪公眾號。
「主管小肥肥:」 最近我們這邊引入了mongodb,不過沒有實際上測試過性能如何,只是聽說讀寫比mysql快,你今天沒有什么排期,測試一下,然后今天內(nèi)給我個答案吧
「小飯飯:」 好的,接下來就是測試性能的一天了。
到了這里,可能大部分人的第一想法應該是直接用這種方式:
- public void test() {
- long start = System.currentTimeMillis();
- // 執(zhí)行邏輯
- long end = System.currentTimeMillis();
- System.out.println(end - start);
- }
no,我這次使用的是JMH
無論出自何種原因需要進行性能評估,量化指標總是必要的,那么如何量化呢?
這就需要我們的主角 JMH 登場了!
先給你們看個效果圖
性能對比圖
什么是JMH
JMH(Java Microbenchmark Harness)是用于代碼微基準測試的工具套件,主要是基于方法層面的基準測試,精度可以達到納秒級。
該工具是由 Oracle 內(nèi)部實現(xiàn) JIT 的大牛們編寫的,他們應該比任何人都了解 JIT 以及 JVM 對于基準測試的影響。
當你定位到熱點方法,希望進一步優(yōu)化方法性能的時候,就可以使用 JMH 對優(yōu)化的結果進行量化的分析。
JMH 比較典型的應用場景如下:
- 想準確地知道某個方法需要執(zhí)行多長時間,以及執(zhí)行時間和輸入之間的相關性
- 對比接口不同實現(xiàn)在給定條件下的吞吐量
- 查看多少百分比的請求在多長時間內(nèi)完成
下面我們以mongodb、hibernate、jdbc數(shù)據(jù)加載性能對比為例,使用 JMH 做基準測試。
怎么做JMH基準測試?
- 加入依賴
因為 JMH 是 JDK9 自帶的,如果是 JDK9 之前的版本需要加入如下依賴:
- <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>
- 編寫基準測試
接下來,創(chuàng)建一個 JMH 測試類,具體代碼如下所示:
- @BenchmarkMode({Mode.AverageTime})
- @Warmup(iterations = 1, time = 5)
- @Measurement(iterations = 3, time = 5)
- @Threads(1)
- @Fork(1)
- @OutputTimeUnit(TimeUnit.MILLISECONDS)
- @State(Scope.Benchmark)
- public class ReadBenchMarks {
- @Benchmark
- public void loadMongoTemplate(){
- // mongoTemplate數(shù)據(jù)加載
- }
- @Benchmark
- public void loadMongoDriver(){
- // mongoDriver數(shù)據(jù)加載
- }
- @Benchmark
- public void loadHibernate(){
- // hibernate數(shù)據(jù)加載
- }
- @Benchmark
- public void loadJdbc(){
- // jdbc數(shù)據(jù)加載
- }
- public static void main(String[] args) throws RunnerException {
- Options options = new OptionsBuilder()
- .include(ReadBenchMarks.class.getSimpleName())
- .output("db.log")
- .build();
- new Runner(options).run();
- }
- }
「核心關注點:」
類上加了注解
- 需要測試的方法用 @Benchmark 注解標識
- 啟動的方式
這些注解的具體含義將在下面介紹。
大家有興趣可以看下官方提供的 jmh 示例 demo:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/
- 執(zhí)行基準測試
準備工作做好了,接下來,運行代碼,等待片刻,測試結果就出來了
- # JMH version: 1.29
- # VM version: JDK 1.8.0_251, Java HotSpot(TM) Client VM, 25.251-b08
- # VM invoker: C:\soft\Java\jdk1.8.0_251\jre\bin\java.exe
- # VM options: -javaagent:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\lib\idea_rt.jar=53895:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\bin -Dfile.encoding=UTF-8
- # Blackhole mode: full + dont-inline hint
- # Warmup: 2 iterations, 5 s each
- # Measurement: 10 iterations, 5 s each
- # Timeout: 10 min per iteration
- # Threads: 1 thread, will synchronize iterations
- # Benchmark mode: Average time, time/op
- # Benchmark: com.db.jmh.write.WriteBenchMarks.writeHibernate
- # Parameters: (info = 10031,1,5)
- # Run progress: 0.00% complete, ETA 00:06:00
- # Fork: 1 of 1
- # Warmup Iteration 1: 7.743 ms/op
- # Warmup Iteration 2: 9.433 ms/op
- Iteration 1: 7.854 ms/op
- Iteration 2: 8.638 ms/op
- Iteration 3: 8.579 ms/op
- Iteration 4: 8.213 ms/op
- Iteration 5: 8.843 ms/op
- Iteration 6: 9.178 ms/op
- Iteration 7: 7.739 ms/op
- Iteration 8: 9.608 ms/op
- Iteration 9: 10.152 ms/op
- Iteration 10: 9.461 ms/op
- Result "com.db.jmh.write.WriteBenchMarks.writeHibernate":
- 8.827 ±(99.9%) 1.182 ms/op [Average]
- (min, avg, max) = (7.739, 8.827, 10.152), stdev = 0.782
- CI (99.9%): [7.645, 10.008] (assumes normal distribution)
- # Run complete. Total time: 00:06:38
- 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 (info) Mode Cnt Score Error Units
- WriteBenchMarks.writeHibernate 10031,1,5 avgt 10 8.827 ± 1.182 ms/op
- WriteBenchMarks.writeHibernate 10032,5,6 avgt 10 8.783 ± 1.478 ms/op
- WriteBenchMarks.writeHibernate 10033,5,20 avgt 10 12.574 ± 0.928 ms/op
- WriteBenchMarks.writeMongo 10031,1,5 avgt 10 5.057 ± 0.358 ms/op
- WriteBenchMarks.writeMongo 10032,5,6 avgt 10 7.392 ± 0.651 ms/op
- WriteBenchMarks.writeMongo 10033,5,20 avgt 10 12.590 ± 0.795 ms/op
下面對結果做下簡單說明:
- # JMH version: 1.29
- # VM version: JDK 1.8.0_251, Java HotSpot(TM) Client VM, 25.251-b08
- # VM invoker: C:\soft\Java\jdk1.8.0_251\jre\bin\java.exe
- # VM options: -javaagent:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\lib\idea_rt.jar=53895:C:\soft\idea\IntelliJ IDEA Community Edition 2020.1.1\bin -Dfile.encoding=UTF-8
- # Blackhole mode: full + dont-inline hint
- # Warmup: 2 iterations, 5 s each
- # Measurement: 10 iterations, 5 s each
- # Timeout: 10 min per iteration
- # Threads: 1 thread, will synchronize iterations
- # Benchmark mode: Average time, time/op
- # Benchmark: com.db.jmh.write.WriteBenchMarks.writeHibernate
- # Parameters: (info = 10031,1,5)
該部分為「測試的基本信息」,比如使用的 Java 路徑,預熱代碼的迭代次數(shù),測量代碼的迭代次數(shù),使用的線程數(shù)量,測試的統(tǒng)計單位等。
- # Warmup Iteration 1: 7.743 ms/op
- # Warmup Iteration 2: 9.433 ms/op
該部分為每一次熱身中的性能指標,預熱測試不會作為最終的統(tǒng)計結果。預熱的目的是「讓 JVM 對被測代碼進行足夠多的優(yōu)化」,比如,在預熱后,被測代碼應該得到了充分的 JIT 編譯和優(yōu)化。
- Iteration 1: 7.854 ms/op
- Iteration 2: 8.638 ms/op
- Iteration 3: 8.579 ms/op
- Iteration 4: 8.213 ms/op
- Iteration 5: 8.843 ms/op
- Iteration 6: 9.178 ms/op
- Iteration 7: 7.739 ms/op
- Iteration 8: 9.608 ms/op
- Iteration 9: 10.152 ms/op
- Iteration 10: 9.461 ms/op
- Result "com.db.jmh.write.WriteBenchMarks.writeHibernate":
- 8.827 ±(99.9%) 1.182 ms/op [Average]
- (min, avg, max) = (7.739, 8.827, 10.152), stdev = 0.782
- CI (99.9%): [7.645, 10.008] (assumes normal distribution)
該部分顯示測量迭代的情況,每一次迭代都顯示了當前的執(zhí)行速率,即一個操作所花費的時,在進行 10 次迭代后,進行統(tǒng)計。
最后的測試結果如下所示:
- Benchmark (info) Mode Cnt Score Error Units
- WriteBenchMarks.writeHibernate 10031,1,5 avgt 10 8.827 ± 1.182 ms/op
- WriteBenchMarks.writeHibernate 10032,5,6 avgt 10 8.783 ± 1.478 ms/op
- WriteBenchMarks.writeHibernate 10033,5,20 avgt 10 12.574 ± 0.928 ms/op
- WriteBenchMarks.writeMongo 10031,1,5 avgt 10 5.057 ± 0.358 ms/op
- WriteBenchMarks.writeMongo 10032,5,6 avgt 10 7.392 ± 0.651 ms/op
- WriteBenchMarks.writeMongo 10033,5,20 avgt 10 12.590 ± 0.795 ms/op
看這些數(shù)據(jù)也能看出個大概,不過我不大可能直接將這個數(shù)據(jù)扔給老大, 因此用了以下兩個網(wǎng)站
- JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
- JMH Visualizer:https://jmh.morethan.io/
生成了一開始看到的那張圖形化界面。
補充下,JMH 基礎
為了能夠更好地使用 JMH 的各項功能,下面對 JMH 的基本概念進行講解:
- @BenchmarkMode
用來配置 Mode 選項,可用于類或者方法上,這個注解的 value 是一個數(shù)組,可以把幾種 Mode 集合在一起執(zhí)行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime}),還可以設置為 Mode.All,即全部執(zhí)行一遍。
- Throughput:整體吞吐量,每秒執(zhí)行了多少次調(diào)用,單位為 ops/time
- AverageTime:用的平均時間,每次操作的平均時間,單位為 time/op
- SampleTime:隨機取樣,最后輸出取樣結果的分布
- SingleShotTime:只運行一次,往往同時把 Warmup 次數(shù)設為 0,用于測試冷啟動時的性能
- All:上面的所有模式都執(zhí)行一次
- @State
通過 State 可以指定一個對象的作用范圍,JMH 根據(jù) scope 來進行實例化和共享操作。@State 可以被繼承使用,如果父類定義了該注解,子類則無需定義。由于 JMH 允許多線程同時執(zhí)行測試,不同的選項含義如下:
- Scope.Benchmark:所有測試線程共享一個實例,測試有狀態(tài)實例在多線程共享下的性能
- Scope.Group:同一個線程在同一個 group 里共享實例
- Scope.Thread:默認的 State,每個測試線程分配一個實例
@OutputTimeUnit
為統(tǒng)計結果的時間單位,可用于類或者方法注解
- @Warmup
預熱所需要配置的一些基本測試參數(shù),可用于類或者方法上。一般前幾次進行程序測試的時候都會比較慢,所以要讓程序進行幾輪預熱,保證測試的準確性。參數(shù)如下所示:
- iterations:預熱的次數(shù)
- time:每次預熱的時間
- timeUnit:時間的單位,默認秒
- batchSize:批處理大小,每次操作調(diào)用幾次方法
- @Measurement
實際調(diào)用方法所需要配置的一些基本測試參數(shù),可用于類或者方法上,參數(shù)和 @Warmup 相同。
- @Threads
每個進程中的測試線程,可用于類或者方法上。
- @Fork
進行 fork 的次數(shù),可用于類或者方法上。如果 fork 數(shù)是 2 的話,則 JMH 會 fork 出兩個進程來進行測試。
- @Param
指定某項參數(shù)的多種情況,特別適合用來測試一個函數(shù)在不同的參數(shù)輸入的情況下的性能,只能作用在字段上,使用該注解必須定義 @State 注解。
在介紹完常用的注解后,讓我們來看下 JMH 有哪些陷阱。
回答個疑問,為什么需要預熱?
因為 JVM 的 JIT 機制的存在,如果某個函數(shù)被調(diào)用多次之后,JVM 會嘗試將其編譯為機器碼,從而提高執(zhí)行速度,所以為了讓 benchmark 的結果更加接近真實情況就需要進行預熱。
如何將測試結果 可視化
其實很簡單,將main函數(shù)改成
- public static void main(String[] args) throws RunnerException {
- Options opt = new OptionsBuilder()
- .include(WriteBenchMarks.class.getSimpleName())
- .result("db_read.json")
- .resultFormat(ResultFormatType.JSON).build();
- new Runner(opt).run();
- }
就可以了,再將生成的json格式文件扔進以下網(wǎng)站:
- JMH Visual Chart:http://deepoove.com/jmh-visual-chart/
- JMH Visualizer:https://jmh.morethan.io/
就可以了啦。
「小飯飯:」 我測完啦,還生成了柱形圖給你看看
「主管小肥肥:」 不錯,mongodb的性能確實ok,你做的也不錯,還以為你會用System.currentTimeMillis()這種low的手段呢,沒想到用上了JMH,做的不錯,快調(diào)薪了,必須給你加一筆。
原文鏈接:https://mp.weixin.qq.com/s/hTRa-eOSvSns0sm2P2BMVg