夏が近づくとウキウキしてくるmikioです。昨日ついにリリースされたKyoto Cabinet 1.0について今回は報告します。

1.0の位置づけ

コミュニティ毎や製品毎にバージョン番号割り当ての方針は異なるわけですが、私の個人的なポリシーでは、1.0には特別な意味があります。すなわち、0.xのバージョンはbeta版的な位置づけで、「実サービスに使うのはちょっと待った方がいいですよ」ということを意味します。一方で、1.xはstable版的な位置づけで、「よろしければ実サービスでも使ってみてください」ということを意味します。私がstable版に設定する原則を以下に列挙します。

  • 安定稼働を至上命題とする(バグがあればその修正を最優先する)
  • APIを変更しない(変更するとしても後方互換性を維持する)
  • DBファイルのフォーマットを変更しない(変更するとしても後方互換性を維持する)
  • なるべく機能追加しない(追加するとしてもモジュールを分割して行う)
  • テスト追加とリファクタリングと文書執筆は継続的に行う。
  • 余剰工数でなるべくユーザサポートを行う。

安定性というのは永遠の課題です。1.0になったからってバグがないと言いきれるわけでもない(見つかったバグはもちろんすべて潰しているが、見つかっていないバグがもう無いということは証明できない)ので、あくまで気持ちの問題にすぎません。しかし、結構テストを書いたのでポピュラーな環境とユースケースでは普通に動くとは思います。

特徴のおさらい

1.0になった今や、Kyoto Cabinetは以下の特徴を備えます。いつの間にかWindowsがサポートされているあたりが激アツですね。

  • 時間効率:ハッシュDBはO(1)、ツリーDBはO(logN)の時間計算量。更新能力は100万qps以上。
  • 空間効率:レコードあたりのフットプリントが、ハッシュDBだと8~16バイト、ツリーDBだと2~4バイト。
  • 並列性:ハッシュDBはレコード単位のread-write lockのみ、ツリーDBはツリーDBはページ単位のread-write lockのみ。
  • 利便性:Visitorパターン風インターフェイスによるレコード操作の一般化。多相DBによるアルゴリズムの動的変更。
  • 堅牢性:手動トランザクション、自動トランザクション、自動リカバリ等の実装。
  • 移植性:UNIX系(Linux、FreeBSD、Solaris、Mac OS X)だけでなくWindows(VC++)もサポート。
  • 他言語バインディング:C++がコアだが、C, Java、Python、Ruby、Perlのバインディングも提供。

Tokyo Cabinetに比べると、KCは特に並列性と利便性と移植性に優れています。ただしシングルスレッドでの時間効率はTCの方が2倍くらいよいです。とはいえ、マルチコア・メニーコアが主流となってくる2010年代ではTCよりもKCの方が活用できると思いますので、ぜひKCの採用を検討していただきたいと思います。なお、TCの保守も継続してバグ修正等の対応はします。

他(多)言語バインディングのススメ

バージョン1.0を出すための条件として、ポピュラーなプログラミング言語の中で私がサポートできるもののバインディングは揃えてしまおうと考えていました。その理由は主に2つあります。ひとつは、バインディングを書く過程でコア側のAPIをいくつか変更したくなることは容易に予想できたからです。もうひとつは、各種の言語のインターフェイスを設計しながら個々の言語の視点でコア側を眺めると、C/C++視点では見つけられなかった設計上の問題がいろいろ検出できるので、それに事前に対応しておきたかったからです。

上記はTCとそのバインディングを書いた時の経験則ですが、おそらくDBM以外のC/C++ライブラリに関しても同じことが言えると思います。他言語バインディングを書く時には特にスレッド対応の点で苦労します。JavaやRubyなどのガベージコレクションを前提とする言語では、言語処理系側に所有権を預けたオブジェクトのデストラクタが制御できないタイミングで呼ばれるので、デストラクタの呼び出しタイミングについて何らかの前提条件を設定していた場合に困ったことになります。今回の例で言うと、あるDBオブジェクトに対して作ったカーソルオブジェクトの生存期間よりもDBオブジェクトの生存期間は長くなければいけないという前提があったのですが、GCでオブジェクトを破棄する場合にはその順序は制御できないのです。そういう場合には、カーソルクラスをラップした「カーソルデポジット」クラスを容易して、DBの生存期間に関連づけてカーソルを破棄するようにしなければなりません。

他にも、Java1.1やRuby1.8やPerl5などにおけるスレッド実装のように、グリーンスレッド(ネイティブスレッドを使わずにユーザ空間でスレッドを実装するモデル)の場合には、C/C++側で使っているネイティブスレッドのロック機構やTLS(スレッドローカルストレージ)が全く機能しないので、個々の処理系のロック機構やTLSを使って排他制御を実装しなおさなければならないという問題があります。この問題に関してはKCでは対応できていません。今後すげー暇になったら対応するかもしれませんが、利得が低いので多分やらないと思います。

他言語バインディングを書く上でスレッドの次にハマるのが、文字列の扱いについてです。JavaやPython3のように文字列クラス(JavaのString、Python3のUnicode)とバイト配列クラス(Javaのbyte[]、Pytohn3のbytes)が分離されている処理系もあれば、PerlやRubyのように双方を同じ型として統合して扱う処理系もあります。分離脳で考えている時に統合スタイルを見ると、別概念を無理やり一緒にしちゃっている感じがして気持ち悪いですが、一方で統合脳で考えている時に分離スタイルを見ると、似たような概念をわざわざ別クラスに分けたおかげで手間が2倍になる気がして気持ち悪いです。個人的には分離でも統合でもなくバイト列だけあればいい派で文字列なんてものがなければいいと思っているのですが、まあいずれも好み(および主想定ユースケース)の問題なんで、郷に入れば郷に従えというのが結論です。

KCとしては、分離スタイルの言語では、ライブラリへの入力(set等のメソッドの引数)としては文字列もバイト列もポリモーフィックに両方受け付けて、内部的にバイト列に変換した上でコアに渡します。ライブラリから出力(get等のメソッドの戻り値)としては、基本的にバイト列を返し、メソッド名に「_str」という接尾辞をつけたメソッドを呼び出した場合には文字列を返すようにしています。一方で統合スタイルの言語では、文字列はUTF-8前提で解釈して暗黙的にバイト列と相互変換して扱うようにしています。

といった感じで、ある言語の世界にどっぷり浸かりながら別の言語に触れると、奇妙に見える点を見つけてはdisってしまうのが人情というものですが、いくつかの言語のバインディングを通して書いてみると見方が変わってきます。こと歴戦の猛者達が議論の限りを尽くして設計している言語処理系に限っては、それぞれにちゃんとした理由があってやっていることがほとんどなのだなぁと実感できるのです。また、自分の実装が特定の言語でしか通用しない前提条件を設けてしまっていることに気づいて修正する機会を得ることができます。TCもKCも他言語バインディングの実装とそれに伴うコアの修正によって製品の品質(と自分の偏狭さ)がかなり改善したので、機会があればぜひ皆さんもC/C++実装+他言語バインディングという開発スタイルに挑戦してみてください。

Perlバインディング

Perlの会社に勤めているので、Perlバインディングについて宣伝しておかないと恰好がつきませんね。どの言語でもできることは基本的に変わらないのでRubyバインディングの記事を見ていただければ機能性については理解したいただけるとは思いますが、いちおうPerlの文脈で書き直してみます。

