Kafka JMH 微基准测试模块完全指南:运行、剖析与编写正确的高性能基准
2026/9/10 11:12:23 网站建设 项目流程

Kafka JMH 微基准测试模块完全指南:运行、剖析与编写正确的高性能基准

【免费下载链接】KafkaApache Kafka - A distributed event streaming platform项目地址: https://gitcode.com/GitHub_Trending/kafka4/kafka

导读

本文以 Apache Kafka 仓库中的 jmh-benchmarks/README.md 为核心,系统讲解如何在 Kafka 项目中利用 OpenJDK JMH 框架编写和运行微基准测试(micro-benchmark)。你将掌握jmh.sh脚本的完整用法、async profiler 与 GC profiler 的正确接入方式、脱离 Gradle 直接运行基准 Jar 的方法,以及基于源码级示例(如LRUCacheBenchmarkRecordBatchIterationBenchmark)编写严谨基准的实战技巧。

一、为什么 Kafka 需要 JMH 基准测试模块

在 JVM 上编写正确的微基准测试非常困难,存在大量因编译器优化而导致的隐蔽陷阱——死代码消除、常量折叠、循环展开都可能让测量结果失真。JMH(Java Microbenchmark Harness)是 OpenJDK 推出的专用于运行和分析 Java(或其他 JVM 语言)微基准与宏基准的框架,它通过 fork 独立 JVM、预热(warmup)、黑洞消费(Blackhole)等机制,帮助开发者获得可信的性能数据。

Kafka 在仓库中专门维护了jmh-benchmarks模块,该模块在根目录的 settings.gradle 中被纳入 Gradle 多模块构建,并在根目录的 build.gradle 中声明使用com.gradleup.shadow插件来组装可执行的 uber-jar。模块内目前包含约 60 个基准类,覆盖了 Kafka 的核心热点路径,例如:

  • recordRecordBatchIterationBenchmark(记录批次迭代)、CompressedRecordBatchValidationBenchmarkUncompressedRecordBatchValidationBenchmark
  • producerProducerRequestBenchmarkRecordAccumulatorReadyBenchmarkRecordAccumulatorFlushBenchmark
  • commonFetchRequestBenchmarkFetchResponseBenchmarkMetadataResponseBenchmark
  • partition / fetcherPartitionMakeFollowerBenchmarkReplicaFetcherThreadBenchmark
  • streams / connect / metadata / storageStreamsStickyAssignorBenchmarkJsonConverterBenchmarkKRaftMetadataRequestBenchmarkProducerStateManagerBench

二、运行基准测试:jmh.sh 快速上手

直接在 Gradle 任务中传递 JMH 参数既繁琐又易错,因此模块提供了现成的脚本 jmh-benchmarks/jmh.sh。该脚本先执行gradlew :jmh-benchmarks:clean :jmh-benchmarks:shadowJar构建 uber-jar,随后通过java -jar直接启动 JMH 运行器并把所有剩余参数原样透传。

脚本默认行为是运行全部基准:

./jmh-benchmarks/jmh.sh

按名称或模式筛选基准

传入一个模式或类名即可只运行匹配的基准,例如只跑 LRU 缓存基准:

./jmh-benchmarks/jmh.sh LRUCacheBenchmark

先列出哪些基准匹配给定模式而不实际执行(-l是 JMH 的 list 参数):

./jmh-benchmarks/jmh.sh -l LRUCacheBenchmark

覆盖 fork、迭代与预热参数

运行指定测试,并将 fork 次数、测量迭代数和预热迭代数全部覆盖为2

./jmh-benchmarks/jmh.sh -f 2 -i 2 -wi 2 LRUCacheBenchmark

结合 profiler 运行

在 Linux 上同时启用 GC profiler 与 async profiler 并输出火焰图(分号需要转义,避免被 Shell 当作命令分隔符):

./jmh-benchmarks/jmh.sh -prof gc -prof async:libPath=/path/to/libasyncProfiler.so\;output=flamegraph LRUCacheBenchmark

三、用 async profiler 验证基准确实在测“该测的东西”

对微基准而言,检查 profiler 输出是一种良好实践:它可以验证基准是否代表了预期的应用行为、是否真的在测量目标代码。常见的反例包括在基准中误用了昂贵的 mock,或者把测试初始化代码意外包含进了被测量代码。

