项目作者: GINK03

项目描述 :
RocksDB <-> C++, Rust, Python, Kotlin
高级语言: Kotlin
项目地址: git://github.com/GINK03/rocksdb-bindings.git
创建时间: 2017-07-25T07:18:26Z
项目社区:https://github.com/GINK03/rocksdb-bindings

开源协议:

下载


RocksDBをさまざまな言語(C++, Rust, Kotlin, Python)で利用する

InstagramのCassandraのバックエンドをJVMベースのものから、RocksDBに切り替えたというニュースが少し話題になりました。

CassandraのJVMは定期的にガーベジコレクタが走って、よろしくないようです。

P99というテストケースではデフォルトのJVMからRocksDBに張り替えたところ10倍近くのパフォーマンスが得られたとのことです。



データ分析でもメモリ収まりきらないけど、Sparkのような分散システムを本格に用意する必要がない場合、NVMe上にLevelDB, RocksDBなどのKVSを用意して加工することがあります。

ローカルで動作させるには最強の速度だし、文句のつけようもない感じです。

LSMというデータ構造で動いており、比較対象としてよく現れるb-treeより書き込み時のパフォーマンスは良いようです[1]

LSMのデータ構造では挿入にO(1)の計算量が必要で、検索と削除にO(N)の計算量が必要です。

前提

  • RocksDBはSSDやnvmeで爆速を引き出すパーマネントKVSです
  • LevelDB, RocksDBはPythonで分析するときの必勝パターンに自分のスキルの中に入っているので、ぜひともRocksDBも開拓したい
  • RocksDBはC++のインターフェースが美しい形で提供さており、他言語とのBindingが簡単そう

もくじ

  • 1. RocksDBのインストール(Linux)
  • 2. Pure C++でのRocksDBの利用
  • 3. C++ Bindingの方針
  • 4. Rustでの利用
  • 5. Kotlinでの利用
  • 6. Python(BoostPython)での利用

これらのコードはこちらにあります。
[https://github.com/GINK03/rocksdb-bindings]

1. RocksDBのインストール

Ubuntuですと標準レポジトリにないので、ビルドしてインストールする必要があります

(GCC >= 7.2.0, cmakeなどの基本的なbuild-toolsが必要なので、ご自身のOSに合わせて用意してください)

  1. $ git clone git@github.com:facebook/rocksdb.git
  2. $ cd rocksdb
  3. $ mkdir build
  4. $ cd build
  5. $ cmake ..
  6. $ make -j12
  7. $ sudo make install

2. Pure C++

注意 最新のClangでは構文エラーでコンパイラが通らないので、gcc(g++ >= 7.2.0)を利用必要があります

C++でRocksDBは記述されているので、C++でのインターフェースが最も優れています。  

DBのopen, get, putはこのようなIFで提供されています

  1. DB* db;
  2. Options options;
  3. // Optimize RocksDB. This is the easiest way to get RocksDB to perform well
  4. options.IncreaseParallelism();
  5. // create the DB if it's not already present
  6. options.create_if_missing = true;
  7. // open DB
  8. string kDBPath = "test.rdb";
  9. Status s = DB::Open(options, kDBPath, &db);
  10. assert(s.ok());
  11. // Put key-value
  12. s = db->Put(WriteOptions(), "key1", "value");
  13. assert(s.ok());
  14. // get value
  15. string value;
  16. s = db->Get(ReadOptions(), "key1", &value);
  17. assert(s.ok());
  18. assert(value == "value");

Pinableという考え方があり、Pinableを用いると、データのコピーが発生しない(memcpyは動作しない)ので、高速性が要求されるときなど良さそうです

  1. PinnableSlice pinnable;
  2. s = db->Get(ReadOptions(), db->DefaultColumnFamily(), "key1", &pinnable); // メモリコピーコストが発生しない

3. C++bindings

C/C++でラッパーを書くことで任意のCのshared objectが利用できる言語とバインディングを行うことができます。

extern “C”で囲んだ範囲が外部のプログラムで見える関数になります。

  1. extern "C" {
  2. void helloDB(const char* dbname);
  3. int putDB(const char* dbname, const char* key, const char* value);
  4. int getDB(const char* dbname, const char* key, char* value);
  5. int delDB(const char* dbname, const char* key);
  6. int keysDB(const char* dbname, char* keys);
  7. }

サンプルのshared objectを作成するコードを用意したので、参考にしていただけると幸いです。

  1. $ cd cpp-shared
  2. $ make
  3. $ ls librocks.so
  4. $ ldd librocks.so
  5. linux-vdso.so.1 => (0x00007fff04ccd000)
  6. librocksdb.so.5 => /usr/lib/x86_64-linux-gnu/librocksdb.so.5 (0x00007fdaf33ab000)
  7. libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdaf3025000)
  8. libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdaf2e0e000)
  9. libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdaf2a2e000)
  10. libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fdaf280f000)
  11. libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdaf24b9000)
  12. /lib64/ld-linux-x86-64.so.2 (0x00007fdaf3e77000)

4. Rust

RustではC++のバインディングを利用してRocksDBにデータを格納したり取り出したりする方法を示します。

サンプルコードを動作させるには、以下のようにterminalを操作します。  

  1. $ cd rust
  2. $ export LD_LIBRARY_PATH=../cpp-shared/:$LD_LIBRARY_PATH
  3. $ make
  4. $ ./sample