まずは基本的なユースケースですね。ライブラリを取り込んで、データベースオブジェクトを作成して、データベースを開いて、いくつかレコードを格納して、検索してみて、さらに全レコードの一覧を取得してみて、最後にデータベースを閉じます。

use KyotoCabinet;

# データベースオブジェクトを作る
my $db = new KyotoCabinet::DB;

# データベースを開く
if (!$db->open('casket.kch', $db->OWRITER | $db->OCREATE)) {
    printf STDERR ("open error: %s\n", $db->error);
}

# レコードを格納する
if (!$db->set('foo', 'hop') ||
    !$db->set('bar', 'step') ||
    !$db->set('baz', 'jump')) {
    printf STDERR ("set error: %s\n", $db->error);
}

# レコードを検索する
my $value = $db->get('foo');
if (defined($value)) {
    printf("%s\n", $value);
} else {
    printf STDERR ("get error: %s\n", $db->error);
}

# レコードの一覧を取得する
my $cur = $db->cursor;
$cur->jump;
while (my ($key, $value) = $cur->get(1)) {
    printf("%s:%s\n", $key, $value);
}
$cur->disable;

# データベースを閉じる
if (!$db->close) {
    printf STDERR ("close error: %s\n", $db->error);
}

TIEHASH

KCでは、言語共通の機能の他に、各言語独自のスタイルでもごにょごにょできるようにしています。Perl独自といえばやはりtieですよね。こんな風に使います。

use KyotoCabinet;

# ハッシュ変数をデータベースに結びつける
my $db = tie(my %db, 'KyotoCabinet::DB', 'casket.kch');

# レコードを格納する
$db{'foo'} = 'hop';   # 文字列が基本
$db{bar} = 'step';    # キーの引用符は省略できるよ
$db{3} = 'jump';      # 数字だって使えちゃう

# レコードを検索する
printf("%s\n", $db{'foo'});

# トランザクション内で更新する
$db->transaction(sub {
    $db{'foo'} = 2.71828;
    1;  # 真を返せばcommit、偽を返せばabort
});

# Visitorパターンでレコードの値を2倍にする
$db->accept('foo', sub {
    my ($key, $value) = @_;
    $value * 2;  # 戻り値が新しい値になる
});

# 標準イテレータでレコードの一覧を得る
while (my ($key, $value) = each(%db)) {
    printf("%s:%s\n", $key, $value);
}

# KC独自イテレータでキーの値を大文字にする
$db->iterate(sub {
    my ($key, $value) = @_;
    uc($value);
});

# カーソルでレコードの一覧を得る
$db->cursor_process(sub {
    my ($cur) = @_;
    $cur->jump;
    while ($cur->accept(sub {
            my ($key, $value) = @_;
            printf("%s:%s\n", $key, $value);
            KyotoCabinet::Visitor::NOP;
        })) {
        $cur->step;
    }
});

# 関連付けを破棄してデータベースを閉じる
undef($db);
untie(%db);

tieしたハッシュ変数を介してはハッシュインターフェイスのメソッドしか呼び出せません。そこで、ここではトランザクション等のKC独自インターフェイスも同時に使うために、tieの戻り値である元来のオブジェクトも利用しています。

my $db = tie(my %db, 'KyotoCabinet::DB', 'casket.kch');

上記で、$dbがKC元来のDBオブジェクトの参照を保持する変数で、%dbはハッシュ変数です。$db{”hoge”} とした時にはハッシュ変数の方が評価され、$db->hogeとした時には元来のオブジェクトの参照変数の方が評価されますが、両者はあくまで別個の変数です(紛らわしいようならば別の名前にするのもよいでしょう)。なので、スコープ内で破棄する時には別個に破棄する必要があります。

undef($db);
untie(%db);

もちろん、ハッシュ互換インターフェイスしか使わないのであれば、tieの戻り値を何にも代入しないでOKです(その場合は当然最後のundefも必要ありません)。

メモリ使用量

tieがあることで組み込みハッシュの省メモリな代替としてKCを使えるわけですが、実際どのくらいメモリ使用量が少なくなるかを見てみましょう。100万レコードを格納した際のメモリ使用量と時間を計ってみました。

メモリ使用量 経過時間
標準ハッシュ 99.332 MB 1.683 秒
KC(オンメモリDB) 53.262 MB 5.431 秒
KC(ファイルハッシュDB) 40.031 MB 7.315 秒
KC(ファイルツリーDB) 60.258 MB 6.486 秒

ということで、KCのオンメモリDBを使えば、時間効率が多少落ちる代わりに、メモリ使用量を半減させられます。ファイルハッシュDBやファイルツリーは上記のテストではデフォルトの設定ですが、チューニング次第でメモリ使用量をさらに減らすこともできます。例えばファイルハッシュDBで ‘casket.kch#msiz=0′ とすればメモリ使用量は3.5MBまで減らせますし、ファイルツリーDBで ‘casket.kct#msiz=0#pccap=0′ としても同じく3.5MBまで減らせます。

経過時間が、KCだといずれも標準ハッシュより大きく劣っていますね。KCのコア自体は100万レコード格納を1秒未満で終わらせる能力があるので、多くの時間をPerl層で食ってしまっているようです。その要因としてはバインディングのXSUB関数を直接呼ぶのでなくPerlコードを一回噛ませてしまっていることと、さらに関数呼び出しの際にスカラのコピーが行われていることが考えられます。おそらく、pm層を省けばあと1秒くらい縮められ、インターフェイスをリファレンス渡しに変更すればさらに1秒以上は縮められると思います。でもそれらのメリットよりはデメリットの方が大きいのでおそらくやらないでしょう。

JavaとPython

KCはどちらかと言えばサーバサイドのさらにバックエンド寄りのユースケースを第一目的として作っているので、その層でC++と並んでよく使われるJavaのバインディングもサポートしました。PythonやRubyやPerlは処理系自体の並列性がないのでKCの並列性能を生かしにくいのですが、JavaVMの並列性はかなり良好なので、C++以外でKCを使うとしたらまずJavaをオススメします。

Google App Engineに採用されるなどで日本でも人気の出てきたPythonのバインディングもサポートしました。個人的にはPythonのプログラミング経験がほぼゼロだったのですが、とりあえずC用のAPI文書だけを読んでPythonバインディングを実装することができました。対象の言語を書いたことがなくてもバインディングは書けるというのは本当です。PythonバインディングではRubyバインディングと同様に「並列モード」を用意してあるので、KCコードの実行時に処理系のロックを外して並列処理を行うことができます(ただしPython処理系に比べてKCが高速すぎるので現状では意味はないです)。なお、現状ではPython3のみのサポートとなっていますが、Python2用のバインディングもいずれは書く予定です。

まとめ

Kyoto Cabinetの正式バージョンたる1.0がリリースされました。計画されていた全機能を実装し、コマンドラインユーティリティやテストツールも付属しています。各種言語バインディングも提供されているので、ぜひ試してみてください。

当初の計画ではLuaバインディングも作る予定だったのですが、ちょっと延期します。テーブルDBやKyoto Tyrantの方が需要がありそうなのでそちらの検討をする所存です。

GW 中の長距離移動のために体調が優れない takahi-i です. 今回は巨大なデータをマイニングする一つの技術として LSH (Localtiy Sensitive Hashing) を紹介させていただきます.

LSH とは

