めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

LevelDBの(低レベル)I/O処理構造

何の話かというと

LevelDBというのは、組み込み型のKeyValue Storeです。SQLiteのような感じでデーモンを立ち上げずにローカルファイルシステムを使って、KeyValue Storeが利用できます。

これをローカルファイルシステムではなくて、GlusterFSのボリュームで使えないか試したのですが、FUSEマウントを利用すれば、あっさりと動くことが確認できました。LevelDBは複数ファイルにデータをばらまくので、ファイル単位で分散するGlusterFSとの相性はよいかも知れません。

で、次のチャレンジというか思いつきで、巷で話題のlibgfapi[1]を使うようにLevelDBを改造できないかと、週末をつぶして頑張りました。結果としては、無事に動くものができあがりました[2]。この際、LevelDBのI/O処理がなかなかおもしろい構造になっていることに気づいたので、メモを残しておく次第です。

I/O処理モジュール

LevelDBのソース(なんとC++です・・・)を見ると、組み込みデバイスでの利用も意識しているようで、デバイス環境依存の高いI/O周りの処理を抽象クラス「Env (util/env.cc)」にまとめてあります。標準では、「PosixEnv (util/env_posix.cc)」という実装が用意されており、通常のPosixファイルシステムを利用するようになっています。なので、先の例では、この代替として、libgfapiを利用するためのクラス「GfapiEnv (util/env_gfapi.cc)」を実装しています。

Envクラスのヘッダ(include/leveldb/env.h)を見ると、実装するべきメソッドが分かりますが、非常にシンプルです。ファイルを読み書きするメソッドは、本質的に次の3つしかありません。

  virtual Status NewSequentialFile(const std::string& fname,
                                   SequentialFile** result) = 0;

  virtual Status NewRandomAccessFile(const std::string& fname,
                                     RandomAccessFile** result) = 0;

  virtual Status NewWritableFile(const std::string& fname,
                                 WritableFile** result) = 0;

指定のファイルについて、上から順に「シーケンシャルリード」「ランダムリード」「新規作成して追記型でオープン」という処理を行うオブジェクトが返ります。次に説明しますが、各オブジェクトの持つメソッドもこれまたシンプルです。

I/O処理のメソッド一式

まず、シーケンシャルリード用の「SequentialFile」は、指定バイトだけ次を読むか、スキップ(seek)するかです。それだけ。

  virtual Status Read(size_t n, Slice* result, char* scratch) = 0;
  virtual Status Skip(uint64_t n) = 0;

ランダムリード用の「RandomAccessFile」は、指定のオフセットから指定のバイト数だけ読み出すのみです。漢らしいです。

  virtual Status Read(uint64_t offset, size_t n, Slice* result,
                      char* scratch) const = 0;

最後に新規作成用の「WritableFile」は、なんと、アペンドするしかありません。後は、クローズするかSyncするかだけ。ますます漢らしいです。

  virtual Status Append(const Slice& data) = 0;
  virtual Status Close() = 0;
  virtual Status Flush() = 0;
  virtual Status Sync() = 0;

このあたりは、組み込みデバイスを意識して単純化している部分と、ランダムライトを排除して、追記しかしないという割り切りで、KVSの性能を追求する部分があるものと想像しています。もちろん、実際には追記だけだと、データ削除に伴う「穴」が増えてくるので、適当なタイミングでCompaction処理(穴空きファイルを新しい1つのファイルにまとめて書き出す処理)が走ります。Compactionに伴う負荷はそれなりにかかるのかも知れません。