Rustではstructで定義したものをimplで拡張していくのですが、例えば、putに関してはこのように設計しました。
C/C++などで文字の終了が示される\0が入らないため、このようなformatで文字を加工してC++に渡しています

  1. pub struct Rocks {
  2. pub dbName:String,
  3. pub cursol:i32,
  4. }
  5. impl Rocks {
  6. pub fn new(dbName:&str) -> Rocks {
  7. let outName = format!("{}\0", dbName);
  8. unsafe { helloDB( outName.as_ptr() as *const c_char) };
  9. Rocks{ dbName:outName.to_string(), cursol:0 }
  10. }
  11. }
  12. impl Rocks {
  13. pub fn put(&self, key:&str, value:&str) -> i32 {
  14. let dbName = format!("{}\0", &*(self.dbName));
  15. let key = format!("{}\0", key);
  16. let value = format!("{}\0", value);
  17. let sub = unsafe { putDB( (&*dbName).as_ptr() as *const c_char, key.as_ptr() as *const c_char, value.as_ptr() as *const c_char) };
  18. sub
  19. }
  20. }

5. Kotlin

Kotlin, JavaではGradleに追加することで簡単に利用可能になります。

  1. compile group: 'org.rocksdb', name: 'rocksdbjni', version: '5.10.3'

Interfaceも整理されており、以下のように簡単に、put, get, iterate, deleteが行えます

  1. import org.rocksdb.RocksDB
  2. import org.rocksdb.Options
  3. fun main(args : Array<String>) {
  4. RocksDB.loadLibrary()
  5. // DBをなければ作成して開く
  6. val options = Options().setCreateIfMissing(true)
  7. val db = RocksDB.open(options, "/tmp/kotlin.rdb")
  8. // データのput
  9. val key1 = "key1".toByteArray()
  10. val value1 = "value1".toByteArray()
  11. db.put(key1, value1)
  12. val key2 = "key2".toByteArray()
  13. val value2 = "value2".toByteArray()
  14. db.put(keygetvalue2)
  15. val bvalue = db.get(key1)
  16. println(String(bvalue))
  17. // seek to end
  18. val iter = db.newIterator()
  19. iter.seekToFirst()
  20. while( iter.isValid() ) {
  21. println("${String(iter.key())} ${String(iter.value())}")
  22. iter.next()
  23. }
  24. // データの削除
  25. db.delete(key1)
  26. db.delete(key2)
  27. db.close()
  28. }

実行

  1. $ cd kotlin
  2. $ ./gradlew run -Dexec.args=""
  3. Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details
  4. :compileKotlin UP-TO-DATE
  5. :compileJava UP-TO-DATE
  6. :copyMainKotlinClasses UP-TO-DATE
  7. :processResources NO-SOURCE
  8. :classes UP-TO-DATE
  9. :runApp
  10. value1
  11. key1 value1
  12. key2 value2
  13. BUILD SUCCESSFUL

6. Python

PythonはBoostPythonを用いると簡単にRocksDB <-> Pythonをつなぐことができます。
Python3とも問題なくBindingすることができて便利です。
ネット上のBoostPythonのドキュメントにはDeprecatedになった大量のSyntaxが入り混じっており、大変混沌としていたので、一つ確実に動く基準を設けて書くのが良さそうでした

CPPファイルの定義

CPPでRocksDBを扱うクラスを定義し、諸々実装を行います

  1. #include <boost/python.hpp>
  2. #include <string>
  3. #include <cstdio>
  4. #include <string>
  5. #include <iostream>
  6. #include "rocksdb/db.h"
  7. #include "rocksdb/slice.h"
  8. #include "rocksdb/options.h"
  9. using namespace rocksdb;
  10. namespace py = boost::python;
  11. class RDB{
  12. private:
  13. DB* db;
  14. public:
  15. std::string dbName;
  16. RDB(std::string dbName): dbName(dbName){
  17. Options options;
  18. options.IncreaseParallelism();
  19. options.create_if_missing = true;
  20. Status s = DB::Open(options, dbName, &(this->db));
  21. };
  22. RDB(py::list ch);
  23. void put(std::string key, std::string value);
  24. std::string get(std::string key);
  25. void dlt(std::string key);
  26. py::list keys();
  27. };
  28. void RDB::put(std::string key, std::string value) {
  29. this->db->Put(WriteOptions(), key, value);
  30. }
  31. ....

pythonの実装

Pythonで用いるのは簡単で、shared object名と同名のやつを読み出して、インスタンスを作成して、関数を叩くだけです(めっちゃ簡単)

  1. from rdb import RDB
  2. # create drow instance
  3. db = RDB('/tmp/boost.rdb')
  4. # access the word and print it
  5. print( db.dbName )
  6. db.put('key1', 'val1')
  7. val = db.get('key1')
  8. print(val)
  9. db.put('key2', 'val2')
  10. print(db.keys())
  11. val = db.delete('key1')

NVMeとHDDとのパフォーマンスの違い

もっと決定的に処理速度の差が出ると思ったのですが、そんなに変わらないという感じでした。



まとめ

ユースケースとして、転置インデックスを巨大なデータ構造そのままで、力でゴリゴリ押ししようとしてもメモリ上に乗らなかったりするとき、KVSとしてデータをファイルに書き出すことで効率的に行えたりします。(例えばWikipediaの記事全量からtf-idfを計算するときなど)

Valueは任意のシリアライザーでシリアライズしておく必要があり、Pythonだとpickle, KotlinだとKotlinx.serialize, Rustだとserdeなどが便利です。

今まではLevelDB(RocksDBのFork元)を用いていたのですが、PythonとC++しか実用的な装系がなく、もっといろんな言語とBindingしようとすると、RocksDBのほうが便利だと思いました。

参考文献