LSH は大量なデータから類似度が高いインスタンスのペアを高速に抽出してくれるアルゴリズムです. ここでインスタンスはデータ集合の一つの要素を表します. たとえば扱うデータが E-コマースサイトの購買ログであれば, インスタンスは各ユーザですし, 画像データ集合であれば, インスタンスは個々の画像データです.

LSH の詳しい解説については以下のサイトがあります.

本稿ではE-コマースサイトの購買履歴データを基に LSH の機能について述べてゆきます.

以下のような E-コマースサイトの購買履歴 (ログ) データがあったとします. この表において一列目はユーザ番号を二列目はユーザが購入した商品一覧をそれぞれ表します.

ユーザID 購買アイテム一覧
0 iPad, Microsoft Zune, うまい棒
1 ヘルシオ, REGZA
2 Nicon D3000, チョコレート
3 チーズケーキ, ロレックス, オメガ
4 エルメス, グッチ, ユニクロ
5 Microsoft Zune, うまい棒, 任天堂DS

LSH  はこの表の中から同じ興味を持つインスタンスペア (上記の表ではユーザペア) を高速に抽出してくれます. たとえば上記の表から抽出されるペアとして (ユーザ 0, ユーザ 5) が考えられます. 同じ商品を二つも購入しているため, 同一の興味をもつユーザであると推測されるためです. このようなユーザペアを抽出することで, ユーザ 0 にユーザ 5 が購入したアイテム (任天堂DS) を推薦することができます.

上記のような表から類似度の高いユーザペアを抽出するには, ユーザが購入した商品が要素となるベクトル間の類似度 (コサイン関数等) を総当たりで計算し, 類似度があるしきい値以上のユーザペアを抽出する方法が一般的です. しかし, たくさんのユーザが存在する場合 (たとえばミクシィでは 2000 万を越えるユーザが利用しています) すべてのユーザ間の類似度を総当りで計算するのでは計算コストが非常に大きくなってしまいます.

これに対して LSH はユーザ間の類似度を総当たりで計算する処理は行いません. LSH ではベクトル (上記の例ではユーザの購入した商品が要素となるベクトル) を入力として, スカラ値を出力する関数 (ハッシュ関数) を定義します. この関数は似たベクトルであれば同一の値を返す (返しやすい) という特徴を持ちます. LSH は関数からかえされるハッシュ値が同一のユーザペアを抽出することで類似するユーザペアを高速に抽出できます. 各ユーザベクトルを並列に処理することができ, さらに類似度を全ての組み合わせで計算しなくても良いため計算量の大幅な低減が望めます.

上記の例ように E-コマースサイトのログから興味が近いユーザを抽出することができますが, LSH を利用することで, ニュースのレコメンド, 類似画像の抽出に利用できることが知られています.

LSH の一実装 (Likelike)

LSH の一実装として, Likelike (リケリケ) というツールを作りました (ここからダウンロードできます). Likelike の特徴として Hadoop の MapReduce 上で動作するため, 使用する計算機の数を増やすことでより大規模なデータに対応できるという点があります. 予備の実験として1000 万件のデータ (各データは 10 個の特徴量を持つ) を自動生成し 4 台の計算機で計算したところ, 一時間程度で終了しました (パラメータ depth=2, iterate=20 の時).

Likelike の入力として以下の様なタブとスペースで区切られたファイルが必要です.

% cat src/test/resources/testInput.txt
0    776 2919 4576 4485 2380 4261 3622 1456 2109 3227
1    4358 4350 559 1136 2848 1345 1428 4212 2681 120
2    3614 3035 4592 1191 441 1408 734 1005 4826 33
3    691 878 1938 3381 127 4283 2269 3814 2908 1864
4    3781 603 1262 1988 1309 864 3511 729 1454 2845
5    3183 3575 180 4367 3616 4072 824 4976 4021 3361
6    3210 1956 2421 203 3006 2706 4794 4805 4180 2124
7    1548 3831 1447 3040 4137 3055 809 4497 2052 4994
...

このファイルにおいて, 一列目はインスタンス ID を表し, スペースで区切られている 2 列目以降はインスタンスの特徴量をそれぞれ表します. たとえば, このファイルに含まれるデータを E-コマースサイトのユーザの購買履歴とすれば, 一列目はユーザ ID を, スペースで区切られた二列目以降は購入した商品 ID を表します.

次にこのファイルを入力として指定して Likelike を実行します. 詳しい使用方法については Likelike の QuickStart を参照してください.

% sh bin/likelike lsh -input src/test/resources/testInput.txt -output testOutputDir

上記のコマンドを実行すると, 以下の様に各行が類似ベクトルペアからなる結果が出力ディレクトリ testOutputDir 内のファイルに出力されます.

0       5766
0       1962
0       2649
...

これはユーザ 0 の関連ユーザとして 5766, 1962 が抽出されたことを示しています.

まとめ

LSH の簡単な解説とその一実装である Likelike について説明させていただきました. 今後はパフォーマンスおよび, 精度向上を目指して開発を続けてゆく予定です. 是非使用していたき, フィードバックをいただければと考えております.

(いわゆる宣伝エントリーなので余裕のある方はお読みください)

お世話になっております。ソーシャルグラフ開発チーム asannou です。
Gmailとmixiがつながりました」ということで、Gmail™のアドレス帳(連絡先)からマイミクシィを追加したり、友人をmixiに招待したりできるようになりました。以前から、メールアドレスでマイミク登録という機能がありますが、ここにアドレス帳のメールアドレスを一個ずつコピペするかわりに、まとめてインポートしてサクサクとマイミク申請&招待できるようになるイメージですね。
今回は技術的な部分のご紹介をさせていただきたいと思います。

アクセス権限を委譲する

本機能では、Gmail™のアドレス帳をインポートする際に、OAuthという仕組みを利用してアクセスをおこなっているので、mixiに、Googleアカウントのユーザー名とパスワードを入力する必要がなく、Gmail™のアドレス帳のみにアクセスする権限を与えられる(委譲できる)ようになっています。

Googleアカウントのログイン画面

Googleアカウントのログイン画面


OAuthの認可画面

OAuthの認可画面

こちらが実際にインポートをするときのスクリーンショットですが、Googleのログイン画面がポップアップされているのがわかります。ここに、Googleアカウントのユーザー名とパスワードを入力し、リクエストを許可することによって、アクセス権限が委譲され、アドレス帳のインポートが可能になるわけです。アドレスバーの先頭が https://www.google.com/ となっていることを確認してから、パスワードを入力するようにしてくださいね。

権限の委譲について、もうちょっと詳しく説明しますと、今回はOpenIDをベースにOAuthを乗せた形の、OpenID OAuth Extensionを採用してみました。理由としては、同時にOpenID User Interface Extensionに対応することで、前述のような、権限の委譲が直感的にわかりやすいUI(mixiとGoogleのfaviconが並んで表示されるとか)を提供する狙いがあります。Extensionとか言われてもよくわかんない、という方はとりあえず、「OAuth単体で使うより、OpenIDと組み合わせたほうが、おしゃれなポップアップウィンドウを出せるから、そうした。あとOpenIDを流行らせたい」とご理解ください。

