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:
groupId:org.rocksdbartifactId:rocksdbjni-jmhversion:1.0-SNAPSHOT- 项目描述:
JMH Benchmarks for RocksDB Java API
工程依赖三个组件:rocksdbjni(RocksDB 的 Java 原生绑定库)、jmh-core与jmh-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-annprocess,provided作用域),为每个标注了@Benchmark的类生成*_jmhType等运行时代理类; - maven-shade-plugin 3.2.1:在
package阶段将所有依赖(rocksdbjni、JMH 运行时等)打入同一个 uber-jar,并把 Main-Class 设置为org.openjdk.jmh.Main,同时剔除签名文件META-INF/*.SF、META-INF/*.DSA、META-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.jar2.2 测试本地修改:安装 SNAPSHOT 版本的 rocksdbjni
README 特别强调了一个关键约束:jmh 工程使用pom.xml的<version>元素声明的 rocksdbjni 构建产物。如果你修改了 RocksDB 的 C++/JNI 代码(例如改动了 java/rocksjni 下的 JNI 实现),那么:
- 先用仓库的 Java 构建系统(java/Makefile 中的
java目标等)构建出带 SNAPSHOT 版本号的本地 JNI jar; - 将本地 jar 安装到本地 Maven 仓库;
- 更新
java/jmh/pom.xml中rocksdbjni依赖的<version>,使其指向刚才安装的 SNAPSHOT 版本; - 再执行
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(...)创建临时数据库目录 → 构造DBOptions(setCreateIfMissing(true)与setCreateMissingColumnFamilies(true))→ 按需创建列族描述符 →RocksDB.open(...)打开数据库 → 灌入测试数据 → 必要时flush将内存数据刷入 SST。teardown 阶段则逆序关闭ColumnFamilyHandle、db、options等资源,并调用 FileUtils.java 的delete递归清理临时目录,保证多次运行互不干扰。
4.1 GetBenchmarks:单点读取
源码见 GetBenchmarks.java。它通过@Param声明了四组可变参数,组合展开后形成多维测试矩阵:
| 参数 | 取值 | 含义 |
|---|---|---|
columnFamilyTestType | no_column_family/1_column_family/20_column_families/100_column_families | 列族规模:0、1、20、100 个列族 |
keyCount | 1000/100000 | 每个列族写入的键数量 |
keySize | 12/64/128 | 键的字节长度 |
valueSize | 64/1024/65536 | 值的字节长度 |
Setup 阶段按列族数量创建名为cf1、cf2……的列族(见 GetBenchmarks.java),向每个列族写入keyCount条形如key0..keyN的数据并 flush 到磁盘。读取时通过AtomicInteger的compareAndSet实现无锁的键轮转,避免并发线程读到相同键;多列族场景下用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)使用堆外ByteBuffer(ByteBuffer.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。除列族规模外,它的参数侧重批量语义:
| 参数 | 取值 | 含义 |
|---|---|---|
keyCount | 10000/25000/100000 | 写入的键总数 |
multiGetSize | 10/100/1000/10000 | 每次批量读取的键数量 |
valueSize | 16/64/250/1000/4000/16000 | 值的字节长度 |
keySize | 16 | 键的字节长度 |
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):
- 缓冲区类型:
direct(setUseDirectBuffer(true),使用堆外 Direct Buffer 传递键数据)与non-direct(setUseDirectBuffer(false)); - 缓冲区复用策略:
reused-64(setMaxReusedBufferSize(64),允许复用最大 64 字节的缓冲区)与no-reuse(setMaxReusedBufferSize(-1),禁用复用); - 并发同步方式:
adaptive-mutex(ReusedSynchronisationType.ADAPTIVE_MUTEX)、non-adaptive-mutex(ReusedSynchronisationType.MUTEX)、thread-local(ReusedSynchronisationType.THREAD_LOCAL)。
基准方法只有put()(见 ComparatorBenchmarks.java):不断写入key0..keyN。由于写入过程中 memtable 需要按比较器维护键序,该基准可以清晰揭示 Java 比较器经 JNI 往返调用与原生比较器之间的性能差距,以及 Direct Buffer、缓冲区复用、同步策略对 Java 比较器开销的缓解效果。
五、编写自己的基准用例
基于上述工程结构,新增一个基准类的步骤是:
- 在
org.rocksdb.jmh包下新建 Java 类(放在 java/jmh/src/main/java/org/rocksdb/jmh 目录),声明@State,用@Param声明可变参数,用@Setup/@TearDown完成数据库生命周期管理,并为每个待测操作添加@Benchmark方法; - 参照工程内既有类的写法,在
@Setup中先RocksDB.loadLibrary(),在@TearDown中逆序关闭所有原生资源(句柄、数据库、选项对象),并清理临时目录,避免资源泄漏影响测量; - 若新类需要测试本地 JNI 改动,按本文 2.2 节的流程安装 SNAPSHOT 并更新 pom.xml 版本;
- 重新执行
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),仅供参考