RocksJava JMH 基准测试完全指南:编译、运行与内置基准集源码解析
2026/9/19 20:12:42 网站建设 项目流程

RocksJava JMH 基准测试完全指南:编译、运行与内置基准集源码解析

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

本文以 java/jmh/README.md 为骨架,系统讲解 RocksDB Java 绑定(RocksJava)内置 JMH(Java Microbenchmark Harness)微基准测试工程rocksdbjni-jmh的完整使用流程:如何编译出可运行的 benchmarks uber-jar、如何针对本地修改的 rocksdbjni SNAPSHOT 版本进行替换安装、如何运行并利用 JMH 运行时参数控制测试,同时结合仓库内四个基准类的源码,逐一解析 Get、Put、MultiGet、Comparator 基准的实现原理与可调参数。读完本文,你可以独立在本仓库中复现这些微基准测试,并学会编写自己的 RocksJava 基准用例。

一、rocksdbjni-jmh 是什么

rocksdbjni-jmh是 RocksDB 仓库中专门面向 RocksJava 功能的微基准测试工程,位于 java/jmh 目录。它基于 JMH(Java Microbenchmark Harness)编写,用于量化 RocksDB Java API 在典型读写场景下的性能表现。

与仓库中的 C++ 基准工具db_bench(见 tools/db_bench.cc)不同,jmh 工程关注的是JNI 边界上的 Java 层开销:同样的逻辑操作,通过 Java 的byte[]ByteBuffer(堆外 Direct Buffer)等不同数据载体传递给 C++ 层,性能存在可观测差异,这正是本工程要度量的核心问题。

该工程是一个独立的 Maven 项目,其 Maven 坐标定义在 java/jmh/pom.xml:

  • groupIdorg.rocksdb
  • artifactIdrocksdbjni-jmh
  • version1.0-SNAPSHOT
  • 项目描述:JMH Benchmarks for RocksDB Java API

工程依赖三个组件:rocksdbjni(RocksDB 的 Java 原生绑定库)、jmh-corejmh-generator-annprocess(JMH 核心与注解处理器,后者负责在编译期扫描@Benchmark方法生成基准运行骨架),依赖版本见 java/jmh/pom.xml。当前仓库的 pom.xml 中声明的rocksdbjni依赖版本为9.0.0,JMH 版本为1.22,Java 编译级别为 17(<project.build.source><project.build.target>均为 17,需要 JDK 17 及以上环境)。

二、编译:构建可运行的 benchmarks JAR

2.1 直接编译(使用 pom.xml 声明的 rocksdbjni 版本)

java/jmh目录下执行标准 Maven 打包命令即可:

$ mvn package