JMH 内置了对 async-profiler 的集成,Linux 下指定.so库路径即可:

./jmh-benchmarks/jmh.sh -prof async:libPath=/path/to/libasyncProfiler.so

输出火焰图(output=flamegraph参数中的分号已转义,防止被 Shell 解释为命令分隔符):

./jmh-benchmarks/jmh.sh -prof async:libPath=/path/to/libasyncProfiler.so\;output=flamegraph

async-profiler 2.0 支持同时进行 CPU、内存分配(alloc)与锁(lock)剖析,并输出 JFR 格式(同样注意分号转义):

./jmh-benchmarks/jmh.sh -prof async:libPath=/path/to/libasyncProfiler.so\;output=jfr\;alloc\;lock LRUCacheBenchmark

async profiler 有大量可配置参数,查看完整参数说明:

./jmh-benchmarks/jmh.sh -prof async:help

四、用 GC profiler 衡量分配率

运行基准时建议加上-prof gc来测量其内存分配速率:

./jmh-benchmarks/jmh.sh -prof gc

其中尤其值得关注norm类分配率指标——它统计的是每次操作(per operation)的分配量,而非每秒分配量。后者在代码变快时反而可能上升,造成“越快越差”的假象;norm指标可以规避这一误导。

五、脱离 Gradle 直接运行基准

构建完成后,基准 Jar 位于jmh-benchmarks/build/libs/下(命名形如kafka-jmh-benchmarks-*.jar),因此完全可以像运行任何可执行 Jar 一样脱离 Gradle 执行,例如:

java -jar <kafka-repo-dir>/jmh-benchmarks/build/libs/kafka-jmh-benchmarks-*.jar -f2 LRUCacheBenchmark

这一能力正是 jmh.sh 底层的执行方式,也便于在 CI 或性能回归脚本中复用。

六、Gradle 任务与 Shadow Jar 机制

JMH 通常期望以独立的 Maven 项目形态存在。jmh-benchmarks模块则通过 Gradle Shadow Jar 插件模拟这一行为:把基准代码与所需的 JMH 运行时类合并组装为单个 uber-jar。

模块提供两个核心 Gradle 任务(默认在 Kafka 仓库根目录用./gradlew执行):

任务作用
jmh-benchmarks:shadowJar创建运行基准所需的 uber jar
jmh-benchmarks:jmh依次执行cleanshadowJar,然后运行全部基准

如果没有显式指定基准模式,JMH 采用默认模式throughput(吞吐量)。此外,根目录 build.gradle 中针对 shadowJar 做了专门的 archiveClassifier 配置,避免与原始 jar 互相覆盖,并保证了kafka-jmh-benchmarks-*.jar的命名形态。

七、常用 JMH 选项速查

以下为模块文档列出的常用 JMH 命令行选项:

-e <regexp+> Benchmarks to exclude from the run. -f <int> How many times to fork a single benchmark. Use 0 to disable forking altogether. Warning: disabling forking may have detrimental impact on benchmark and infrastructure reliability, you might want to use different warmup mode instead. -i <int> Number of measurement iterations to do. Measurement iterations are counted towards the benchmark score. (default: 1 for SingleShotTime, and 5 for all other modes) -l List the benchmarks that match a filter, and exit. -lprof List profilers, and exit. -o <filename> Redirect human-readable output to a given file. -prof <profiler> Use profilers to collect additional benchmark data. Some profilers are not available on all JVMs and/or all OSes. Please see the list of available profilers with -lprof. -v <mode> Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA] -wi <int> Number of warmup iterations to do. Warmup iterations are not counted towards the benchmark score. (default: 0 for SingleShotTime, and 5 for all other modes)

查看全部选项可运行 JMH 时加-h标志。

八、编写基准测试:源码级范例拆解

8.1 最简单的入门范例:LRUCacheBenchmark

LRUCacheBenchmark.java 是模块内最简单的基准之一,完整展示了 JMH 的核心骨架:

  • @State(Scope.Thread):状态对象仅对单个线程可见(线程私有);
  • @OutputTimeUnit(TimeUnit.MILLISECONDS):结果以毫秒为单位输出;
  • @Setup(Level.Trial):在整轮基准开始前初始化 10,000 个不同的 key/value 以及容量为 100 的LRUCache
  • @Benchmark方法testCachePerformance():通过自增 counter 轮换使用不同 key 执行putget,其返回值被 JMH 自动消费,防止死代码消除;
  • main()方法内使用OptionsBuilder声明.include(LRUCacheBenchmark.class.getSimpleName()).forks(2)后交给Runner执行——这是 JMH 标准编程式启动入口。