とにかく、これにてmixiは、一気にOAuth ConsumerおよびOpenID Relying Partyになることができました。技術的に、mixiにOpenIDでログインしたり、新規登録するための条件が整ったということですので、希望される方はどこかでつぶやくと、私の来期あたりのコミットメントに「OpenIDによるログイン機能のリリース」が追加されるかもしれません。
もうお忘れになっている方もいらっしゃるかもしれませんが、mixiはOpenID Providerでもあるので、よろしければそちらもあわせてご利用ください。

ODF

そもそも、ウェブメールのアドレス帳をインポートする機能なんていうのは、今やSNSでは標準装備だったりします。
実は、2年ほど前にODFで取り組んだネタが発端だったのですが、紆余曲折がありまして、一旦ペンディングになっていたのでした。そんなプロジェクトを拾い上げてくれた、今のチームのリーダーや、一緒に仕様を考えてくれたディレクターには感謝です。
mixiのユーザー層で、Gmail™を利用されている方の割合は多くありませんが、スマートフォンのシェアが伸びていき、サーバサイドでアドレス帳を管理するのが一般的になれば、このような機能も活きてくるのではないかと思っています。
それにしても、当時の資料のタイムスタンプが2008年などとなっているのを見ると、胸が熱くなりますね。

サードパーティにパスワードを預けること

逆に、OAuthのような、限定的に権限を委譲できる仕組みを利用しないことにより、思いもよらないリスクが存在するケースもあります。
最近、mixiのリソースにアクセスするために、mixiのログインメールアドレスとパスワードを要求する、サードパーティ製のウェブサービスを多く見かけるようになりました。mixiはSNSという性質上、サードパーティにパスワードを預けてしまうと、自分のコンテンツだけでなく、マイミクが自分に対して、「友人まで公開」しているプロフィールや日記などにアクセスする権限を与えたことと等価となってしまいます。自分がリスクを負うだけならまだしも、マイミクの全員にそのことを納得してもらうのは難しいですよね。。。また、「パスワードは目的以外に使用することはありません」と但し書きがあり、それが真実だとしても、なんらかのトラブルでそのサードパーティから、mixiのパスワードが流出しないとも言い切れません。そのようなサービスにパスワードを預ける前に、上記のことを思い出していただけますよう、どうかよろしくお願いいたします。

とはいえ、mixiにはAPIでアクセスできないリソースがたくさんあります。
パスワードで認証をおこなうウェブサービスである限り、ユーザビリティを確保しながら、サードパーティからのパスワードによるアクセスを完全に防ぐ手段は現状ありません。ミクシィのエンジニアであるという立場を離れて言うと、サービス提供者は可能な限り、OAuthを利用して、サードパーティが安全に、すべてのリソースにアクセスできるAPIを用意し、そちらの利用を啓蒙していくべきだと考えます。

少し話が脱線してしまいました。
パスワードの扱いについて考えつつ、まずはこの機能でGmail™の友人たちとmixiって(つながって)みてはいかがでしょうか。

最後になりましたが、Google Contacts Data APIの提供と、ブランドの使用許諾をくださいましたGoogle社様には、心より御礼申し上げます。

静かに暮らしたいmikioです。今回は、新進気鋭のDBMであるKyoto CabinetのRubyバインディングを駆使してお手軽にデータベースプログラミングを行う方法について述べます。

Kyoto Cabinetのおさらい

Kyoto Cabinet(KC)は、Tokyo Cabinet(TC)に比べて、最適化された性能よりも保守性を重視したDBMの実装です。オブジェクト指向プログラミングの技法を用いて、少ないコード記述量で容易に機能追加できるように設計しています。また、実装としては、空間効率の向上と並列処理性能の向上を重視しています。以下のプレゼン資料も参考になると思います。

TCでもハッシュ表やB+木などのデータ構造を動的に切り替えて同じインターフェイスで操作するための「抽象データベース」という機構がありましたが、KCでは同じことを「多相データベース(polymorphic database)」という名前の機構として提供しています。なぜ「抽象データベース」という名前を止めたかと言うと、KCでは各種のデータ構造のDBクラスの基底クラスとして純粋仮想関数のみを持つ抽象クラスを導出していて、そちらを抽象データベースと呼びたいからです。多相データベースは、敢えてデザインパターンの用語で言えば、抽象データベースの実装クラスを実行時にデータベース名によって切り替えてインスタンス化するファクトリメソッドを内包したストラテジ的な具象クラスです。

使い方はTCの抽象データベースAPIとほぼ同じで、DBオブジェクトをインスタンス化しておいてから、openメソッドでデータベースを開いた時の名前で実際に開かれるデータベースの構造が決定されます。C++では以下のようなコードになります。

PolyDB db;
db.open("casket.kch", PolyDB::OWRITER | PolyDB::OCREATE);
...
db.close();

上記では、データベース名が「.kch」という拡張子を持つので、ファイルハッシュデータベースが開かれます。もし拡張子が「.kct」なら、ファイルツリーデータベースが開かれます。open、close、set、getなどなどの全てのメソッドはどのデータ構造でも同じインターフェイスで操作できるので、いちいち全てのAPI文書に目を通す必要はありません。抽象データベース(FileDB)のAPIだけ知っていればよいのです。なお、拡張子をつけたくないという人は、「casket#type=kch」などとすれば、「casket」というファイル名のままでハッシュDBを作ることができます。

多相データベースで利用できる具象データベースの一覧を以下に示します。

プロトタイプハッシュDB
ファイル名:「-」そのもの
機能:STLのunordered_map(ハッシュ表)を使ったオンメモリのDB
レコード参照の時間計算量:O(1)
ロックモデル:DB全体のリードライトロック
プロトタイプツリーDB
ファイル名:「+」そのもの
機能:STLのmap(赤黒木)を使ったオンメモリのDB
レコード参照の時間計算量:O(log N)
ロックモデル:DB全体のリードライトロック
キャッシュDB
ファイル名:「*」そのもの
機能:二重リンクリストでLRU消去機能を実装したオンメモリのDB
レコード参照の時間計算量:O(1)
ロックモデル:レコード単位の通常ロック
ファイルハッシュDB
ファイル名:拡張子が「.kch」
機能:ハッシュ表と二分探索木を使ったファイル上のDB
レコード参照の時間計算量:O(1)
ロックモデル:レコード単位のリードライトロック
ファイルツリーDB
ファイル名:拡張子が「.kct」
機能:B+木とLRUページキャッシュを使ったファイル上のDB
レコード参照の時間計算量:O(log N)
ロックモデル:ページ単位のリードライトロック

伝統的なDBMに相当する機能を持つのはファイルハッシュDBであり、これがKCにおいて最も重要な実装になります。TCのファイルハッシュDBでもレコード単位のロックを実現していましたが、新規レコードを挿入する際にDB全体をロックする区間がありました。KCではそれも無くしてほぼ完全なレコード単位ロックになっています。また、PthreadのロックでなくCPUのcas命令を使った自作のロックを使うことでロック自体の性能も向上させています。

次に重要なのがファイルツリーDBで、速度はファイルハッシュDBに劣りますがキーの順序が保証される実装です。TCにおけるファイルツリーDBの実装ではロックモデルがDB全体のリードライトロックでしたが、KCではページ単位になっているところが飛躍的な進化といえるでしょう。また、ページキャッシュはTCでは単一のLRUリストでしたが、KCではInnoDBなどを真似して2段式(中間挿入&格上げ方式)のLRUリストを採用することにより、実際のユースケースでのヒット率を向上させています。

