JMH 使用实践

JMH

jmh 是java 的微测试工具

运行方式

官方文档 建议用官方的 mvn archetypeArtifactIdb, 他内部会把 你的代码 以及 依赖 打成 一个 jar包(默认打的包时 benchmarks.jar)运行。

这种方式如果遇到类似 依赖 spring 的项目需要注意在 shade plugin 里面添加上 spring 的schmea

1
2
3
4
5
6
7
8
9
10
11
<configuration>
<finalName>${uberjar.name}</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.handlers</resource>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/spring.schemas</resource>
</transformer>
</transformers>
</configuration>

不过也可以自己写main 函数运行

1
2
3
4
5
6
7
8
9
10
11

public class MyBenchmark {
// ...
}

public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}

配置

1
2
3
4
// 配置预热, 几轮,每轮实践
@Warmup(iterations = 1, time = 10, timeUnit = TimeUnit.SECONDS)
// 配置测试, 几轮,每轮实践
@Measurement(iterations = 1, time = 20, timeUnit = TimeUnit.SECONDS)

初始化环境

有的时候需要, 比如先启动spring, 搞到xx bean ,然后对 它做压测。

这里需要定义一个 state beachmark 启动时会帮你初始化好 state, 将它作为参数传入你的 beachmark 函数。

state 里面的field 可以设置默认参数 @Param("6"), 所有参数支持数组 @Param({"1", "4", "16", "32"})

1
2
3
4
5
6
7
8
9
// 表示全局共用一个state
@State(Scope.Benchmark)
public static class MyState {

@Param("6")
public int clientTimeoutSec;
@Param({"1", "4", "16", "32"})
public int ioThread;
}

注意, 党设置了多个参数, beachmark 运行时会根据不同参数组合, 将beachmark 跑多次

比如上面的代码会运行 1 x 4 = 4

beachmark + spring 例子

下面是一个配合spring 的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@State(Scope.Benchmark)
public static class MyState {

@Param("6")
public int clientTimeoutSec;
@Param({"1", "4", "16", "32"})
public int ioThread;
ApplicationContext applicationContext;
TDemoService.FutureIface iface;

// 运行 beachmark 的前后的 前
@Setup(Level.Trial)
public void doSetup() {
try {
applicationContext = new ClassPathXmlApplicationContext(
new String[] { "classpath*:application-context.xml" });
iface = applicationContext.getBean(TDemoService.FutureIface.class);
} catch (Throwable e) {
LOG.error(e.getMessage(), e);
throw e;
}
}

@TearDown(Level.Trial)
public void doTearDown() {
((ClassPathXmlApplicationContext) applicationContext).close();
}
}

@Benchmark
public void testMethod(MyState state) {
try {
state.iface.echo(state.clientTimeoutSec, state.ioThread);
} catch (Exception e) {
e.printStackTrace();
}
}

根据上面的配置, beachmark 的 clientTimeoutSec 和 ioThread 分别以 [6, 1], [6, 4], [6, 16], [6, 32] 4套配置 运行4次

并给出相应的结果。

参数化运行

有时需要最好可以通过配置跑出不同结果, 这种时候建议上面的 state 全部都配置默认值, 然后通过 喂参数的方式达到运行多套配置的结果。

比如

1
java -jar benchmarks.jar -p ioThread=1,2,3,4

运行时 命令行的 参数会 覆盖 代码内部的参数。

Profiler

支持跑 beachmark 时, 查看对 linux perf, 查看代码的堆栈cpu 消耗比。

列出所有支持的 profilers

1
java -jar beachmarks.jar -lprof

参数化的配置通过添加

1
-prof stack -prof perf

注意, 额外的 profiler 也会纳入 beachmark的结果列表, 建议 仅仅添加需要的。

通过类似如下方式将不要的 去除

1
grep -v ':·stack' benchmark_machine_result.csv

我只用了 stack, 可以观察具体哪些方法耗cpu 厉害

fork 和 thread

-f 表示同样的beachmark 跑几次。

-t 表示beachmark 的并发数量。

我是 -f 1 -t 40

结果输出

结果能打出 2份, 一份是 human-readable output 另一份是 machine-readable results

其实 前者的 最后 一段就是 后者。

后者支持的格式 Available formats: text, csv, scsv, json, latex

1
2
3
-o   设置 前者的 文件名字
-rf 设置 format
-rff 设置 后者的 文件名字

这里的输出有一个缺点,当使用了 -prof,她的 beachmark 也会被记录下来,可以通过

cat xxx.xx |grep -v ':·stack' > xxx.xx2 过滤掉

tips

建议的使用方式, 如果你有多套参数要测试的话, 建议不要一次性测试多个维度。

比如 ioThread 设置为 {1,4,16,32}, connCount 设置为 {1,10,100,1000}

这样的话 为了测试各种情况, 她的测试次数将是 一个笛卡尔集 4 * 4 = 16 次

建议测试时,一个一个维度测试, 代码里写上默认配置,然后 通过 配置 -p 设置参数组 覆盖。

reference

http://hg.openjdk.java.net/code-tools/jmh/file/a128fd4a5901/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_35_Profilers.java

http://openjdk.java.net/projects/code-tools/jmh/

avatar

lelouchcr's blog