-
Notifications
You must be signed in to change notification settings - Fork 8
SidePlugin Java Binding
ToplingDB 在 Java Binding 中只增加了必要的 class SidePluginRepo,这是一个非常简单的 class(此处仅列出声明):
class SidePluginRepo extends RocksObject {
public SidePluginRepo(); // constructor
public void importAutoFile(String fname) throws RocksDBException;
public RocksDB openDB(String js) throws RocksDBException;
public RocksDB openDB(String js, List<ColumnFamilyHandle> out_cfhs) throws RocksDBException;
public RocksDB openDB() throws RocksDBException;
public RocksDB openDB(List<ColumnFamilyHandle> out_cfhs) throws RocksDBException;
public void startHttpServer() throws RocksDBException;
public void closeHttpServer();
public void closeAllDB(); // conform to C++ native name CloseAllDB
public void close(); // synonym to closeAllDB
public void put(String name, String spec, Options opt); // rarely used
public void put(String name, String spec, DBOptions dbo); // rarely used
public void put(String name, String spec, ColumnFamilyOptions cfo); // rarely used
}两句题外话
这么小的工作量,我已经多年没碰过 java,jni 更是从来没用过,实现这个 Binding,现学现卖,从头至尾,也只花了不到一天的时间。所以,良好的架构设计,可以极大地减小工作量,如果按照 SidePlugin 的架构,rocksdbjava 中的代码(java 代码与 c++ 各约 4 万行)至少可以缩减 90%。
参考 C++ 版的 101,Java 版的写法没啥大的不同;再参考实际的例子 SideGetBenchmarks,相信大家很容易使用 java 调用 ToplingDB。
其中的 put 方法是为了兼容已有代码:如果现存代码对各种 Option 进行了一些设置,并且这些设置无法通过配置文件进行配置(例如自定义了 java 版的 CompactionFilter),就先把 option put 到 SidePluginRepo,然后再调用 importAutoFile —— importAutoFile 会补充 put option 中的缺失选项,并覆盖同名选项!
将这个 Binding 再精简归纳总结:
void importAutoFile(String fname) throws RocksDBException;
RocksDB openDB(4 个重载) throws RocksDBException;
void startHttpServer() throws RocksDBException;
void close();
void put(String name, String spec, 三种 option); // 很少使用openDB 的四个重载中:
-
js参数一般是 json/yaml 中定义的 dbname,极少数情况下可以是 db 的 json 定义- 没有该参数的 openDB 指的是打开 json 中
open对象指定的 db, 这是多数情况
- 没有该参数的 openDB 指的是打开 json 中
-
out_cfhs指的是打开包含多个 ColumnFamily 的 db- 没有该参数的 openDB 指的是打开仅使用 default cf 的 db
一般情况下,在 openDB ... startHttpServer ... closeHttpServer ... closeAllDB 中,closeHttpServer 可以省略,因为 http 会在 closeAllDB 中自动关闭,同时,close 是 closeAllDB 的同义词,从而,可以简化为 openDB ... startHttpServer ... close,在使用了 try (...) {} 的情况下,close 也可以省略。
仍然提供 closeHttpServer 的原因在于,用户可能并不想在 db 的整个生存期内一直开启 http
ToplingDB jni 为 RocksIterator 增加了 Zero Copy 的能力。
ToplingDB 的 native iter 迭代每个 kv 不到 50 纳秒,现有的接口扫描每条 kv 要 4 次 jni 调用并拷贝 kv,zero copy 在无需拷贝的前提下扫描每条 kv 只要 1 个 jni 调用,大幅提升了扫描性能。
(点击展开) RocksIterator 新增的方法是 LowLevel 方法,允许用户程序可以通过 Unsafe 接口来使用 ZeroCopy,性能最高,但使用麻烦。
// 新增的方法,用于支持 Zero Copy
public Unsafe getUnsafe(); // 方便用户直接获取 Unsafe 实例
public long getZeroCopyKeyPtr();
public long getZeroCopyKeyLen();
public long getZeroCopyValuePtr();
public long getZeroCopyValueLen();
public boolean isValueFetched();
// 如果未使用下述 xxxWithValue 方法而是使用 xxx 方法,需要显式调用
// fetchValue 通过 jni 获取 value,效率低于 xxxWithValue。
// 典型场景是 value 很大,同时可通过 key 判断是否需要 fetchValue
public void fetchValue();
public void nextWithValue();
public void prevWithValue();
public void seekToFirstWithValue();
public void seekToLastWithValue();
public void seekWithValue(final byte[] target);
public void seekForPrevWithValue(final byte[] target);
public void seekWithValue(final ByteBuffer target);
public void seekForPrevWithValue(final ByteBuffer target);|
推荐方案:如果传递给
最小化迁移:如果现有代码中 DirectBuffer 是由 export ROCKSDB_FORCE_DIRECT_BUFFER_ZERO_COPY=true
java --add-opens java.base/jdk.internal.ref=ALL-UNNAMED ....最小化迁移的细节,太长不看这就需要在 zero copy 之前,先将原本在 java --add-opens java.base/jdk.internal.ref=ALL-UNNAMED ...总之,这种方式不是推荐的方案,不仅是因为使用了非标准的技术,还因为这会导致用户申请的内存被 zero copy “借来的内存”代替,有可能破坏现有逻辑:当用户设置 DirectBuffer.position 非零 或 limit 需要的尺寸时。 ——因为 zero copy 总是将 DirectBuffer 进行重置:position=0, limit 和 capacity 为其长度(remaining 也等于其长度)。 |
|
旧有方法的改进: |
|
|
RocksIterator 新增了 countKeysInRange(begKey, endKey[, fixedKeyLen]) 方法,比写 java 代码自己实现该功能快3倍以上。 非 0 的 fixedKeyLen 是个深度优化,需要满足条件:
|
要在点查中使用 Zero Copy,必须修改现有代码:
使用 DirectSlice.newZeroCopyDirectBuffer() 创建 DirectBufer,
并在点查前后使用 ReadOptions.startZeroCopy() / finishZeroCopy(),
为了避免粗心导致错误,推荐使用 ReadOptions.autoZeroCopy().
只有在 startZeroCopy() / finishZeroCopy() 之间,zero copy 才是安全的, 超出这个范围,zero copy 就有可能出错——这个出错的可能性非常低,但必须排除这个可能。
- Iterator 使用 zero copy 不需要 startZeroCopy() / finishZeroCopy(),因为 Iterator 本身就是一个隐式的 Context 对象,可以保证它引用的内存合法有效,不需要 startZeroCopy() / finishZeroCopy() 来保活。
可以在同一对 startZeroCopy() / finishZeroCopy() 之间执行多个 get 操作,这样做 效率更高,如果可能的话,应该尽量这样做,但是也不应太多,比如多到几千几万个,因为每个 zero copy 的结果在 Native 侧都可能会 Pin 住一些资源,直到 finishZeroCopy 的时候这些资源才会被释放。
旧有的使用 DirectBuffer 的 db.get 支持 zero copy,key, value 参数必须同时都是 DirectBuffer,这是上游 rocksdb 旧有的限制。
get 方法新增重载:key 是 byte[],可选参数 offset 和 len;value 是 ByteBuffer。
multiGetByteBuffers 也支持 zero copy;推荐新增方法 multiGetZeroCopy,更快更易用。
环境变量 ROCKSDB_FORCE_DIRECT_BUFFER_ZERO_COPY 在这里也有效力。
注意事项1:zero copy 总是将 value 的 DirectBuffer 进行重置:
设置 position=0, limit 和 capacity 为 value 长度(remaining 也等于长度)。
当现有代码依赖非0 position 或 limit 非全部 value 长度时,
环境变量 ROCKSDB_FORCE_DIRECT_BUFFER_ZERO_COPY=true 会将它们覆盖,导致
严重错误,甚至出现 segfault。虽然这不是典型用法,但是必须注意这一点。
注意事项2:在 zero copy 作用范围内,不能使用不带 ReadOption 参数的 get/multiGet/keyExists 等方法,因为这些方法在 JNI 端会使用一个临时 ReadOptions 对象,进而导致出现问题。
注意事项3:创建 DirectBuffer 很慢,所以创建出来的 DirectBuffer 必须重复使用才有价值,否则是负优化,参考以下测试(CPU 是 老古董 E5-2682v4):
| JIT 预热后 测试 | 典型耗时 |
|---|---|
| ToplingDB C++ Iter::Next | 50 纳秒 |
| ToplingDB C++ DB::Get | 200 纳秒 |
| JNI 调用(什么都不干的空 JNI 函数) | 40 纳秒 |
| JNI GetByteArrayRegion 4 字节 | 22 纳秒 |
| DirectSlice.newZeroCopyDirectBuffer() |
280 纳秒 60 纳秒 |
| ByteBuffer.allocateDirect(capacity=99) | 550 纳秒 |
| Unsafe.allocateMemory/freeMemory 分配尺寸 len 1k ~ 10K 指数分布 |
190 纳秒 160 纳秒 |
要想 DirectSlice.newZeroCopyDirectBuffer() 从280纳秒减小到60纳秒,需要添加
--add-opens java.base/java.nio=ALL-UNNAMED 命令行选项。
主要目的是提高 byte[] 创建即拷贝的性能:
对于返回 byte[] 的现存方法,ToplingDB 尽可能把 Native Slice 返回给 Java 并在 Java 侧创建 byte[] 并进行拷贝,这比在 JNI 侧创建 byte[] 并进行拷贝更快。通常情况下 Java 无法跳过创建 byte[] 时全部填0的初始化,使用 Java9 以上的 jdk.internal.misc.Unsafe.allocateUninitializedArray 可以实现这个优化,因为新创建出来的 byte[] 我们要马上把 Native 传来的 Slice 拷贝到里面, 此时全部填0的初始化纯属浪费。
优化方案:当环境变量 USE_INTERNAL_UNSAFE=true 时,跳过 byte[] 创建时全部填0的初始化,此时必须添加 java 启动参数:
--add-exports java.base/jdk.internal.misc=ALL-UNNAMED
如前所述。
如前所述。
默认值为 true, 表示 RocksDB.get 优先使用 zero copy, 不过这里的 zero copy 指的是尽量在 java 端执行 copy, 因为可使用 allocateUninitializedArray 来 创建 byte[] 并在 java 端使用 Unsafe 执行 copy, 减少了 JNI 交互, 充分利用 了 JVM 的优化能力。
完全的 zero copy 需要使用带 ByteBuffer value 参数的 get 。
sudo yum -y install git gcc-c++ jemalloc-devel
sudo yum -y install java-latest-openjdk java-latest-openjdk-devel maven
sudo yum -y install libaio-devel gflags-devel zlib-devel bzip2-devel libcurl-devel liburing-develgit clone https://github.com/topling/toplingdb.git --depth 1
cd toplingdb
git submodule update --init --recursive
ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts # silent git
make -j`nproc` DEBUG_LEVEL=0 shared_lib
sudo make install-shared PREFIX=/opt DEBUG_LEVEL=0# 请设置正确的环境变量 JAVA_HOME, 这里设置的是 open jdk 的默认路径
# Makefile 会尝试检测 JAVA_HOME, 但最好还是自己指定
export JAVA_HOME=/usr/lib/jvm/jre-openjdk
make rocksdbjava -j`nproc` DEBUG_LEVEL=0
# 安装 librocksdbjni 动态库(可选)
sudo make install-jni PREFIX=/opt DEBUG_LEVEL=0注意:因为动态库尺寸太大,ToplingDB 编译出来的 jar 包中不包含动态库。
你现在应该位于 toplingdb 仓库的根目录
提示:步骤 3+4 已包装在 toplingdb/java/jmh/build.sh 中。
cd java/target
cp rocksdbjni-8.10.2-linux64.jar rocksdbjni-8.10.2-SNAPSHOT-linux64.jar
mvn install:install-file -Dfile=rocksdbjni-8.10.2-SNAPSHOT-linux64.jar \
-DgroupId=org.rocksdb -DartifactId=rocksdbjni \
-Dversion=8.10.2-SNAPSHOT -Dpackaging=jar假定你现在位于 toplingdb 仓库的根目录
提示:步骤 3+4 已包装在 toplingdb/java/jmh/build.sh 中。
cd java/jmh
mvn clean package假定你现在已经位于 toplingdb/java/jmh
提示:本步骤已包装在 toplingdb/java/jmh/run.sh 中。
mkdir -p /dev/shm/db_bench_enterprise
cp ../../sideplugin/rockside/src/topling/web/{style.css,index.html} /dev/shm/db_bench_enterprise
# 将 toplingdb 和 librocksdbjni 的目录加入 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/opt/lib:$LD_LIBRARY_PATH
# LD_PRELOAD 至少包含以下两项,libjemalloc 必须是第一个
export LD_PRELOAD=libjemalloc.so:librocksdbjni-linux64.so
java -jar target/rocksdbjni-jmh-1.0-SNAPSHOT-benchmarks.jar \
-p keyCount=1000 -p keySize=128 -p valueSize=32768 \
-p sideConf=../../sideplugin/rockside/sample-conf/db_bench_enterprise.yaml \
SideGetBenchmarks运行起来之后,可以访问 http://127.0.0.1:2011 观测 db 状态。
说明 1:社区版用户请将 db_bench_enterprise.yaml 替换为 db_bench_community.yaml,这两个文件都在 toplingdb/sideplugin/rockside/sample-conf 中, sideplugin/rockside 是 ToplingDB 的 submodule。
社区版强行使用 db_bench_enterprise.yaml 也可以正常工作,只是 compact 中不会创建 ToplingZipTable 的 SST,与 yaml 中 level_writers 定义的不一致。
说明 2:设置 LD_PRELOAD 是因为 toplingdb 和 jemalloc 动态库使用了 init exec 类型的 TLS(线程局部存储),这种 TLS 访问速度最快,但使用了这种 TLS 的动态库必须预先加载,或者由可执行文件显式链接。而 java 加载 jni 是由 jvm 动态加载的,如果不用 LD_PRELOAD,会导致动态库中的这种 TLS 初始化失败。
目前使用 Java 实现的组件无法用 Java 代码注册到 SidePlugin 的插件库中,一般情况下这并不是什么严重问题,唯一的问题是使用分布式 Compact 会有些麻烦。
例如在 Java 中实现了自定义的 CompactionFilter/MergeOperator ,因为无法注册到 SidePlugin 的插件库中,所以无法为 dcompact_worker 预加载相应的动态库以运行分布式 Compact。除非使用以下方式:
(1)实现专门的动态库,在其中用 jni 创建 JVM 并调用 Java 代码,并创建 C++ 包装类,把相应的组件注册到 SidePlugin 中。
(2)使用 C++ 实现相同功能的插件并注册到 SidePlugin 中,例如数据都是 protobuf 格式,用 C++ 实现相应插件并不困难,而且性能还更高。实际项目中应首选该方案。
例如 flink rocksdb state backend 就是用 C++ 实现的 CompactionFilter,用 jni 包装后再通过 java 来管理