@State(Scope.Thread) @OutputTimeUnit(TimeUnit.MILLISECONDS) public class LRUCacheBenchmark { private static final int DISTINCT_KEYS = 10_000; private LRUCache<String, String> lruCache; private long counter = 0; @Setup(Level.Trial) public void setUp() { // 预生成 DISTINCT_KEYS 个 key/value,创建容量 100 的 LRUCache lruCache = new LRUCache<>(100); } @Benchmark public String testCachePerformance() { counter++; int index = (int) (counter % DISTINCT_KEYS); String hashkey = keys[index]; lruCache.put(hashkey, values[index]); return lruCache.get(hashkey); } public static void main(String[] args) throws RunnerException { Options opt = new OptionsBuilder() .include(LRUCacheBenchmark.class.getSimpleName()) .forks(2) .build(); new Runner(opt).run(); } }

8.2 进阶注解组合:RecordBatchIterationBenchmark

RecordBatchIterationBenchmark.java 展示了更完整的生产级注解用法,是理解“如何把基准写严谨”的绝佳样本:

  • @State(Scope.Benchmark):状态在所有线程间共享;
  • @Fork(value = 1):仅 fork 一次;
  • @Warmup(iterations = 5)@Measurement(iterations = 15):5 轮预热、15 轮正式测量,且通过@Param(value = {"LZ4", "SNAPPY", "GZIP", "ZSTD", "NONE"})对五种压缩类型进行参数化扫描;
  • @OperationsPerInvocation(value = batchCount):声明单次调用内实际处理了batchCount个批次,保证 ops/s 统计口径准确;
  • @Fork(jvmArgsAppend = "-Xmx8g"):为特定基准单独放大堆内存;
  • 基准方法内部使用Blackhole.consume(...)消费每条记录,配合 try-with-resources 关闭迭代器,避免测试初始化代码与资源泄漏污染测量结果。

8.3 消除编译器优化干扰:ByteUtilsBenchmark

ByteUtilsBenchmark.java 用于对比 Kafka 中 VarInt/VarLong 编解码的多种实现(legacy、Protobuf 风格、Netty 风格、unrolled 展开写法),其中值得借鉴的关键技巧包括:

  • @CompilerControl(CompilerControl.Mode.DONT_INLINE):禁止 JIT 将被测方法内联,确保测量到真实方法体;
  • @State(Scope.Benchmark)嵌套类 +@Setup(Level.Invocation):每次调用前重建 ByteBuffer,避免迭代间状态残留;
  • 使用固定种子(random.setSeed(133713371337L))的SecureRandom生成可复现的随机数据集,并通过@Param({"1", "3", "5", "7"})控制不同字节长度的取值分布——其注释还说明 Kafka 实际场景中大多数整数只有 1~2 字节。

8.4 基准中的真实环境初始化:BenchmarkConfigUtils

需要启动真实组件(如 GroupCoordinator、存储层)的基准,可复用 BenchmarkConfigUtils.java 的createDummyBrokerConfig():它以Properties形式一次性配置 KRaft 角色(process.roles=broker,controller)、监听器、日志目录、副本因子、offset topic 分区数、网络/后台线程数等完整 broker 参数,为基准构造接近真实部署的测试环境。

九、结语

jmh-benchmarks模块是 Kafka 性能工程体系的重要组成部分:jmh.sh屏蔽了 Gradle 参数透传的繁琐,-prof async-prof gc帮助开发者验证测量目标与分配行为,而模块内近 60 个覆盖 producer、consumer、record、metadata、streams 等核心路径的基准类,既是对热点代码的性能守护,也是学习正确 JMH 写法的现成教材。当你需要为 Kafka 提交一个性能敏感的改动时,善用该模块进行 fork 隔离、预热与分配率验证,能让每一个性能结论都建立在可复现、可解释的测量之上。

【免费下载链接】KafkaApache Kafka - A distributed event streaming platform项目地址: https://gitcode.com/GitHub_Trending/kafka4/kafka

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询