ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

reactor-core 基准测试实战:JMH 任务使用指南(benchmarks 模块)

reactor-core 基准测试实战:JMH 任务使用指南(benchmarks 模块)
  • 后端
  • 异步编程

【免费下载链接】reactor-core

Non-Blocking Reactive Foundation for the JVM

项目地址:https://gitcode.com/gh_mirrors/re/reactor-core
点击查看免费下载

导读

本文以 benchmarks/README.md 为核心,系统讲解 reactor-core 仓库中benchmarks模块的 JMH(Java Microbenchmark Harness)基准测试任务用法,包括如何精确指定被测类、如何附加 profiler 分析 GC 与线程行为、如何切换报告格式、如何注入 JVM 参数,以及如何借助jmhBaseline与最新发布版本进行 A/B 对比。读完本文,你将能够在本地复现 reactor-core 的性能回归验证流程,并理解这些 Gradle 任务底层是如何拼装 JMH 命令行参数的。

一、benchmarks 模块与 JMH 基础设施概览

benchmarks是 reactor-core 仓库中的一个独立 Gradle 子项目(在 settings.gradle 中通过include 'benchmarks', ...注册),专门用于存放针对reactor-core及reactor-core-micrometer的 JMH 基准测试。它的 Gradle 配置位于 benchmarks/build.gradle,其核心依赖与任务关系如下:

  • 依赖org.openjdk.jmh:jmh-core(版本 1.37,见 gradle/libs.versions.toml),并通过annotationProcessor libs.jmh.generatorAnnprocess使用 JMH 注解处理器自动生成*_jmh运行类;
  • 使用compileOnly引入reactor-perfBaseline-core等基线依赖,目的是防止在编写 benchmark 时意外使用新版本 API(见 benchmarks/build.gradle 的注释 "Use the baseline to avoid using new APIs in the benchmarks");
  • 定义了三个关键任务:jmh、jmhBaseline与jmhProfilers,其中jmh和jmhBaseline都继承自自定义任务类 JmhExecTask(位于 buildSrc 中),jmhProfilers则直接以org.openjdk.jmh.Main -lprof方式列出所有可用 profiler(见 benchmarks/build.gradle)。

由于任务定义在benchmarks子项目内,从仓库根目录运行需写为./gradlew :benchmarks:jmh ...,而原文档中的../gradlew jmh ...则是约定在benchmarks/目录下执行的形式,二者等价。

二、精确控制被测 Benchmark:include、fullInclude 与 exclude

JMH 默认会运行所有@Benchmark方法,而 reactor-core 的基准测试往往数量较多、耗时较长。因此第一步通常是缩小范围。

2.1 按子串匹配:--include

原文档给出的第一种方式是按子串过滤,Gradle 会自动在传入值前后各加一个.*通配符:

../gradlew jmh --include="(BenchmarkPrimary|OtherBench)"

上述命令实际匹配的是类名(JMH 内部按 "类名.方法名" 形式的 benchmark 全名做正则匹配),等效于正则.*(BenchmarkPrimary|OtherBench).*,可以一次匹配多个含不同关键字的 benchmark。

2.2 自己写完整正则:--fullInclude

如果你需要更精细的控制,可以直接传入完整的 JMH 风格正则:

../gradlew jmh --fullInclude=.*MyBenchmark.*

2.3 反向排除:--exclude

虽然原文档未列出,但从 JmhExecTask.java 的源码可以看到任务还提供了exclude选项,它会被映射为 JMH 的-e参数:

../gradlew jmh --exclude=.*SomeSlowBench.*

三个选项在任务实现中的优先级关系(见 JmhExecTask.java)是:若设置了include则使用.*<include>.*;否则若设置了fullInclude则直接使用该正则;exclude独立生效,两者可组合使用。

三、附加 Profiler:定位 GC、线程与 JIT 瓶颈

JMH 本身只负责测量吞吐量或耗时,要回答“时间花在哪了”,需要挂载 profiler。

../gradlew jmh --profilers="gc,stack"

--profilers接受逗号分隔的多个 profiler 名称,底层实现会对每个名称依次追加一个-prof <name>参数(见 JmhExecTask.java)。

原文档重点推荐了四个常用 profiler:

Profiler用途
compJitCompilations,查看 JIT 编译情况,用于微调迭代次数
stack统计哪些方法耗时最多(热点方法分布)
gc打印垃圾回收统计信息(分配率、GC 次数与耗时)
hs_thr线程使用情况(HotSpot 线程状态统计)

要查看完整 profiler 清单,运行专门的列出任务:

../gradlew jmhProfilers

该任务即执行org.openjdk.jmh.Main -lprof(见 benchmarks/build.gradle),会打印诸如gc、stack、comp、hs_thr、async、perfasm、javaProfiler等全部可用选项,供你按需选用。

四、报告格式:从 JSON 切换到 CSV/TEXT

JMH 默认输出 JSON 格式报告(DEFAULT_FORMAT = "json",见 JmhExecTask.java),原文档允许切换为以下五种之一:

./gradlew jmh --format=csv

--format的合法取值为CSV、JSON、NONE、SCSV、TEXT:

  • CSV/SCSV:便于导入 Excel 或 pandas 做后处理;
  • JSON:默认,结构完整,适合程序化解析;
  • TEXT:人类可读的控制台式文本;
  • NONE:不生成机器可读报告。

在任务实现中,该参数同时决定结果文件路径:build/reports/<任务名>/result.<format>(见 JmhExecTask.java)。也就是说,用--format=csv跑jmh任务后,报告位于benchmarks/build/reports/jmh/result.csv,供持续集成或人工对比读取。

五、注入 JVM 参数

某些 benchmark 需要特定的系统属性或 JVM 开关,可通过--jvmArgs传入(注意参数按空格拆分,多个参数用空格分隔,见 JmhExecTask.java):

../gradlew jmh --jvmArgs="-Dtest.cluster=local"

仓库中的 benchmark 也大量依赖此类 JVM 开关。例如 AssemblyTraceBenchmark 通过@Fork(jvmArgsAppend = ...)在 fork 的 JVM 上分别设置-Dreactor.trace.operatorStacktrace=true/false来对比启用/关闭操作符堆栈跟踪时的开销,并追加-XX:-OmitStackTraceInFastThrow防止 JIT 优化掉异常堆栈。这类测试场景,配合--jvmArgs可以进一步叠加自定义属性。

需要说明的是,任务实现还会无条件追加两个堆内存参数(见 JmhExecTask.java):

-Xmx3072m -Xms3072m

即每个 fork 出的 JVM 固定使用 3GB 堆,避免因默认堆过小导致 GC 干扰测量结果。

六、验证模式:只检查 benchmark 能否运行

完整跑一轮基准测试往往需要数十分钟甚至更久。改动 benchmark 代码后想快速确认“能编译、能跑通、不出错”,用验证模式:

../gradlew jmh --verify=true

从源码看,verify模式会把 fork、预热迭代、测量迭代全部压到最小值(见 JmhExecTask.java):

  • -f 1:只 fork 一次;
  • -wi 1:仅 1 轮预热迭代;
  • -i 1:仅 1 轮测量迭代。

同时任务始终追加-foe true(fail-on-error,任何 benchmark 抛错立即失败)与-v NORMAL(输出级别),因此验证模式下任何异常都会直接导致构建失败,便于在 CI 或本地快速回归。

七、与最新发布版基线对比:jmhBaseline

性能优化的关键问题是:“我的改动相比上一个版本是变快还是变慢?”为此仓库提供了配套任务jmhBaseline,它用最新发布版本(而非当前工作区代码)运行同一批 benchmark:

../gradlew jmh jmhBaseline --include=MyBenchmark

在 benchmarks/build.gradle 中,jmh与jmhBaseline分别使用两组独立配置:

  • jmh的 classpath 为sourceSets.main.runtimeClasspath + configurations.current,current指向当前模块project(':reactor-core')与project(':reactor-core-micrometer')(见 benchmarks/build.gradle);
  • jmhBaseline使用configurations.baseline,即通过libs.reactor.perfBaseline.core解析到的发布版坐标,当前版本配置为baselinePerfCore = "3.8.7"(见 gradle/libs.versions.toml),并设置changing = true、缓存时间为 0 秒,确保总是拉取最新基线。

两个任务产生的报告分别落在build/reports/jmh/result.json与build/reports/jmhBaseline/result.json,对比两份 JSON 即可量化本次改动的性能影响。这也是 reactor-core 维护者在发布前验证性能回归的标准流程。

八、仓库内典型 Benchmark 速览

