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 的方法,以及基于源码级示例(如LRUCacheBenchmark、RecordBatchIterationBenchmark)编写严谨基准的实战技巧。
一、为什么 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 的核心热点路径,例如:
- record:
RecordBatchIterationBenchmark(记录批次迭代)、CompressedRecordBatchValidationBenchmark、UncompressedRecordBatchValidationBenchmark - producer:
ProducerRequestBenchmark、RecordAccumulatorReadyBenchmark、RecordAccumulatorFlushBenchmark - common:
FetchRequestBenchmark、FetchResponseBenchmark、MetadataResponseBenchmark - partition / fetcher:
PartitionMakeFollowerBenchmark、ReplicaFetcherThreadBenchmark - streams / connect / metadata / storage:
StreamsStickyAssignorBenchmark、JsonConverterBenchmark、KRaftMetadataRequestBenchmark、ProducerStateManagerBench等
二、运行基准测试: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=flamegraphasync-profiler 2.0 支持同时进行 CPU、内存分配(alloc)与锁(lock)剖析,并输出 JFR 格式(同样注意分号转义):
./jmh-benchmarks/jmh.sh -prof async:libPath=/path/to/libasyncProfiler.so\;output=jfr\;alloc\;lock LRUCacheBenchmarkasync 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 | 依次执行clean与shadowJar,然后运行全部基准 |
如果没有显式指定基准模式,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 执行put与get,其返回值被 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),仅供参考