KCのデータフォーマット再考

ID: 69
creation date: 2009/12/28 03:37
modification date: 2009/12/28 03:37
owner: mikio

2009年12月26日づけでKyoto Cabinetの初版をリリースした。経緯に関する詳しい話は後日まとめるとして、さっそく仕様変更をバンバンやっていく。なるべく初期に仕様変更は出し尽くしてしまいたいのだ。で、まずはDBファイルのフォーマットを変える。

新しいヘッダ部のフォーマット

仕様は変わるものであり、ミッションクリティカルな用途では後方互換性を確保できるということが製品価値につながる。すなわち、ファイルフォーマットを変更した新しいバージョンのライブラリを出したときに、そのライブラリで古いバージョンのファイルも読み書きできるとうれしい。何らかの事情で書き込み機能は諦めたとしても、読み込んで新しい形式に変換できることは重要だ。そのためには、ファイルの中にそのフォーマットのバージョン情報を含めないといけない。

また、KCではハッシュ関数やレコードの圧縮器をプラグインできるようにしているのだが、同一のファイルの読み書きをする時には常に同じプラグインのセットを使っていることを保証しなければいけない。しかしプラグインのコード自体をデータベースフォーマットに入れるわけにはいかないので、プラグインの出力が同一であることでもって同一性を判定する。そのためには、一定の入力をプラグインに読ませて、その出力を記録しておけばいい。ファイルを再度開いた際にも同じ計算をして、記録しておいたものと一致することを確かめるのだ。「__kyotocabinet__」という文字列に圧縮器をかけ、その出力をさらにハッシュ関数に通し、その値の下位1バイトを代表値とすればいい。

ついでに、予約領域を静的なものと動的なものに分けた。KCをstable宣言してからどうしてもメタデータを追加したいような場合には予約領域を使うわけだが、静的データ(DB作成時に決まって以後は変わらない)と動的データ(DBを更新する度に変わる可能性がある)を明確に区別してトランザクションの保護対象を削りたい。

ということで、ヘッダ部の新しいフォーマットは以下のようになった。全体で64バイトというところは変わっていないので性能への影響は皆無だと思う。

名前 オフセット 長さ 機能
マジックナンバ 0 4 KCのDBファイルであることの判別。「KC\n\0」
ライブラリバージョン 4 1 ライブラリのメジャーバージョン番号
ライブラリバージョン 5 1 ライブラリのマイナーバージョン番号
フォーマットバージョン 6 1 現状では1
チェックサム 7 1 文字列「__kyotocabinet__」のハッシュ値の下位1バイト
データベースタイプ 8 1 ハッシュ表(0x01)かその他(未定)か
アラインメント力 9 1 アラインメントに対する2の冪乗
フリーブロックプール力 10 1 フリーブロックプールの要素数に対する2の冪乗
オプション 11 1 スモール(1<<1)、リニア(1<<2)、圧縮(1<<3)の論理和
静的予約領域 12 4 静的データの未使用領域
バケット数 16 8 バケット配列の要素数
状態フラグ 24 1 開きっぱなし(1<<0)、致命的エラー(1<<1)の論理和
動的予約領域 25 7 動的データの未使用領域
レコード数 32 8 格納しているレコードの数
ファイルサイズ 40 8 データベースファイルのサイズ
不透明データ 48 16 不透明データのサイズに対する2の冪乗

レコード部のフォーマット

フォーマット以前と変わっていないけれど、後で参照するのでここにも書いておこう。オフセットのサイズは、デフォルトだと6バイトで、スモールモードだと4バイトである。また、リニアモードの場合は右チェーンの領域がない。よって、キーと値とパディングの実データを除いたヘッダ部分の最小サイズ(つまりレコード毎のフットプリント)は、デフォルトで16バイト、スモールモードで12バイト、リニアモードで10バイト、スモールリニアモードで8バイトとなる。

