前文分享了几种性能测试中常用到的生成全局唯一标识的案例,虽然在文中我猜测了几种方案设计的性能,并根据自己的经验给出了适用的场景。
但对于一个性能测试工程师来讲,有真是测试数据才更有说服力。这让我想起来之前学过的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();
}
}
最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:【文末小卡片领取】
这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!