キャッシュDBは文字通りキャッシュとして利用するための実装で、並列性と省メモリ性能に特長があります。プロトタイプハッシュDBとプロトタイプツリーDBは主に比較テストのためにのみ存在する機能ですが、ツリーの方は順序の保持が重要なオンメモリDBとして利用する価値があるかもしれません。プロトタイプDBは純粋なテンプレートとして実装されているので、std::mapに互換する実装であれば何でもスレッドセーフなDBオブジェクトとしてラッピングできるという利点もあります。

Rubyバインディング

KCのRubyバインディングは、上述の多相データベースAPIをRubyから操作できるようにしたものです。つまりKCの全ての機能をRubyから利用することができるということです。C++もRubyもクラスベースのオブジェクト指向言語として共通した機能を持っている言語ですので、KCのAPIも両者でだいたい同じ感じになっています。各種言語においけるAPIの指針はIDL(kyotocabinet.idl)で定義しているので、もし別の言語でのバインディングを書いてくれる方がいらっしゃれば参考にしていただきたいところです。ちなみに、C言語のプログラムからKCを使いたい人のために、多相データベースを extern “C” な関数でラップしたCバインディングもコアパッケージに同梱されています。

本題のRubyバインディングに話を戻します。自分で言うのもなんですが、すごく…使いやすいです。最も基本的なサンプルプログラムは以下のようになります。

require 'kyotocabinet'
include KyotoCabinet

# データベースオブジェクトを作成する
db = DB::new

# データベースを開く
unless db.open('casket.kch', DB::OWRITER | DB::OCREATE)
  printf("open error: %s\n", db.error)
end

# レコードを格納する
unless db.set('foo', 'hop') and
    db.set('bar', 'step') and
    db.set('baz', 'jump')
  printf("set error: %s\n", db.error)
end

# レコードを参照する
value = db.get('foo')
if value
  printf("%s\n", value)
else
  printf("get error: %s\n", db.error)
end

# 全てのレコードを横断的に参照する
class Traverser < Visitor
  def visit_full(key, value)
    printf("%s:%s\n", key, value)
    return NOP
  end
end
db.iterate(Traverser::new, false)

# データベースを閉じる
unless db.close
  printf("close error: %s\n", db.error)
end

上記ではIDLに合わせたインターフェイスしか使っていないので、C++やCや今後現れるだろうその他の言語と同じノリで記述することができます。統一されてわかりやすい反面、Rubyの良さが生かしきれていないとも言えるでしょう。ご安心ください。RubyっぽいAPIも用意してあります。

基本操作の簡略化

データベースを開いたら閉じるというのは極めて重要なことですが、閉じ忘れるというバグは実にありがちなものです。C++ではDBオブジェクトをスタックに貼り付けることでRAIIによる保証を得ることができます。closeしていないオブジェクトが破棄されると暗黙的にcloseされるようになっており、スコープから抜けた時点でオブジェクト破棄されることは言語が保証してくれるということです。Rubyにおいては、DBオブジェクトを作ってファイルを開いてから、最後に閉じるという操作の間に任意のユーザ定義操作を挟むという構造をイテレータで表現することでRAIIっぽくしてみます。

# DBを開いて、何かして、閉じる
err = DB::process('casket.kch') { |db|
  db['nagisa'] = 'kagikakko'
  p db['nagisa']
}
# エラーがあれば表示する
if err
  p err
end

newやopenやcloseがコードから消失しています。DB::processメソッドを呼び出すと、DBオブジェクトを準備した上で、引数のブロックのパラメータに与えて実行します。ブロックが終了したら、正常終了でも例外などの大域脱出でも、必ずDBは閉じられます。ついでに、組み込みのHashクラスと同じように “[]=” メソッドでレコードを格納して “[]” メソッドでレコードを参照していることにもお気づきいただいたと思います。その他にもHashクラスと互換のメソッドを実装していますので、それを使っている既存のコードにKCを適用するのはとても簡単です。

横断操作の簡略化

集合(=データベース)に格納されている要素(=レコード)をひとつひとつ参照すると言えば、DB#eachメソッドです。KCでももちろん対応しています。全レコードの内容を表示するには以下のようにします。

db.each { |key, value|
  printf("%s:%s\n", key, value)
}

さらに便利なことに、KCでは、イテレータでレコードの値を更新することもできてしまいます。DB#iterateメソッドを用います。全レコードの値の小文字を大文字に置き換えるには以下のようにします。

db.iterate { |key, value|
  value.upcase
}

たったこれだけです。DB#iterateはブロックの戻り値を新しい値とみなしてデータベースに更新をかけてくれます。よって、値に対する任意の加工をブロック内で定義することができます。なお、更新したくない場合にはVisitor::NOPという値を返し、そのレコードを消したい場合にはVisitor::REMOVEを返します。

ちょっと複雑になりますが、カーソル(外部イテレータ)も使えます。全レコードの内容を表示するには以下のようにします。

cur = db.cursor
cur.jump
while cur.accept { |key, value|
    printf("%s:%s\n", key, value)
    Visitor::NOP
  }
  cur.step
end

カーソルでもブロックの戻り値を新しい値とみなして更新をかけることになっているので、レコードの値に対する任意の加工を行うことができます。NOPとREMOVEについても同じです。なお、内部イテレータとカーソルの使い分けですが、内部イテレータは一連のイテレーション操作をアトミックに行いたい時に使い、カーソルはアトミックでなくてもいいから複数のイテレーション操作を並行して走らせたい時に使うということになります。

Visitor風インターフェイスの詳細

既に横断検索のところで話題に出てしまっていますが、ユーザが定義したブロックを呼び出してレコードの値の参照や更新を任意に行わせるという操作ができるというところがKCのセールスポイントです。元来はVisiotorというインターフェイスを実装したオブジェクトをDB#acceptメソッドに渡すという以下のようなAPIになっています。DB#iterateやCursor#acceptも同様です。

class Visitor
  NOP = 0
  REMOVE = 1
  def visit_full(key, value) end
  def visit_empty(key) end
end

class Cursor
  def accept(visitor = nil, writable = true, step = false) end
end

class DB
  def accept(key, visitor = nil, writable = true) end
  def iterate(visitor = nil, writable = true) end
end

各メソッドで、visitorがnilの場合にはブロックパラメータが呼び出されます。ブロックにはレコードのキーと値が渡されます(該当のレコードが無い場合には値はnilになる)。writableはDBの内側のコードでリードライトロックの判断に用いられるもので、コールバックによって更新が発生しうる場合には真、更新が絶対に発生しない場合には偽にします。stepはカーソルを自動的に進めたい場合に真にします。なお、C++ではvisitorとして渡すオブジェクトはVisitorクラスを継承している必要がありますが、Rubyではduck typingに基づくので継承関係は必要ありません。通常パラメータとして与える場合にはvisit_fullやvisit_emptyを備えるオブジェクトを、ブロックパラメータとして与える場合にはkeyとvalueを受け取るブロックを指定すればOKです。

暗黙の型変換

KCではレコードのキーと値は単なるバイナリ(C/C++ではcharの配列)であり、RubyではそれをStringクラスとして表現しています。ただ、キーや値に指定するデータを全て明示的にStringクラスのオブジェクトに変換しているとかなり面倒なので、多くのクラスを暗黙的に変換してくれます。以下のコードはいずれもそれっぽく動作します。trueは “true”、falseは “false”、nilは空文字列に変換されます、