名前 オフセット 長さ 機能
マジックナンバ 0 2 データの識別とパディングのサイズ
左チェーン 2 6 左チェーン接続先のオフセットのアラインメント商
右チェーン 8 6 右チェーン接続先のオフセットのアラインメント商
キーサイズ 14 可変 キーのサイズ
値サイズ 可変 可変 値のサイズ
キー 可変 可変 キーのデータ
可変 可変 値のデータ
パディング 可変 可変 意味を持たないデータ。先頭バイトは整合性チェッカ(0xEE)

レコードの削除や移動などでフリーブロックとなった領域には、以下のフォーマットでデータが記録される。ブロックサイズがデフォルトとスモールモードで違うのはレコードの時と同じである。

名前 オフセット 長さ 機能
マジックナンバ 0 2 データの識別(0xDDを2つ)
ブロックサイズ 2 6 ブロックのサイズのアラインメント商
パディング 8 可変 意味を持たないデータ。先頭バイトは整合性チェッカ(0xEE)

パディングのサイズはアラインメントサイズ未満であることが保証され、アラインメントの最大値は2<<15==32768であるため、つまりパディングのサイズはINT16_MAX以下であることが保証されている。したがって、レコードの先頭バイトは0x7f以下であることが保証され、0x80以上である場合にはフリーブロックであるということになる。なお、フリーブロックを再利用してレコードを記録した場合にはパディングのサイズがINT16_MAXを越えてしまう恐れがあるので、その場合はパディングを最小のアラインメントで切って、残りをフリーブロックとしてフリーブロックプールに再登録する。

レコード部の開始オフセットは、バケット部の末尾以降の最初のアラインメントの開始位置に合わせられる。

フリーブロックプール部のフォーマット

ヘッダ部の直後から、フリーブロックプールのサイズに6を掛けた長さの領域がフリーブロックプール部である。DBを閉じる時に持っているフリーブロックプールをオフセットの昇順で整列してから、各要素のオフセット部分を前の要素のオフセットとの差分に変換する。さらに各要素のオフセットとサイズの双方をアラインメントで割る。その値を先頭から順にBERエンコードで直列化して連結していく。そして最後に0x00を番兵として置く。

各要素をオフセット6バイトとサイズ6バイトの12バイトずつで直接化してもよかったのだが、それよりは差分列に圧縮して保存した方が領域を節約できる。ただし、常に圧縮率が50%を達成できるとは限らない。その場合に溢れた分は切り捨てる。フリーブロックプールはオマケ機能にすぎず、プールから消えたフリーブロックは別のデフラグ機能で回収することを期待すればよい。

バケット部のフォーマット

フリーブロックプール部の直後から、バケット数に6を掛けた長さの領域がバケット部である。スモールモードの場合は6でなく4となる。各要素はチェーンの先頭レコードのオフセットをアラインメントで割った値か、チェーンが空なら0が記録される。

TCではバケット全体をmmapで確保することを強制していたが、KCではその制限はなくなった。設定(tune_map)によって任意のサイズを指定して内部的にmmapを使わせることができる。0にもできるし仮想メモリの限界までとることもできる。mmapを使うのかpread/pwriteを使うのかFileクラスで完全に隠蔽されているので、ハッシュDB層ではそれについて考える必要はない。ただ、抽象化の分だけオーバーヘッドはでかくなっている。

おまけ

KCは動的ハッシュ系の手法を利用していないので、ハッシュバケットの利用率が高くなると性能が悪化する。レコードの参照にかかる時間は、各ハッシュバケットに連なるチェーンの長さに比例するので、その期待値をハッシュ表全体の「負荷」として可視化できると嬉しい。

バケット配列全体をスキャンするとその使用数がわかり、レコード数をバケット使用数で割ると、各バケットに連なるレコードの平均値が求まる。リニアモードの場合はその値がそのまま負荷となる。デフォルトでは、最初のレコードは1本道で、それ以後が2分探索木なので、リニアモードの値に1を足してから2の対数をとった値が負荷となる。以上の計算をコマンド一発「kchashmgr inform -st foobar」でできるようにしてあるので便利だ。

0 comments
riddle for guest comment authorization:
Where is the capital city of Japan? ...