打包流程由 java/jmh/pom.xml 中的插件链驱动:

  • maven-compiler-plugin 3.8.1:以 Java 17 级别编译源码,同时触发 JMH 注解处理器(jmh-generator-annprocessprovided作用域),为每个标注了@Benchmark的类生成*_jmhType等运行时代理类;
  • maven-shade-plugin 3.2.1:在package阶段将所有依赖(rocksdbjni、JMH 运行时等)打入同一个 uber-jar,并把 Main-Class 设置为org.openjdk.jmh.Main,同时剔除签名文件META-INF/*.SFMETA-INF/*.DSAMETA-INF/*.RSA,避免出现 “Invalid signature file” 启动错误;
  • license-maven-plugin 3.0:校验所有源码文件携带 java/jmh/LICENSE-HEADER.txt 声明的许可证头(Apache 2.0 / GPLv2 双许可),不满足会直接构建失败。

shade 插件配置的finalName${project.artifactId}-${project.version}-${uberjar.name},其中uberjar.name属性值为benchmarks,因此最终生成的可执行 JAR 路径为:

target/rocksdbjni-jmh-1.0-SNAPSHOT-benchmarks.jar

2.2 测试本地修改:安装 SNAPSHOT 版本的 rocksdbjni

README 特别强调了一个关键约束:jmh 工程使用pom.xml<version>元素声明的 rocksdbjni 构建产物。如果你修改了 RocksDB 的 C++/JNI 代码(例如改动了 java/rocksjni 下的 JNI 实现),那么:

  1. 先用仓库的 Java 构建系统(java/Makefile 中的java目标等)构建出带 SNAPSHOT 版本号的本地 JNI jar;
  2. 将本地 jar 安装到本地 Maven 仓库;
  3. 更新java/jmh/pom.xmlrocksdbjni依赖的<version>,使其指向刚才安装的 SNAPSHOT 版本;
  4. 再执行mvn package,基准测试就会链接到你本地改动过的原生库。

以 README 中 OSX 平台、版本号8.11.0-SNAPSHOT为例,安装本地 jar 的命令如下:

$ mvn install:install-file -Dfile=./java/target/rocksdbjni-8.11.0-SNAPSHOT-osx.jar \ -DgroupId=org.rocksdb \ -DartifactId=rocksdbjni \ -Dversion=8.11.0-SNAPSHOT \ -Dpackaging=jar

说明:

  • -Dfile指向你构建出的 JNI jar 的完整路径。RocksJava 构建产物位于java/target/目录,jar 名称带平台后缀(如-osx-linux),具体以你本机构建产物为准;
  • -DgroupId-DartifactId必须与 pom.xml 中依赖声明一致(org.rocksdb:rocksdbjni),否则 Maven 无法解析;
  • -Dversion需与你准备写入 pom.xml 的版本号严格对应;
  • 安装完成后,修改 java/jmh/pom.xml 中<artifactId>rocksdbjni</artifactId>对应的<version>元素,再执行mvn package

需要说明的是,README 示例中的8.11.0只是当时写作时期的版本号。当前仓库 include/rocksdb/version.h 定义的 C++ 版本为 11.11.0,而 java/jmh/pom.xml 当前声明的rocksdbjni依赖为9.0.0。实际操作时,版本号请以你本地构建产物和 pom.xml 的当前内容为准,只需保证三步之间的版本号一致即可。

2.3 常见编译问题

  • Java 版本过低:pom 要求 Java 17,低于该版本会在 maven-compiler-plugin 编译阶段报错;
  • 许可证头缺失:新增的基准源码文件必须携带与仓库其他 Java 文件一致的许可证头,否则 license-maven-plugin 的strictCheck会中断构建;
  • rocksdbjni 依赖无法解析:如果 pom.xml 中版本指向的是尚未发布到 Maven 中央仓库的 SNAPSHOT,必须先用mvn install:install-file完成本地安装。

三、运行:启动 JMH 基准

编译完成后,通过java -jar直接启动:

$ java -jar target/rocksdbjni-jmh-1.0-SNAPSHOT-benchmarks.jar

不带任何参数时,JMH 会依次运行工程内所有@Benchmark方法(当前共有 4 个基准类,详见下一节),每项默认执行多轮 warmup 与 measurement 迭代,最终输出吞吐量/平均耗时等指标与置信区间。

README 特别提示:在命令后追加-help可以查看 JMH 的全部运行时选项

$ java -jar target/rocksdbjni-jmh-1.0-SNAPSHOT-benchmarks.jar -help

借助 JMH 运行时参数,你可以只跑指定基准、调整迭代策略或线程数。例如按类名或方法名过滤要执行的基准、控制 fork 数、预热迭代数与测量迭代数。工程内也提供了编程式构造运行参数的参考实现:在 MultiGetBenchmarks.java 的main方法中,通过OptionsBuilder设置了.include(...)(过滤基准类)、.forks(1)(fork 数)、.jvmArgs("-ea")(JVM 参数)、.warmupIterations(1).measurementIterations(2).param(...)(覆盖参数)与.output("jmh_output")(结果输出文件),展示了以Runner编程式驱动基准的完整写法。

四、内置基准集源码解析

工程共有 4 个基准类,均位于 java/jmh/src/main/java/org/rocksdb/jmh,全部使用@State(Scope.Benchmark)级别的共享状态(MultiGetBenchmarks的字段状态为Scope.Thread),并在@Setup(Level.Trial)中完成数据库打开与数据灌入、在@TearDown(Level.Trial)中完成资源释放与临时目录清理。

所有基准在 setup 阶段都遵循同一套模式:RocksDB.loadLibrary()加载原生库 →Files.createTempDirectory(...)创建临时数据库目录 → 构造DBOptionssetCreateIfMissing(true)setCreateMissingColumnFamilies(true))→ 按需创建列族描述符 →RocksDB.open(...)打开数据库 → 灌入测试数据 → 必要时flush将内存数据刷入 SST。teardown 阶段则逆序关闭ColumnFamilyHandledboptions等资源,并调用 FileUtils.java 的delete递归清理临时目录,保证多次运行互不干扰。

4.1 GetBenchmarks:单点读取

源码见 GetBenchmarks.java。它通过@Param声明了四组可变参数,组合展开后形成多维测试矩阵:

参数取值含义
columnFamilyTestTypeno_column_family/1_column_family/20_column_families/100_column_families列族规模:0、1、20、100 个列族
keyCount1000/100000每个列族写入的键数量
keySize12/64/128键的字节长度
valueSize64/1024/65536值的字节长度

Setup 阶段按列族数量创建名为cf1cf2……的列族(见 GetBenchmarks.java),向每个列族写入keyCount条形如key0..keyN的数据并 flush 到磁盘。读取时通过AtomicIntegercompareAndSet实现无锁的键轮转,避免并发线程读到相同键;多列族场景下用cfHandlesIdx轮转选择目标列族(不追求完美均匀分布,注释中明确说明 "doesn't ensure a perfect distribution, but it's ok")。

三个@Benchmark方法分别度量三种不同的取值 API 开销(见 GetBenchmarks.java):

  • get():最简调用db.get(cfHandle, keyArr),每次从 JNI 层返回新分配的byte[]
  • preallocatedGet()db.get(cfHandle, keyArr, valueArr)使用预分配的目标缓冲区接收结果,避免每次返回新数组的分配开销;
  • preallocatedByteBufferGet()db.get(cfHandle, readOptions, keyBuf, valueBuf)使用堆外ByteBufferByteBuffer.allocateDirect)读写,度量 Direct Buffer 路径下的 JNI 数据传递成本。方法内保留了一段被注释掉的正确性校验代码,可作为测试基准正确性的参考。

4.2 PutBenchmarks:写入路径

源码见 PutBenchmarks.java。参数与 Get 基准基本一致,另有一个bufferListSize(当前固定为16)控制缓冲区池大小。写入键由内部Counter状态类的AtomicInteger自增生成,保证每次写入的键互不重复。

为避免@Benchmark方法内的对象分配污染测量结果,Put 基准实现了一个简单的缓冲区借用池(见 PutBenchmarks.java):borrow从池中取出空闲缓冲区(池空时休眠等待),repay用完后归还,全程在synchronized保护下进行。

三个@Benchmark方法对比三种写入 API(见 PutBenchmarks.java):

  • put()db.put(cfHandle, keyBuf, valueBuf),不带显式WriteOptions,使用默认写选项;
  • putByteArrays()db.put(cfHandle, new WriteOptions(), keyBuf, valueBuf),显式传入新建的WriteOptions,度量每次构造写选项对象的开销;
  • putByteBuffers()db.put(cfHandle, new WriteOptions(), keyBuf, valueBuf),键值均以堆外ByteBuffer传入,度量 Direct Buffer 写入路径的开销。

通过对比这三个方法的结果,可以定位 RocksJava 写入链路中 "JNI 数据拷贝方式" 与 "选项对象构造" 各自的性能占比。

4.3 MultiGetBenchmarks:批量读取

源码见 MultiGetBenchmarks.java。除列族规模外,它的参数侧重批量语义:

参数取值含义
keyCount10000/25000/100000写入的键总数
multiGetSize10/100/1000/10000每次批量读取的键数量
valueSize16/64/250/1000/4000/16000值的字节长度
keySize16键的字节长度

Setup 阶段除了向默认列族与各命名列族灌数外,还预构建了两个列族句柄列表(见 MultiGetBenchmarks.java):defaultCFHandles(全部指向默认列族)与randomCFHandles(随机指向 20 个命名列族中的一个),分别用于显式列族与随机列族批量读取的对照。

四个@Benchmark方法(见 MultiGetBenchmarks.java):

  • multiGetList10()db.multiGetAsList(keys),批量键不指定列族(走默认列族),返回byte[]列表;
  • multiGetListExplicitCF20()db.multiGetAsList(columnFamilyHandles, keys),批量键显式绑定默认列族句柄;
  • multiGetListRandomCF30():批量键随机绑定到不同命名列族,度量跨列族批量读取的开销;
  • multiGetBB200()db.multiGetByteBuffers(keys, values),使用堆外ByteBuffer批量读写,通过返回的ByteBufferGetStatus校验每个键的Status.Code是否为Ok且值长度符合预期。

每个方法都会对结果长度/状态做断言式校验,任何不满足假设的返回都会抛出RuntimeException,从机制上防止基准在错误数据上得出无意义数字。

4.4 ComparatorBenchmarks:比较器开销

源码见 ComparatorBenchmarks.java。它专门度量不同比较器实现对写入路径的影响,通过@Param声明了 17 种比较器配置组合(见 ComparatorBenchmarks.java),可归为三类:

  • native 内置比较器native_bytewise(字节序)与native_reverse_bytewise(逆字节序),通过BuiltinComparator.BYTEWISE_COMPARATOR/REVERSE_BYTEWISE_COMPARATOR设置,比较逻辑完全在 C++ 层执行;
  • Java 实现的 bytewise 比较器java_bytewise_*系列,由org.rocksdb.util.BytewiseComparator实现;
  • Java 实现的逆序比较器java_reverse_bytewise_*系列,由org.rocksdb.util.ReverseBytewiseComparator实现。

Java 比较器名称中还编码了三组ComparatorOptions调优维度(解析逻辑见 ComparatorBenchmarks.java):

  1. 缓冲区类型directsetUseDirectBuffer(true),使用堆外 Direct Buffer 传递键数据)与non-directsetUseDirectBuffer(false));
  2. 缓冲区复用策略reused-64setMaxReusedBufferSize(64),允许复用最大 64 字节的缓冲区)与no-reusesetMaxReusedBufferSize(-1),禁用复用);
  3. 并发同步方式adaptive-mutexReusedSynchronisationType.ADAPTIVE_MUTEX)、non-adaptive-mutexReusedSynchronisationType.MUTEX)、thread-localReusedSynchronisationType.THREAD_LOCAL)。

基准方法只有put()(见 ComparatorBenchmarks.java):不断写入key0..keyN。由于写入过程中 memtable 需要按比较器维护键序,该基准可以清晰揭示 Java 比较器经 JNI 往返调用与原生比较器之间的性能差距,以及 Direct Buffer、缓冲区复用、同步策略对 Java 比较器开销的缓解效果。

五、编写自己的基准用例

基于上述工程结构,新增一个基准类的步骤是:

  1. org.rocksdb.jmh包下新建 Java 类(放在 java/jmh/src/main/java/org/rocksdb/jmh 目录),声明@State,用@Param声明可变参数,用@Setup/@TearDown完成数据库生命周期管理,并为每个待测操作添加@Benchmark方法;
  2. 参照工程内既有类的写法,在@Setup中先RocksDB.loadLibrary(),在@TearDown中逆序关闭所有原生资源(句柄、数据库、选项对象),并清理临时目录,避免资源泄漏影响测量;
  3. 若新类需要测试本地 JNI 改动,按本文 2.2 节的流程安装 SNAPSHOT 并更新 pom.xml 版本;
  4. 重新执行mvn package(license 插件会要求新文件携带许可证头),JMH 注解处理器会自动为你的@Benchmark方法生成运行骨架,随后即可用java -jar启动运行。

在编辑基准时,建议遵循工程内已有的两个原则:一是复用缓冲区(如 Put 基准的借用池、Get 基准的预分配数组),把分配开销排除在测量之外;二是对结果做断言校验(如 MultiGet 对状态码与值长度的检查),确保基准度量的是正确行为。

六、小结

rocksdbjni-jmh为 RocksJava 提供了开箱即用的微基准测试框架:mvn package一条命令即可产出包含全部依赖的 benchmarks uber-jar,java -jar即可运行;测试本地 JNI 修改时,通过mvn install:install-file安装 SNAPSHOT 并同步 pom.xml 版本即可无缝切换被测代码。工程内置的 Get、Put、MultiGet、Comparator 四组基准,覆盖了读写 API 的多种数据载体(byte[]与堆外ByteBuffer)、多列族场景、批量读取与自定义比较器配置,其@Param参数矩阵可直接用于横向对比 Java 层不同调用方式的开销差异,是评估 RocksJava 性能与验证 JNI 改动的实用工具。

【免费下载链接】rocksdbA library that provides an embeddable, persistent key-value store for fast storage.项目地址: https://gitcode.com/gh_mirrors/ro/rocksdb

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

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

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

立即咨询