db["tako"] = "ika"                   # String
db[:kyoto] = :cabinet                # Symbol
db[12] = 34                          # Fixnum
db[12345678901234] = 98765432109876  # Bignum
db[12.34] = 56.78                    # Float
db[true] = false                     # TrueClass/FalseClass
db[nil] = nil                        # NilClass

障害対応とトランザクション

開いたデータベースを閉じないでプロセスが終了すると、データベースが壊れたり書いたはずのレコードが消失したりするリスクがあります。RAII的な手法を使えばcloseを呼び忘れるというバグは無くすことができますが、シグナルでプロセスが殺されたり電源断でマシンごと落ちたりすればやはり閉じ損ねが生じることになります。TCではそのような事態においては「データベースが壊れた」というエラーを提示してユーザに明示的な修復を求めることにしていました。たとえ全てのレコードが生存していたとしても、正常に終了しなかった場合は破壊とみなして明示的対応を求めるということです。一貫性が失われているかもしれない状態で通常運用を続けるのはよろしくないという判断で敢えてそうしています。

しかし、自分でマシンを落としておきながらヒステリックな反応をする人が結構多いようなので、KCのデフォルトではデータベースが壊れた場合にも勝手にベストエフォートで修復して通常運用を続けるようにポリシーを変更しました。open/closeの呼び出しで上げ下げされるフラグをDB内で管理し、起動時に前回のcloseが検出されない場合には、fsckのようにDB全体をスキャンして整合性を回復させます。このような動作が望ましくない場合には、openメソッドのmodeパラメータに DB::ONOREPAIR を加えてください。

というか、「そもそも壊れるなよ」と言いたい人もいるかと思います。closeで更新操作のdurabilityを確定させるのではなく、setやremoveなどの呼び出し毎にdurabilityを確定させたい場合には、自動トランザクション機能を使ってください。これは、各々の更新操作の前に更新内容のログをとるWAL(write ahead logging)という手法でdurabilityを保証するものです。ただし、WALを書くためのオーバーヘッドによって性能が半減することは避けられません。自動トランザクションを有効にするにはopenメソッドのmodeパラメータに DB::OAUTOTRAN を加えてください。さらに、ファイルシステムが不整合を起こした場合に備えて DB::OAUTOSYNC を加えることもできます。これはDBファイルやWALファイルの更新の度にfsyncシステムコールでディスクとの同期をとるものです。むちゃくちゃ遅くなりますが、単一マシンでのdurabilityを最大化させたければこの機能を利用してください(単一マシンでのdurabilityにそこまで重きを置くかはよく考えてほしいところですが)。

自動トランザクションは毎回の更新操作で暗黙的にコミットされるトランザクション機能ですが、任意の操作群をまとめてコミットしたり、ロールバックさせたりしたいこともあるかと思います。その場合は明示的にトランザクションを実行することもできます。begin_transactionでトランザクションを開始して、end_transactionで終了します。end_transactionの引数が真の場合、トランザクションの更新内容がコミットされて確定します。偽の場合、トランザクションの更新内容は破棄されて、トランザクション開始前の状態にロールバックされます。KCではトランザクションを実行できるのは同時に1スレッドだけですので、isolationレベルはSerializable相当ということになります。

db.begin_transaction
db["tako"] = "ika"
db["uni"] = "ikura"
db.end_transaction(true)

開始したトランザクションは必ず終了させたいわけですから、例によってイテレータでそれを保証した方がいいですよね。もちろんサポートしてますよ。transactionメソッドに与えたブロックの実行前にトランザクションを開始し、ブロックの戻り値が真の場合はトランザクションはコミットされ、偽の場合は破棄されます。

db.transaction {
  db["tako"] = "ika"
  db["uni"] = "ikura"
  true
}

例外は不採用

Rubyにおいては、モジュール内で起きたエラーを例外として報告してそれを呼び出し側で補足してエラー処理を行うというスタイルが一般的ですが、KCではエラーは例外ではなく戻り値で報告するようにしています。TCと同じく、IDLに基づいて言語間で統一的なインターフェイスを提供したいというのがその理由の第一です。CやLuaやPerlなど例外機構のない言語でも同じように利用できるようにしたいし、C++で例外安全でないライブラリとも共存させたいということです。

さらに、例外になるエラーとそうでないエラーの区別を仕様書にイチイチ書かなきゃならないの煩雑で嫌だというのが第二の理由です。DB#getが返すrecord not foundエラーを例外にすべきか、であればDB#openが返すfile not foundエラーやpermission deniedエラーはどうなのか、DB#casやDB#incrementのconflictエラーはどうなのか、といったあたりの議論とそれに基づく仕様変更が面倒なので、一貫して全て例外でないということにしました。

メモリ使用量対決

TCの抽象データベースでも好評でしたが、言語組み込みのHashクラスの代替として省メモリな連想配列としてDBMを使うというのも一興です。そこで、言語組み込みのHashクラスとKCの各種データベースとで時間性能とメモリ使用量を測ってみました。「00000000」から「00999999」までの100万レコードの格納にかかった時間とメモリ使用量を比べてみます。

経過時間 メモリ使用量 ファイルサイズ
組み込みHash 7.018 秒 177.41 MB -
プロトタイプハッシュDB 3.442 秒 135.12 MB -
プロトタイプツリーDB 6.802 秒 157.55 MB -
キャッシュDB 2.648 秒 74.05 MB -
ファイルハッシュDB 3.418 秒 41.77 MB 36.52 MB
ファイルツリーDB 3.250 秒 61.06 MB 19.00 MB

ということで、省メモリな連想配列が欲しければ、まずKCのキャッシュDBを使ってみてください。速度もメモリ使用量もお得です。それより少し遅くてもいいからもっとメモリ使用量を減らしたい場合には、データの特性に応じてファイルハッシュDBやファイルツリーDBを使ってみてもよいでしょう。

チューニングパラメータ

openメソッドに渡す名前でデータ構造を選択できることは既に述べましたが、名前の後に「#」で区切ってチューニングパラメータを指定することもできます。各データ構造でサポートされるパラメータを以下に示します。

プロトタイプハッシュDB、プロトタイプツリーDB
なし
キャッシュDB
bnum, capcount, capsize
ファイルハッシュDB
apow, fpow, opts, bnum, msiz, dfunit, erstrm, ervbs
ファイルツリーDB
apow, fpow, opts, bnum, msiz, dfunit, erstrm, ervbs, psiz, rcomp, pccap

それぞれのパラメータの意味を以下に示します。サイズを指定する際には「10m」のように接尾辞で単位を指定することもできます。

  • bnum : ハッシュ表のバケット数。デフォルトは100万くらい。格納するレコードの総数の2倍くらいがオススメ。
  • capcount : 格納するレコード数の上限。溢れたはLRUなものから削除される。デフォルトは制限なし。
  • capsize : 格納するレコードサイズの合計の上限。溢れた分はLRUなものから削除される。デフォルトは制限なし。
  • apow : レコードのアラインメント。2の冪で指定。デフォルトは3(ハッシュ)か8(ツリー)。
  • fpow : フリーブロックプールのサイズ。2の冪で指定。デフォルトは10。
  • opts : “s”(small)、”l”(linear)、”c”(compress)を含めた文字列。デフォルトはなし。
  • msiz : mmapする領域のサイズ。デフォルトは64MB。
  • dfunit : 動的デフラグの単位。デフォルトはなし。動的デフラグをしたい場合は8くらいがオススメ。
  • erstrm : エラーレポートの出力先。”stdout” か “stderr”。デフォルトはなし。
  • ervbs : エラーレポートの冗長フラグ。”true” か “false”。デフォルトは偽。
  • psiz : B+木のページサイズ。デフォルトは8192バイト。4096でもOK。
  • rcomp : B+木の比較関数。”lex”(lexical)か “dec”(decimal)。デフォルトはlexical。
  • pccap : ページキャッシュのサイズの合計の上限。デフォルトは64MB。

