前文分享了幾種性能測試中常用到的生成全局唯一標識的案例,雖然在文中我猜測了幾種方案設計的性能,並根據自己的經驗給出了適用的場景。
但對於一個性能測試工程師來講,有真是測試數據才更有說服力。這讓我想起來之前學過的Java微基準測試框架 JMH
,所以不妨一試。
JMH簡介
JMH (Java Microbenchmark Harness)是一個用於編寫和運行Java基準測試的工具。它被廣泛用於評估Java應用程序的性能,並幫助開發人員發現和優化性能瓶頸。
JMH的主要特點包括:
- 高可信度:JMH提供了多種機制來消除測試過程中的噪音和偏差,確保測試結果的可靠性。
- 易用性:JMH提供了豐富的註解和API,使編寫和運行基準測試變得相對簡單。
- 靈活性:JMH支持多種測試模式,如簡單的吞吐量測試、微基準測試以及更復雜的測試場景。
- 可擴展性:JMH允許用戶自定義測試環境,如GC策略、編譯器選項等,以滿足特定的性能評估需求。
- 廣泛應用:JMH被廣泛應用於Java生態系統中,包括JDK自身的性能優化、第三方開源庫的性能評估等。
JMH是Java開發者評估應用程序性能的強大工具,有助於提高Java應用程序的整體質量和性能。同樣地對於性能測試而言,也可以通過 JMH
測試評估一段代碼在實際執行當中的表現。
實測
除了 使用分佈式服務生成GUID
這個方案以外,其他四種方案(其中兩種是我自己常用的)均參與測試。原因是分佈式服務需要網絡交互,這個一聽就不高性能,還有我暫時沒條件測試這個。
下面有限展示實測結果,總結使用線程共享和線程獨享的方案性能均遠遠高於 UUID
和 雪花算法
。爲了省事兒以下測試均預熱2次,預熱批次大小2,測試迭代次數1次,迭代批次大小也是1次。配置如下:
.warmupIterations(2)//預熱次數
.warmupBatchSize(2)//預熱批次大小
.measurementIterations(1)//測試迭代次數
.measurementBatchSize(1)//測試批次大小
.build();
PS:JMH
貌似還不支持 Groovy
所以我用 Java
寫了這個用例。
下面是運行1個線程的測試結果:
UniqueNumberTest.exclusive thrpt 203.146 ops/us
UniqueNumberTest.share thrpt 99.860 ops/us
UniqueNumberTest.snow thrpt 4.096 ops/us
UniqueNumberTest.uuid thrpt 11.758 ops/us
下面是運行10個線程的測試結果:
Benchmark Mode Cnt Score Error Units
UniqueNumberTest.exclusive thrpt 1117.347 ops/us
UniqueNumberTest.share thrpt 670.141 ops/us
UniqueNumberTest.snow thrpt 10.925 ops/us
UniqueNumberTest.uuid thrpt 3.608 ops/us
PS:此時機器的性能基本跑滿了。
下面是40個線程的測試結果:
Benchmark Mode Cnt Score Error Units
UniqueNumberTest.exclusive thrpt 1110.273 ops/us
UniqueNumberTest.share thrpt 649.350 ops/us
UniqueNumberTest.snow thrpt 8.908 ops/us
UniqueNumberTest.uuid thrpt 4.205 ops/us
可以看出跟10個線程結果差不多。
本機配置12核心,以上的測試結果單位是微秒,把結果乘以100萬就是每秒的處理量,各位在使用不同方案時可以適當參考。
測試用例
下面是我的測試用例,測試結果我就不進行可視化了。
package com.funtest.jmh;
import com.funtester.utils.SnowflakeUtils;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
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 java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@BenchmarkMode(Mode.Throughput)
//@Warmup(Ω = 3, time = 2, timeUnit = TimeUnit.SECONDS)//預熱次數,含義是每個測試會跑多久
//@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)//測試迭代次數,含義是每個測試會跑多久
//@Threads(1)//測試線程數
//@Fork(2)//fork表示每個測試會fork出幾個進程,也就是說每個測試會跑幾次
@State(value = Scope.Thread)//默認爲Scope.Thread,含義是每個線程都會有一個實例
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class UniqueNumberTest {
SnowflakeUtils snowflakeUtils = new SnowflakeUtils(1, 1);
ThreadLocal<Integer> exclusive = ThreadLocal.withInitial(() -> 0);
AtomicInteger share = new AtomicInteger(0);
@Benchmark
public void uuid() {
UUID.randomUUID();
}
@Benchmark
public void snow() {
snowflakeUtils.nextId();
}
@Benchmark
public void exclusive(Blackhole blackhole) {
Integer i = exclusive.get();
i++;
blackhole.consume(i + "");
}
@Benchmark
public void share(Blackhole blackhole) {
blackhole.consume(share.incrementAndGet() + "");
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(UniqueNumberTest.class.getSimpleName())//測試類名
.result("long/result.json")//測試結果輸出到result.json文件
.resultFormat(ResultFormatType.JSON)//輸出格式
.forks(1)//fork表示每個測試會fork出幾個進程,也就是說每個測試會跑幾次
.threads(40)//測試線程數
.warmupIterations(2)//預熱次數
.warmupBatchSize(2)//預熱批次大小
.measurementIterations(1)//測試迭代次數
.measurementBatchSize(1)//測試批次大小
.build();
new Runner(options).run();
}
}