理解了任务用法之后,快速浏览仓库中已有的 benchmark 可以帮助你写出风格一致、结果可信的新用例:

  • FluxPublishBenchmark:使用@Param({"0", "10", "1000", "100000"})在四种数据规模下测量Flux.range().publish().autoConnect(availableProcessors())的多线程发布吞吐;@Threads(Threads.MAX)让线程数与 CPU 核数对齐,配合@Fork(value = 1)固定 fork 次数,可对比不同rangeSize的扩展性。
  • MonoAllBenchmark:用Flux.range(0, rangeSize).all(i -> i < Integer.MAX_VALUE).block()测量all()谓词短路运算在不同数据规模下的平均耗时(纳秒级输出)。
  • BoundedElasticBusyStructureBenchmark:同一被测场景下对比当前Schedulers.newBoundedElastic(...)与仓库保留的旧实现OldBoundedElasticScheduler(其任务队列基于PriorityBlockingQueue,见 OldBoundedElasticScheduler.java),用于验证调度器数据结构改造带来的收益。
  • FluxBufferTimeoutBenchmark:通过自定义JmhSubscriber模拟两种背压请求模式(一次请求Long.MAX_VALUE的unlimited与逐条request(1)的oneByOne),测量bufferTimeout在不同缓冲大小下的行为差异。
  • CheckpointBenchmark:对比checkpoint("desc", true)(完整堆栈跟踪)与轻量checkpoint("identifier")两种调试模式对吞吐量(Throughput)与采样耗时(SampleTime)的影响,直接指导生产环境调试开关的选型。
  • AssemblyTraceBenchmark:以递归方式构造不同深度(@Param({"10", "40", "80"}))的 operator 链,在开启/关闭reactor.trace.operatorStacktrace两种模式下测量装配期堆栈跟踪的开销。

这些用例普遍遵循同一套 JMH 规范:类级标注@BenchmarkMode、@Warmup、@Measurement、@Fork、@OutputTimeUnit与@State(Scope.Benchmark),方法级使用Blackhole消费结果防止死代码消除。新写 benchmark 时照搬这套模板即可。

九、执行与报告速查

需求命令(在benchmarks/下执行)说明
按子串运行../gradlew jmh --include="Benchmark"自动加.*通配
按正则运行../gradlew jmh --fullInclude=.*MyBenchmark.*完整 JMH 正则
附加 profiler../gradlew jmh --profilers="gc,stack"逗号分隔多个
列出全部 profiler../gradlew jmhProfilers等价jmh -lprof
切换报告格式./gradlew jmh --format=csvCSV/JSON/NONE/SCSV/TEXT
注入 JVM 参数../gradlew jmh --jvmArgs="-Dkey=value"空格分隔多个参数
快速验证../gradlew jmh --verify=true1 fork、1 预热、1 测量
对比基线../gradlew jmh jmhBaseline --include=MyBenchmark基线为最新发布版

几点补充提醒:

  1. 结果文件位置:jmh与jmhBaseline的报告分别写入build/reports/jmh/result.<format>与build/reports/jmhBaseline/result.<format>(见 JmhExecTask.java),对比时注意区分目录;
  2. 堆内存固定 3GB:任务会自动追加-Xmx3072m -Xms3072m,若需调整只能覆盖 fork 参数而非直接覆盖该内存设置;
  3. fail-on-error 恒定开启:-foe true保证任何 benchmark 异常都会使构建失败,适合接入 CI;
  4. 验证模式结果无参考意义:--verify=true只用于“能否运行”的检查,其数值因迭代过少不具备统计价值,正式数据应以默认完整模式产出为准。

通过组合上述选项,你可以在不改动任何仓库代码的前提下,完成从“精确跑某个 benchmark”到“当前分支 vs 最新发布版”的完整性能验证闭环,这正是 reactor-core 对每个 PR 进行性能守护的实践基础。

  • 后端
  • 异步编程

【免费下载链接】reactor-core

Non-Blocking Reactive Foundation for the JVM

项目地址:https://gitcode.com/gh_mirrors/re/reactor-core
点击查看免费下载
上一篇:vue-flow 图形截图实战:基于 html-to-image 实现流程图导出与下载
下一篇:用Python雪球接口pysnowball搭建个人投资数据监控:一份含踩坑记录的新手完整实战指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进