大抵の値はデフォルトのままでOKですが、bnumだけは設定した方がよいでしょう。また、巨大なデータベースを扱う場合、メモリに余裕があるならmsizやpccapを増やすと性能が劇的に改善します。1億レコードを格納する10GBのファイルハッシュデータベースを作るなら、”casket.kch#bnum=200000000#msiz=16g” などとすればよいでしょう。

Rubyスレッド対応

まず現状を説明すると、KCのRubyバインディングは、Ruby 1.8系ではスレッドセーフではありませんが、Ruby 1.9系ではスレッドセーフです。ここで言うスレッドセーフとは、Rubyレベルのスレッドに対してのことです。KCはネイティブスレッド(POSIXスレッド)に対してはネイティブのロック機構やTLS(スレッド局所記憶)を用いてDBオブジェクトの排他制御を行うことによってスレッドセーフを達成しています。しかし、1.8系におけるRubyスレッドは単一のネイティブスレッド上でユーザ層のコンテキストを切り替えて動作する仕組みであり、ネイティブのスレッド関連機能が働かないために、必ずしもRubyスレッドセーフにはならないのです。このあたりの問題は最近発覚して調査やテストをしているところで、おそらく私の理解が甘いところが多いので、識者のご意見を賜りたく存じます。

1.9系はRubyスレッドとネイティブスレッドが1対1で対応するっぽいので、ネイティブのロックやTLSが期待通りに動作すると思われます。しかし、KCではDB#acceptメソッドなどでRubyコードをコールバックする関係で、KCのクリティカルセクションの中でRubyのGVL(Global Vm Lock。一般にはGIL)が開放されて別スレッドが同じクリティカルセクションに突入しようとしてデッドロックする問題があります。よって、現状では全てのAPIをRubyのMutexクラスによるクリティカルセクションで保護してデッドロックを防いでいます。この問題に関しては、Thread#criticalに相当する機能でDB#acceptなどを保護することで対応できると思っていますが、1.9ではThread#criticalが廃止されているのでそれに相当する機能が使えるか否かを調査しているところです。

上記が解決するかDB#accept等を廃止するかした場合、1.9系では、ネイティブのスレッド関連機能に任せてスレッドセーフを達成できるため、ネイティブコードを実行している最中にはGVLを外すことができると思われます。1.9系においてもGVLによってRubyコードの並列性はないわけですが、ボトルネックになることが多いIO関連機能の処理をGVL外で実行することによってプログラム全体のスループットを上げることが期待できます。DBMはIO関連機能の典型なのでこの手法がまさにうってつけだと思っています。thread.cのrb_thread_blocking_regionが使えるかどうか検討しています。

で、とりあえずRubyのMutexでKCのDBオブジェクトを保護している現状では1.8系でもスレッドセーフだと言えるんじゃないかと思ったのですが、それは早計でした。KCのエラーコードはネイティブのTLSで管理しているのですが、1.8系ではそれをRubyのTLSにコピーする必要があるようです。1.8系と1.9系で実装をかき分けたり、効率が悪い方に合わせて効率が良い方にオーバーヘッドを強いるのはちょっと心苦しいこともあり、現状では1.8系のスレッドセーフ性は放置ということになっています。現状でTLSのコピーをして1.8対応したとしても上述の1.9向けの最適化をする足かせになってしまうので、1.8系スレッド対応の優先度は低いというのが正直な気持ちです。

まとめ

Kyoto CabinetのRubyバインディングの使い方と内部事情について簡単に説明しました。まだまだインターフェイスや実装に改良の余地があるとは思っていますが、実用できるレベルにはなってきたと思っています。実際に自分で使ってみた限りではかなり快適で性能も悪くないので、皆様にもぜひお試しいただければ幸いです。今後は1.9向けの最適化を手探りでも進めていこうと思っています。

蛇足ですが、「あんたのところPerlの会社じゃないの?」というツッコミに対するレスを予め示しておきます。もちろんPerlバインディングも作る予定なのですが、バインディングシリーズの最初に取り組むのは回避したということです。PerlのXSを書くのは興味深い仕事ではあるのですが、ルールが多くて神経をすり減らす面もあり、設計と実装を同時に進めると両方が疎かになってしまうと判断しました。そして、私のスキルセットにあるスクリプト言語(Perl、Java、Ruby、Lua)の中で最もバインディングを作りやすいRubyにまずは取り組んだというわけです。先にRubyでインターフェイスの設計とC++側の実装を整えておけば、その他の言語でも比較的楽に作業を進めることができるでしょう。急がば回れ。急いては事を仕損ずる。

はじめまして。開発部じゃない加藤和良です。

最近、mixi では Buildbot をつかった継続的インテグレーションをはじめています。安定版の mixi のソースコードにコミットすると Buildbot がそれを検知し、自動的にテストが走るようになりました。

ここでの「テスト」は Test::Simple や prove(1) をつかった、Perl でかかれた開発者テストを指しています。mixi の開発者テストをとりまく環境は、ここ数年でかなり改善されました。今回はその歩みをふりかえりながら、テストの無いコードベースをどこからどうやって変えていったかという話をしたいと思います。

開発環境

はじめに、前提となる mixi の開発環境について説明します。mixi では複数人の開発者がひとつのマシンで作業を行います。それぞれの開発者は、あらかじめ割り当てられたポートで Apache を起動し、自分の好きなエディタでソースコードを編集しながら生活しています。

開発環境

memcached は各マシンに一台。そして、沢山の MySQL 群や、数台の Tokyo Tyrant, Shindig などは社内のほぼ全員が同じものを共有しています。

当然のことながら、これらのサーバーは皆さんがお使いの 本番の mixi で使っているものとは、別のものです。以下で「日記」とか「ボイス」とか書いてあるのは、開発者が開発環境でつくっているデータをさすことに、あらかじめ注意してください。

2007年4月 – t/ の追加と当時の問題

mixi のソースコードは以下のように配置されています。

lib/, t/ があるのは CPAN モジュールでもありがちですが、t/ の下に lib/ と同じような階層構造があるのは、ちょっと珍しいかもしれません。

  • /
    • lib/
      • Mixi/Application/Object.pm
    • t/lib/
      • Mixi/Application/Object/leave.t

この t/ フォルダは、2007年4月ごろにレポジトリに追加されました。ここから mixi の開発者テストの歴史がはじまるわけですが、当時のテストまわりにはいくつかの問題がありました。

一般に、テストを書きやすいコードは、以下のような性質を持っていると思います。

テストしやすい

  • 結果を左右する入力をどこから与えるべきかわかりやすい
  • 結果がどこに出力されているかわかりやすい
  • それ以外の副作用が無い

たとえば、文字列のうち HTML 上で特殊な意味をもつものをエスケープする関数や

is(escape('foo & bar'), 'foo &amp; bar');

スタックを表現するクラスなどは「テストを書きやすい」部類のコードと呼べます。

my $stack = Stack->new;
ok(! $stack->pop);
$stack->push(123);
is($stack->pop, 123);

しかし、当時の mixi のコードベースには、前述の性質をそなえていないものが多くありました。

  • 入力として、どこかの環境変数を読んでいたり、どこかのデータベースの情報を読んでいたり、new しづらいクラスを使っている
  • 出力が標準出力への print だったり、データベースのどこかへの更新や削除だったり、例外処理で exit してしまったりしている
  • 副作用としてどこかのデータベースや memcached に更新や削除を行ってしまう

結果、テストを書くのがむずかしかったり、せっかく書いたテストが、誰かが日記を消したり、マイミク関係を解消したのをきっかけに突然失敗するようになったり、テストを実行したらどこかのボイスの投稿が消えるかも、みたいな不安で他人が書いたテストを動かしづらかったり、といったことが起こりがちでした。

2008年12月 – Mixi::Test::Fixtures

これらの問題を解決するべく、2008年12月ごろ Mixi::Test::Fixtures という新しいライブラリが追加されました。

Mixi::Test::Fixtures は、RailsDjango にある “fixture” の仕組みを、mixi のコードベースに導入するためのライブラリです。具体的には、以下のような動作を行います。

  1. テストの実行直前に、一時的なデータベースを作成
  2. 指定された YAML から、データベースに値を挿入
  3. テストの実行
  4. テストの実行直後に、いろいろ変更のあったデータベースを削除

mixi は OR マッパを使っていない部分もいろいろあり、手書きの SQL が MySQL を期待しているところが多々あります。そのため、一時的なデータベースにも SQLite のようなインプロセスのデータベースは使わず、MySQL をそのまま使っています。テストの実行前後に CREATE DATABASE / DROP DATABASE が走るのはなかなか富豪的です。

Mixi::Test::Fixtures の導入によって

  • テストが、いつ変わるかわからないデータベース上の情報に依存している
  • テスト実行後にデータベース上に様々なデータが作成・削除され、2回目以降の実行では前提となる条件が変わってしまう

といった問題は回避できるようになりました。

その後、何度かのバージョンアップがあり、現在ではデータベース以外に

  • memcached
  • メールの送信
  • 外部への HTTP アクセス

も Mixi::Test::Fixtures で差し替えが可能です。これらの動作を模倣するのはデータベースに比べるとずっと簡単なので、インプロセスで代替のコードが動くようになっています。

2009年4月 – ブラックリストの導入

Mixi::Test::Fixtures 導入以後、新しいテストについては、だれでもいつでも安心して実行できるようになりました。ただ、古いテストのなかには、まだ fixture への移行がすんでいないものもあり、気軽に実行するのは不安がありました。

一方で、すべてのテストを実行したいという要求もありました。コードの変更が予想外の部分をこわしていないかを確かめるためには、なるたけ多くのテストを実行したほうが安心です。

この中途半端な状態をなんとかするために2009年4月ごろ導入されたのが、ブラックリストと、それを使って「ホワイトな」テストを列挙する仕組みです。

まず t/blacklist.yaml に

patterns:
 - "^t/lib/Mixi/Official/Member/.*$"
files:
 - t/lb/Mixi/Application/DB/Ad.t
...

と、だれもが気軽に実行するのがまだ難しいテストを列挙していきます。これを元に list-tests というスクリプトが

% ./script/devel/list-tests
t/lib/Mixi/Application/Object/leave.t
...
%

t/ 以下をなめて blacklist.yaml にマッチしないテストの一覧を出力します。これを

% prove -lv `./script/devel/list-tests`
....

といった感じで prove(1) にわたすことで、誰もが何時でも実行できるテストだけを、簡単に実行することができます。列挙までして prove(1) の実行は行わないのは

% prove -lv `./script/devel/list-tests | grep Application`
....

のように他のソフトウェアとの連携を考えてのものです。

ブラックリストの導入に関しては

  1. 実行に注意が必要なテストはだめなので、修正するか消す
  2. t/, xt/ などテストをいれるフォルダを二つに分ける
  3. 個々のテストに plan skip_all => ‘…’; みたいなものを記述する

というやりかたもあったと思います。

ただ、1 は修正が完了するまでテストの全実行をあきらめなくてはいけないこと、かといって締切を設定し、それまでに修正されないテストは消す、となるとテストが消えすぎること、そもそも「実行に注意が必要なテストはだめ」というのが微妙で、書いた人しか実行できないテストでもないよりはマシなことを考えて、無しになりました。

2 は、ファイルの移動に Subversion が弱いことと、2つの分割ですまなくなったときにどうしよう、というのが不安なところでした。

3 については、柔軟で良さげなので、将来的にはここに落ち着くんじゃないかと思っています。いまになって思えば、最初からこれでも良かったかもしれないですね。

2009年5月 – Test::Apache2

Mixi::Test::Fixtures とブラックリストは mixi 全体にかかわる部分でした。ここからは、各々のコードに関わる細かな部分に目をむけてみようと思います。

Test::Apache2 は mod_perl ハンドラのテストを書くためのライブラリです。目的だけみれば Apache::Test によく似ていますが、本物の Apache を立ち上げないでインプロセスで動くところが大きなちがいです。mod_perl 特有の細かな状況 (具体的にはあんまり把握していません) についてテストを書くのには向きませんが、反面、実行速度が速く準備も簡単です。

Test::Apache2 は2009年の5月ごろに mixi の安定版で使えるようになりました。現在では mixi から切り出して CPAN 上で配布しているので、mixi の外でも使えます。

既存の mod_perl ハンドラの挙動をとりあえずテストで囲いたい、みたいなときに、ちまちま Test::MockObject で Apache2::RequestRec を模倣するよりは便利だと思うので、ぜひ試してみてください。

2009年12月 – Test::Exit

Test::Exit は exit の呼び出しをテストするためのライブラリで、Andrew Rodland さんが開発されています。

...
exits_nonzero {
  invalid_op(); # なかで exit(1) する
};
done_testing;

こんなふうにつかえます。mixi では例外処理のなかで exit してしまう CGI ノリのコードがごくまれにあり、そういうものに対処するために2009年12月ごろにつかいはじめました。

Test::Exit には当時 exit($status); のみ対応していて exit; のような引数なしの呼び出しをうまく扱えないバグがあったのですが、これはパッチを送って 0.03 で直してもらいました。

こういうごくニッチなところにも先人がいるのは、CPAN というか Perl のすごいところだなあと思います。

まとめ

というわけで、mixi の開発者テストのいままでの歩みをふりかえってみました。今年に入ってからは Buildbot の設定をいろいろ詰めているのですが、これはまだ固まりきっていないので、紹介はまた次の機会に。

Buildbot

mixi の開発者テストまわりの仕組みは、基本的に「テストのことを考えずにだらだらコードを書いても、後から挽回できるように」できています。できているというか、そうせざるをえなかった、というのが実際のところです。

そのため、ださいところや、無理矢理なところも多々ありますし、実行速度も結構遅かったりします。ただ「テスト書きたいのは山々なんだけど、既存コードに手をいれるのはきつくて、次の機会をうかがっている」というありがちな状況からは一歩抜け出せました。「おれ、新しいフレームワーク (or ライブラリ or 言語) がきたら、次はテスト書くんだ」なんてのは、良くない類のフラグを立てているだけであると、個人的には思います。