5月15日に、mixi&OpenSocial-Japan主催OpenSocial Hackathonが開催されましたので、ここで簡単にレポートをしたいと思います。会場は渋谷にあるGoogleの一室をお借りしました。

Hackathonの様子

Hackathonはグループに分かれ、グループごとにひとつの作品を作り上げる過程で開発を体験して頂く、という趣旨で行われます。今回は総勢19名、6チームに分かれて開発を行いました。参加者の皆さんは、事前ミーティングでIdeathonと呼ばれるチーム分けやアイディア出しを行っていたため、Hackathon当日は開発作業に集中していました。とにかく時間との戦いです。今回も、Google API ExpertやGoogleのエンジニアの方、そしてミクシィからも数名がチューターとして参加しました。

チームと席順と落書き

では、ここで各チームの成果を見ていくことにしましょう。

宝探し – 位置&Tutorialチーム

PCと携帯電話を連携させた「宝探し」アプリです。まず目を引くのは、Google Mapsですね。予め、地図上で宝の位置を決めておきます。携帯電話を持ったユーザが、そのユーザがいる付近をカメラで撮影してメール送信します。すると、宝がある位置からの距離が携帯電話に返ってきます。それを繰り返すことで、宝を見つけます。PCからそのアプリを見ると、地図上に次々とマイミクがいる位置と写真が集まってきて、地図上にマーカーとして現れます。マイミクたちの行動を把握できるわけです。

PCでのOpenSocialアプリを超えたアイディアですね。今後はこのような携帯端末でもソーシャル化が進んでいくことを予感させるアプリです。

rimg3455

rimg3456

Event Calendar – 情報共有Aチーム

例えば友達と何か予定を調整するのは、ちょっとした難しさがあります。この「Event Calendar」アプリは、そんな調整作業を助けてくれます。このアプリの面白さは、予定自体を決めていく過程をサポートする、というアイディアにあります。いつ、どこで、何を、といったことを順次アプリ上で入力していくことで、予定が具体的に決まっていきます。

参加人数やコメントなども書くことができるので、ソーシャルアプリケーションの代表例と言えるでしょう。

rimg3458

rimg3460

みんと~ – 投稿チーム

いろいろなコンテンツを友達間で投稿しあって、星をつけたりコメントしたりすることができるアプリが「みんと~」です。Hackathonの時間内は、メッセージの投稿のみの実装でしたが、ゆくゆくは画像や動画などを投稿できると、より面白さがアップするでしょう。

面白いところとして、投稿されたメッセージは、ホームページやプロフィールページにて「流れるように」表示されるところでしょう。こういった、ちょっとしたアイディアも、OpenSocialアプリでは非常に有効です。

rimg3471

rimg3474

FEED Apps – フィードチーム

YouTube、Flickr、Amazonなどのエントリを友達と共有するアプリです。画像や動画などをマッシュアップしたアプリを作れることを証明したチームと言えるでしょう。Hackathon時点では、URLを手入力していましたが、各フィードを読み込んで手軽に選択できるUIが備われば、より使われるアプリになると思います。

このチームでは、Tumblrライクなアプリを目指していたようです。かなり近いものになっているのではないでしょうか。

rimg3486

rimg3495

FINAL QUESTION – ゲームチーム

ゲームチームによってクイズとソーシャルのマッシュアップアプリ「FINAL QUESTION」が生み出されました。ゲームの内容は、

・敵は強すぎて普通では勝てない(理不尽な問題ばかり)。負けると友達に通知される。
・最後の姫はとても我がままであり、なかなか満足させられない。満足させられないと即死亡。
・何回死んだか、何回クリアしたかなどを、友達と競える。

となっています。難しすぎる問題が、逆にゲームバランスを良くしています。また、クイズという題材を選択した結果、SNSの特徴である非同期性をうまく使うことができています。

クイズは外部サービスから取得するように実装されていたようです。非常にレベルが高いアプリです。

rimg3501

rimg3515

トゥルースオブ県 – 情報共有Bチーム

SNSのユーザは、日本の様々な県に住んでいます。そして各県には、独特の方言があります。「トゥルースオブ県」アプリは、方言の面白さを取り入れたソーシャルアプリケーションです。ある問いかけに対する返答について、ある方言に限定して友達などに投稿してもらいます。方言は決まったものではなく、人によって正解が微妙に異なるところに面白さがあります。ある投稿に関して「それは関西弁じゃない」などとコメントをすることができ、方言が自然とユーザのアクションが増えていく仕掛けとなっています。

ランキングなども表示されますが、その集計のためにバックエンドとしてRuby on Railsで作られたサーバプログラムも開発したようです。アイディア的にも技術的にも、非常に質の高いアプリケーションでした。

rimg3566

rimg3571

いかがでしたか?どれも面白いアプリケーションばかりです。これらは、たった1日のHackathonで作られたものです。ちょっとしたアイディアとチームによる開発力が合わされば、これらのような成果を生み出すことができます。参加された皆さまは、非常にエキサイティングな時間を過ごしたことでしょう。

これらのアプリは、基本的にmixiアプリとして最終的には動作確認が取れています。これをお読みのあなたも、ぜひmixiアプリを開発してみてください。その第一歩を踏み出すために、mixi Developer Centerにお越しください。

最後に、OpenSocial Hackathonは(主催は変わるかもしれませんが)基本的に毎月開催されています。次回は、Google Developer Day 2009 Japanイベントの一環として行われます。すでに参加募集を開始していますので、このフォームよりぜひ応募してみてください。

7月以降も、mixi主催OpenSocial Hackathonの開催を予定しています。お楽しみに!

ノートPCの冷却ファンがうるさいのを対処しようとしてWebで調べたら、そのファンの設計者が「静音性へのこだわり」を語ったページにたどり着いて複雑な心境のmikioです。今回は、Tokyo Cabinet(TC)の最新バージョンで実装された動的デフラグ機能について長々と説明します。

断片化とデフラグ

任意のサイズのデータを管理する記憶装置においては、利用可能領域の断片化(fragmentation)の問題が常につきまといます。ファイルシステム上で任意のサイズのファイルを管理する際にも、データベースファイル内で任意のサイズのレコードを管理する際にも、C言語のmalloc/free関数群でメモリの管理をする際にも、様々なレイヤで断片化が起きうるのです。なぜなら、データを削除もしくは移動した際の空き領域を再利用するにあたって、その領域と同じサイズのデータが常に入ってくるとは限らないからです。特にデータのサイズが変わるような更新操作を頻繁に行う場合や、サイズがまちまちのデータの追加や削除を頻繁に行う場合には、断片化の問題が顕著になります。断片化が進むと占有領域がどんどん肥大化してしまい、性能が悪化したり、それ以上データが入れられなくなったりといった不都合が起こることになります。

tcfragment

断片化を解消するには、使用している領域を連続した場所に詰めて再配置するとともに、空き領域も連続させて再配置して再利用可能にすることが必要です。そのような操作を「デフラグ(defragmentation)」と呼びます。Windowsのコントロールパネルなどからファイルシステムのデフラグをかけた経験のある方も多いと思います。メモリ管理のレイヤでは「コンパクション」と呼ばれる機能として実装されています。TCでも従来からデフラグ機能を最適化という名目で実装しています。

tcdefrag

なお、TCには従来から「フリーブロックプール」という機構があります。空き領域のリストを管理して、新たにレコードの領域を割り当てる際に、空き領域の中で要求サイズ以上の最小の領域を再利用することで断片化の影響を最小化するものです。割り当てた領域と空き領域の差分(余り領域)が一定サイズ以上であればさらに空き領域としてリストに還元するといった工夫もしています。しかし、そこまでしても、固定長でなく任意のサイズの領域を割り当てようとする時点で、余り領域が各所に発生するのは避けられず、断片化は起きてしまうのです。

静的デフラグと動的デフラグ

一般的に言って、ファイルシステムやデータベースの文脈でいうところの「デフラグ」ないし「最適化」という操作は、その操作をしている間にその記憶領域全体が掌握されて、他の操作ができなくなることが多いです。このような静的かつ大域的なデフラグ操作をここでは「静的デフラグ」と呼ぶことにします。それに対して、記憶領域全体を掌握することなく、他の操作と並行に行われるような局所的なデフラグ操作を「動的デフラグ」と呼ぶことにします。TCのoptimize系メソッドは静的デフラグですし、MySQLなどのRDBMSで使われるoptimize文も静的デフラグです。一方でZFSやBtrfsなどの新しめのファイルシステムでは動的デフラグを実装しているようです。

静的デフラグの実装は比較的単純で、他スレッドの更新操作をブロックしてしまえばレースコンディションにまつわる複雑な問題に悩まされずに済むので、結果的に効率的な処理を実現することが容易になります。しかし、デフラグをかけている数秒から数分の間に更新操作ができないというのは、24時間365日稼働のWebサービスのバックエンドに適用するには辛いものがあります。RDBMSの場合、デュアルマスタのレプリケーション構成でアクティブマスタを切り替えながら最適化をかけるといったワークアラウンドもありますが、できればそういう面倒は避けたいものです。一方で、動的デフラグであれば、アベイラビリティの問題を特に意識することなく、ストレージのサイズとパフォーマンスを一定に保つことができます。

ということで、TCに動的デフラグを実装してTokyo Tyrant(TT)から利用可能にすることで、24*365対応が必要な皆様のハートにアピールしてしまおうという魂胆です。実装が複雑になりがちですが、それだけの価値があるでしょう。

動的デフラグの実装

レコードの使用領域と空き領域が混在している状態が断片化であるわけですから、それを解消するには、使用領域は使用領域だけで集めて連続した位置に詰めて配置し、空き領域は空き領域で集めて連結して一つの領域にしてフリーブロックプールに入れるという操作を行えばよいことになります。

TCでは、ハッシュデータベース内部に動的デフラグ用のイテレータを備えています。データベースを開いた際にはイテレータはデータベースの先頭に配置され、動的デフラグの遂行が指示される度に、イテレータの位置の領域を読み込みます。読み込んだ領域がレコードであればイテレータを次の領域まで進めます。読み込んだ領域がフリーブロックであれば、次のレコードを読むまでフリーブロックを読み飛ばし、そのレコードをイテレータの位置に移動させて、イテレータをその直後まで進めます。レコードが元々あった領域は空き領域になり、かつ既存のフリーブロックと隣接しているので、連続したフリーブロックを連結した領域とみなし、それをフリーブロックプールに登録します。

tcdefragstep

動的デフラグ用イテレータがファイルの末尾まで到達した場合、直前のレコードの末尾をファイルの終端とみなしてtruncateしてファイルサイズを縮めるとともに、イテレータをファイルの先頭まで戻します。末尾まで到達しなかった場合でも連結されたフリーブロックがフリーブロックプールに戻されて再利用されるので、以降の更新操作で肥大化が起きる確率をどんどん下げていくことができます。

なお、B+木データベースやテーブルデータベースはハッシュデータベースの上に実装されているので、ハッシュデータベースの機能追加の恩恵は自動的にB+木データベースやテーブルデータベースにも波及します。固定長データベースはそもそも断片化が起きないのでデフラグという概念もありません。

使い方

まず、TCおよびTTの最新版をインストールしてください。それで、TCの各種データベースオブジェクトを開く部分のコードに、以下のような自動デフラグの設定を追加してください。ハッシュデータベースでもB+木データベースでもテーブルデータベースでも使い方はほぼ同じです。

TCHDB *hdb = tchdbnew();
tchdbsetdfunit(hdb, 8);  // 必ずopenの前に呼ぶ
tchdbopen(hdb, "casket", HDBOWRITER | HDBOCREAT);

上記でtchdbsetdfunit関数に与えている引数「8」は、断片化を8回検出したら、それに相当する領域を回収するくらいの動的デフラグ操作を暗黙的に行うという意味です。動的デフラグといえども、その操作を行っている間は該当する領域をロックせざるを得ないので、その処理の粒度を指定することは比較的重要です。「1」などとして粒度を小さくすれば並行性は上がりますが、一度に処理できる領域が限られるので時間効率と空間効率は下がります。逆に「128」などとして粒度を大きくすれば、一度に処理できる領域が増えて時間効率と空間効率を上げられますが、その間に他の操作がブロックされる確率も上昇します。私の経験では、ハッシュデータベースでは8くらい、B+木データベースでは2くらい、テーブルデータベースでは4くらいにすると丁度良いような気がしています。なお、デフォルトでは自動デフラグ機能は無効化されています。

抽象データベースでは以下のようにopenの際にパラメータを指定します。抽象データベースから使えるということはTTからも起動時に指定するだけで使えるということですね。楽ちんです。

TCADB *adb = tcadbnew();
tcadbopen(adb, "casket.tch#dfunit=8");

断片化を検出する度にデフラグを少しずつ進めるというのは多くのユースケースで妥当な選択なのですが、高負荷なWebサービスのバックエンドに用いる場合は事情が異なります。例えばmixiの場合、午後11時から日付変わって午前1時までのいわゆるピークタイムの負荷は、それ以外の時間帯の負荷に比べると3倍もしくはそれ以上になります。ピークタイムの負荷がバックエンドに対する要求スループットを決定するので、ピークタイムには余計な処理をしたくないというのが本音です。デフラグなんてのは負荷の比較的低い時間帯にちょこちょこやれば十分でしょう。ということで、任意のタイミングで明示的に動的デフラグ操作を進められるようにもしています。

tchdbdefrag(hdb, 8);

暇な時間帯であれば、更新操作を行った後に上記の関数を呼ぶというのもよいし、別スレッドを立てて継続的に上記の関数を呼ぶというのでもよいでしょう。例によって「8」は、8個分の空き領域を回収するという意味です。ここで「0」を指定すると、データベース全体を動的にデフラグします。全体にやったら静的デフラグじゃないかと言われそうですが、微妙に違います。排他制御のロックをかけたり外したりを繰り返しながら休み休みで処理を進めるので、見掛け上、他のスレッドがブロックされないのです。

性能

動的デフラグの性能を見るための恣意的なテストプログラムを実行してみました。アラインメントやフリーブロックプールが効かないようにチューニングした上で、できるだけたくさん断片化が起きるように、まちまちのサイズのレコードを追加したり削除したりを1000万回繰り返すものです。以下のような結果になりました。

DBサイズ 経過時間
デフォルト 270.58 MB 54.853 秒
dfunit=8 74.797 MB 43.242 秒

データベースのサイズを30%以下に抑えつつも、処理時間が79%に短縮されています。空間効率が上がった分だけ、時間効率も下がるという素晴らしい結果になっています。また、上記のデータベースに静的デフラグをかけると65.33MBになりました。

処理内容は複雑化しているのに時間効率が上がるというのはちょっと不思議な感じがしますね。動的デフラグの処理の中では、I/Oをするにしても同一もしくは隣接ブロックの読み書きをするケースがほとんどなので、ファイルシステムのキャッシュが効きやすく、性能劣化の原因になりにくいと考えられます。その上で、データベースが小さくなった分だけファイルI/Oが効率化されたり各種のレイヤでキャッシュが効きやすくなるのが全体の処理速度を高速化した原因となっていると考えられます。

こんなにいいことずくめならデフォルトで動的デフラグを有効化しろという声も聞こえてきそうですが、肥大化が問題になりにくいようなユースケースで動的デフラグ有効にしてもオーバーヘッドになるだけですし、前述の明示的なデフラグ操作を行いたい場合も多いでしょうから、デフォルトでは依然として無効にしています。

永続的だが時限的なキャッシュ、再び

以前に書いたテーブルデータベースの記事で述べた、期限付きのセッション情報を永続記憶装置上で保持するユースケースを再び考えてみます。TCのテーブルデータベースのコラムとして期限切れ時刻を持たせて、1秒毎に起動する削除関数で古いレコードを消していくというものです。実はこれは最も断片化を起こしやすいユースケースのひとつで、今回の動的デフラグ機能は、キャッシュの肥大化に対して静的なoptimizeをせずに対処するために用意したようなものです。

おさらいになりますが、テストしてみましょう。以下のLuaスクリプトを準備して、ttexpire.luaとして保存してください。

function expire()
   local args = {}
   local cdate = string.format("%d", _time())
   table.insert(args, "addcond\0x\0NUMLE\0" .. cdate)
   table.insert(args, "out")
   local res = _misc("search", args)
   if not res then
      _log("expiration was failed")
   end
   print("rnum=" .. _rnum() .. "  size=" .. _size())
end

まずは、動的デフラグを無効にした状態で、どれくらい肥大化が起きるかを確認しておきます。以下のようにしてサーバを立ち上げてください。

ttserver -ext ttexpire.lua -extpc expire 1 "casket.tct#idx=x:dec#bnum=2000000"

別の端末を開いて、以下のテストコマンドを実行します。これは、50%の確率でランダムなセッション情報を格納し、10%の確率でその削除を試み、40%の確率でセッションの読み出しを行うというクエリを1000万回発行するテストを行います。セッションは入れられた時刻から180秒後に期限切れになって自動削除されます。

tcrtest table -exp 180 localhost 10000000

私の環境では、上記の条件だとだいたい130万セッションくらいを保持した状態でデータベースが維持され、598秒でテストが完了しました。つまり130万セッション保持で16722QPSくらいの性能は出そうです。それはよいのですが、問題は、データベースのサイズが213MBにもなってしまったことです。この調子でサイズが肥大化していけば性能もだんだん劣化してくるので最適化をかけるわけですが、この状態で「tcrmgr optimize localhost」したところ、12.6秒かかってサイズは102MBになりました。このことから、このアクセスパターンではDBが約2倍に肥大化するということと、それを解消するための静的デフラグには約13秒間のブロッキング(=サービス停止)が発生することがわかります。うーむ。それくらいなら無理からぬ運用体制とも言えますが、正直イマイチですね。

今度は、データベースを作り直して、動的デフラグを有効にしてサーバを起動します。

rm -rf casket*
ttserver -ext ttexpire.lua -extpc expire 1 "casket.tct#idx=x:dec#bnum=2000000#dfunit=8"

クライアント側は同じコマンドです。

tcrtest table -exp 180 localhost 10000000

今度は599秒かかり、サイズは133MBでした。つまり肥大化率を1.3倍に抑えて定常運用できるということです。このサイズは同じ処理をいくら続けても増えることはないので、最適化をかける必要はありません。これで安心してセッション情報を入れ続けることができます。

ピーク時のスループットを重視して、ピーク時以外に動的デフラグをかけたい場合には、以下のLuaスクリプトを30分間隔くらいで定期実行(-extpc defrag 1800)するとよいでしょう。動的デフラグなので実行中も他の操作をブロックしないというのがポイントです。

function defrag()
   local time = tonumber(os.date("%H%M"))
   if time >= 2230 or time <= 0130 then
      return nil
   end
   if not _misc("defrag") then
      _log("defragmentation was failed", 2)
   end
end

まとめ

Tokyo Cabinetで実装された動的デフラグ機能の設計と実装について説明し、その性能を確認しました。動的デフラグを有効にすると、サービスを止めてoptimizeすることなく、データベースの肥大化を抑制しながら運用を続けることができます。また、暗黙的な動的デフラグと明示的な動的デフラグを使い分けることで、実サービスのアクセスパターンを踏まえてスループットを追求することができます。そして、それらの工夫で「永続的だが時限的なキャッシュ」のアベイラビリティを向上させられることを示しました。地味な機能を長々と語りましたが、ここまで読んでくれたあなたはその価値をわかってくれますよね?

余談ですが、またもt.maesaka氏が主導して開発を進めているTCを使ったMySQLのストレージエンジン「BlitzDB」でも動的デフラグ機構は活用されるようです。optimizeしないで運用できるというだけでもMyISAMに対してちょっぴりアドバンテージがあるかなと思っている次第です。

OpenSocialとかC++0xとか世の中の流れが早すぎて、いろいろと勉強しなきゃなと焦りつつも、ついついピクミン2にはまってしまうmikioです。今回はTokyo Tyrant(TT)を使ってユーザ独自のストレージシステムを簡単に構築する方法について説明します。

ttplugin

プラグインとは

オブジェクト指向プログラミングに慣れた人にとっては、インターフェイスと実装を分離することによってプログラムの拡張性や保守性を向上させる技法(データ抽象)は常識ですよね。その考えをさらに進めると、インターフェイスのみをプログラムに記述しておいて、具体的な実装は実行時に割り当てるという、いわゆるプラグイン(plug-in)という技法に至ります。プラグインでカスタマイズできる能力をプラガブル(pluggable)などと言ったりもします。

例えばTokyo Cabinet(TC)では、レコードの挿入、削除、参照といった操作を抽象データベースAPIという共通のインターフェイスにまとめて、それを介してハッシュ表データベースやB+木データベースの実装を呼び出すことで、拡張性と保守性を向上させています。TTはTCの抽象データベースを用いているので、TTのコードにいっさい手を加える加えることなく、TCの新機能をTT経由で利用できるわけです。この時点では単なるデータ抽象にすぎないのですが、今回はさらに進んで、UNIXのダイナミックリンク機構を用いてプラグインを実現してみました。

余談ですが、memcachedの次のバージョン(1.4系)では、弊社のt.maesaka氏が主導して開発を進めているプラガブルストレージ機構が組み込まれる見込みです。今回ご紹介するTTの機能はそれとほぼ同じコンセプトで作られています。

まずは作ってみよう

TCとTTのそれぞれ最新版をインストールしてください。そして、プラグインとして以下のコードを用意します。これは「get」コマンドのみをサポートしていて、キーのデータをそのままエコーバックする機能を持ちます。こんなんではストレージとは言えませんが、インターフェイスさえ合っていれば何をやってもいいのです。

#include <tcadb.h>

/* openのダミー実装 */
bool myopen(void *opq, const char *name){
  return true;
}

/* closeのダミー実装 */
bool myclose(void *opq){
  return true;
}

/* getの置き換え */
void *myget(void *opq, const void *kbuf, int ksiz, int *sp){
  *sp = ksiz;                    /* 戻り値のサイズを指定 */
  return tcmemdup(kbuf, ksiz);   /* キーをコピーして返す */
}

/* ライブラリを初期化して、実装をオーバーライドする */
bool initialize(ADBSKEL *skel){
  skel->open = myopen;
  skel->close = myclose;
  skel->get = myget;
  return true;
}

このファイルを「ttskelecho.c」という名前で保存したならば、以下のコマンドでダイナミックリンクライブラリ(共有オブジェクト)である「ttshelecho.so」をビルドできます(Macでは「-shared」の代わりに「-bundle -undefined suppress」を指定してください)。

gcc -shared -o ttskelecho.so ttskelecho.c

そして、作ったライブラリをロードしつつ、サーバを起動します。

$ ttserver -skel ./ttskelecho.so

別の端末を開いて、置き換えた「get」コマンドを実行してみましょう。

$ tcrmgr get localhost hello

「hello」とエコーバックされたなら成功です。簡単ですよね。「get」以外でも全てのメソッドがオーバーライドできるので、いろいろ遊んでみてください。TTはmemcached互換プロトコルやHTTP互換プロトコルもサポートしていますので、TTを使うだけで独自のキャッシュサーバやHTTPサーバを簡単に実装できるというわけです。

スケルトンデータベース

ここからは、TTがどうやってプラグインを実現しているか、内部のカラクリを説明します。実は、プラグインストレージは、TTよりも下のTCの層でサポートされています。TCの抽象データベースに「スケルトン」と名付けた機構を挿入することでプラグインを実現しているのです。

TCの抽象データベースは従来からいくつかの具象データベースオブジェクトの操作をswitch文で切り替えて呼び出すという実装になっています。例えばデータベースサイズを返すtcadbsizeメソッドは以下のような実装になっています(なお、switch文による分岐がdisられるかもしれないので念のため補足しますが、case文が多くなってもラベルが離散的でなければいわゆるテーブルジャンプを使った最適化がコンパイル時に施されるので、実行時のオーバーヘッドはそれほど大きくなりません)。

uint64_t tcadbsize(TCADB *adb){
  uint64_t rv = 0;
  switch(adb->omode){
  case ADBOMDB: rv = tcmdbmsiz(adb->mdb); break;
  case ADBONDB: rv = tcndbmsiz(adb->ndb); break;
  case ADBOHDB: rv = tchdbfsiz(adb->hdb); break;
  case ADBOBDB: rv = tcbdbfsiz(adb->bdb); break;
  case ADBOFDB: rv = tcfdbfsiz(adb->fdb); break;
  case ADBOTDB: rv = tctdbfsiz(adb->tdb); break;
  }
  return rv;
}

しかし、上記だと、TCのコードに気軽に手を加えられる私にとっては新しい具象データベースをどんどん追加できて楽ではあるのですが、逆に言えば、TCに手を加えないと拡張できないのでユーザの皆さんは困ってしまいます。そこで、関数ポインタを使って任意の実行コードに制御を移せるようにしてみました。

typedef struct {            // スケルトンデータベースの構造体
  void *opq;                // レシーバへのポインタ
  ...
  uint64_t (*size)(void *); // sizeメソッドの関数ポインタ
  ...
} ADBSKEL;

typedef struct {            // 抽象データベースの構造体
  int omode;                // 動作モード
  ...
  ADBSKEL *skel;            // スケルトンへのポインタ
  ...
} TCADB;

uint64_t tcadbsize(TCADB *adb){
  uint64_t rv = 0;
  switch(adb->omode){
  case ADBOMDB: rv = tcmdbmsiz(adb->mdb); break;
  case ADBONDB: rv = tcndbmsiz(adb->ndb); break;
  case ADBOHDB: rv = tchdbfsiz(adb->hdb); break;
  case ADBOBDB: rv = tcbdbfsiz(adb->bdb); break;
  case ADBOFDB: rv = tcfdbfsiz(adb->fdb); break;
  case ADBOTDB: rv = tctdbfsiz(adb->tdb); break;
  case ADBOSKEL: rv = adb->skel->size(adb->skel->opq); break;
  }
  return rv;
}

ADBSKELという構造体型のメンバ変数に抽象データベースの各種メソッドと互換する型の関数ポインタを持たせ、任意の関数を参照できるようにします。この構造体をスケルトンデータベースと呼ぶことにしましょう。そして、実行時にスケルトンモードの場合(adb->omodeがADBOSKELの場合)には、スケルトンデータベースが参照する関数に制御を移すのです。タネを明かして見れば簡単な仕組みですよね。C++言語などのオブジェクトシステムも単純化すればこのようなディスパッチテーブルを用いて実現されているようです。個人的には「オブジェクト指向xxx」とか言われるとものすごく高尚な感じがしてびびっちゃいますが、「関数ポインタによるディスパッチテーブルを階層化した奴」だと思うと何とか使いこなせる気がしてくる今日この頃です。

さて、TCの層の抽象データベースAPIに加わったスケルトンデータベース機構の完全な定義は以下のようになります。

typedef struct {
  void *opq;
  void (*del)(void *);
  bool (*open)(void *, const char *);
  bool (*close)(void *);
  bool (*put)(void *, const void *, int, const void *, int);
  bool (*putkeep)(void *, const void *, int, const void *, int);
  bool (*putcat)(void *, const void *, int, const void *, int);
  bool (*out)(void *, const void *, int);
  void *(*get)(void *, const void *, int, int *);
  int (*vsiz)(void *, const void *, int);
  bool (*iterinit)(void *);
  void *(*iternext)(void *, int *);
  TCLIST *(*fwmkeys)(void *, const void *, int, int);
  int (*addint)(void *, const void *, int, int);
  double (*adddouble)(void *, const void *, int, double);
  bool (*sync)(void *);
  bool (*optimize)(void *, const char *);
  bool (*vanish)(void *);
  bool (*copy)(void *, const char *);
  bool (*tranbegin)(void *);
  bool (*trancommit)(void *);
  bool (*tranabort)(void *);
  const char *(*path)(void *);
  uint64_t (*rnum)(void *);
  uint64_t (*size)(void *);
  TCLIST *(*misc)(void *, const char *, const TCLIST *);
  bool (*putproc)(void *, const void *, int, const void *, int, TCPDPROC, void *);
  bool (*foreach)(void *, TCITER, void *);
} ADBSKEL;

bool tcadbsetskel(TCADB *adb, ADBSKEL *skel);

アプリケーションは、ADBSKEL構造体の各メンバを設定してから、tcadbsetskelにそのポインタを渡すと、データベースの各操作をオーバーライドできるというわけです。各メソッドの第1引数(レシーバ)として渡されるポインタにはopqメンバで指定したものがそのまま渡されます。

思考実験のために、抽象データベースの中でさらに抽象データベースを呼び出すスケルトンの実装を考えてみましょう。以下のようなコードになります。もちろん動作もちゃんとしますよ(使う意味は全くないけれど)。

ADBSKEL skel;
memset(skel, 0, sizeof(skel));
skel.opq = tcadbnew();
skel.del = (void (*)(void *))tcadbdel;
skel.open = (bool (*)(void *, const char *))tcadbopen;
skel.close = (bool (*)(void *))tcadbclose;
skel.put = (bool (*)(void *, const void *, int, const void *, int))tcadbput;
skel.putkeep = (bool (*)(void *, const void *, int, const void *, int))tcadbputkeep;
skel.putcat = (bool (*)(void *, const void *, int, const void *, int))tcadbputcat;
skel.out = (bool (*)(void *, const void *, int))tcadbout;
skel.get = (void *(*)(void *, const void *, int, int *))tcadbget;
skel.vsiz = (int (*)(void *, const void *, int))tcadbvsiz;
skel.iterinit = (bool (*)(void *))tcadbiterinit;
skel.iternext = (void *(*)(void *, int *))tcadbiternext;
skel.fwmkeys = (TCLIST *(*)(void *, const void *, int, int))tcadbfwmkeys;
skel.addint = (int (*)(void *, const void *, int, int))tcadbaddint;
skel.adddouble = (double (*)(void *, const void *, int, double))tcadbadddouble;
skel.sync = (bool (*)(void *))tcadbsync;
skel.optimize = (bool (*)(void *, const char *))tcadboptimize;
skel.vanish = (bool (*)(void *))tcadbvanish;
skel.copy = (bool (*)(void *, const char *))tcadbcopy;
skel.tranbegin = (bool (*)(void *))tcadbtranbegin;
skel.trancommit = (bool (*)(void *))tcadbtrancommit;
skel.tranabort = (bool (*)(void *))tcadbtranabort;
skel.path = (const char *(*)(void *))tcadbpath;
skel.rnum = (uint64_t (*)(void *))tcadbrnum;
skel.size = (uint64_t (*)(void *))tcadbsize;
skel.misc = (TCLIST *(*)(void *, const char *, const TCLIST *))tcadbmisc;
skel.putproc =
  (bool (*)(void *, const void *, int, const void *, int, TCPDPROC, void *))tcadbputproc;
skel.foreach = (bool (*)(void *, TCITER, void *))tcadbforeach;
tcadbsetskel(adb, &skel);

関数ポインタをキャストする必要があるのは、レシーバの型が(TCADB*)と(void*)で差があるからです(ポインタ同士の互換性は言語仕様で保証されています)。ここでは抽象データベースの全てのメソッドをオーバーライドしていますが、自分に必要ないメソッドは実装しなくても大丈夫です。最初にmemsetしたことで全てのメンバにあらかじめNULLを代入しているのと同じ効果があり、呼び出そうとする関数がNULLの場合は単にエラーを返す仕様だからです。

ダイナミックリンク

さて、スケルトンに関数ポインタの集合を与えることで抽象データベースの振る舞いを完全にオーバーライドできることがわかったところで、実際に呼び出される関数の実装をどうやってTCやTTのパッケージの外に追い出すかが次の課題となります。最初に思いつくのは、TCやTTのビルド時に./configureでユーザ指定のファイルを組み込む「スタティックリンク」戦略ですが、それだと機能を拡張する度に毎回TCやTTをインストールしなおさなければいけなくなります。そうでなく、実行時に任意のライブラリをリンクさせる「ダイナミックリンク」戦略をここでは採用しましょう。

UNIX系のプラットフォームでは、ダイナミックリンクはdlopenファミリと呼ばれる関数群で実現されています。dlopenでファイル名を指定してライブラリを開いてハンドルを得て、dlsymでハンドルと関数名を指定してそのライブラリ内の関数実装を指す関数ポインタを得て、使い終わったらdlcloseでハンドルを破棄するというわけです。例えば「hoge.so」というライブラリで定義されている、char*を引数にして戻り値のない「myfunc」という関数を呼び出したい場合には、以下のようにします。

void *lib = dlopen("hoge.so", RTLD_LAZY);
void (*myfunc)(char *) = dlsym(lib, "myfunc");
myfunc("hello");
dlclose(lib);

簡単ですね。あとはTTのサーバ(ttserver)の起動時にスケルトンを定義するコードを含んだライブラリを指定できるようにすればOKです。TTの仕様では、ライブラリ内に「initialize」という名前の関数が定義されていて、それはADBSKEL構造体へのポインタを受け取って、その各メンバに任意の値を設定してから、実行の成否をboolで返すということを要求しています。すなわち以下のシグネチャです。

bool initialize(ADBSKEL *skel);

openとcloseは必ずオーバーライドしてください(前述したようなダミーでOKです)。さらに、opqに設定したオブジェクトを抽象データベースの解放時に始末したい場合は、del(デストラクタ)をオーバーライドするとよいでしょう。仕上げに「gcc -shared …」でコンパイルして「ttserver -skel …」で組み込むだけです。簡単ですよね。なお、「-skel」の引数に渡す名前に「/」が含まれない場合には、dlopenによってライブラリサーチパスから自動的にライブラリを探してくれますので、例えば「/usr/lib」の下にライブラリをおいておけば、「ttserver -skel hoge.so」とするだけでロードすることができます。ライブラリサーチパスの指定はldconfigコマンドや環境変数LD_LIBRARY_PATHで行うことができます(Macではちょっと違うみたいですが、私はよく知りません)。

スケルトンデータベースはTCの層の機能ですが、dlopen経由でスケルトンを設定する機能はTTの層でのみサポートされます。なぜなら、TCを直接使っているということはADBSKEL構造体に直接触れる状態にあるということと同義なので、わざわざダイナミックリンクしないでも実装を切り替えられるはずだからです。また、いくつかの処理系でdlopenが実装されていないことが想定されるために、TCをdlopenに依存させることは避けたかったという理由もあります。

ディレクトリデータベース

もう少し実践的な例も見てみましょう。少数のでかいデータをネットワーク経由で管理したい場合は、各レコードをTCなどのDBMに格納するのではなく、ファイルシステム上の単一のファイルとして扱った方が効率的な場合もあります。そうすると各種のファイルシステム管理ツールと連携できるという利点もあるでしょう。ということで、特定のディレクトリの中に、URLエンコードしたキーを名前とするファイルを作ってその中に値を記録するという、「ディレクトリデータベース」を実装してみます。ここでは、del, open, close, put, out, get のみをオーバーライドします。openの引数にはディレクトリ名を指定する仕様です。

#include <tcutil.h>
#include <tcadb.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

typedef struct {
  char *name;
} TCDDB;

/* キーからファイルパスを作るユーティリティ */
static char *makepath(TCDDB *ddb, const void *kbuf, int ksiz){
  char *uenc = tcurlencode(kbuf, ksiz);
  char *path = tcsprintf("%s/%s", ddb->name, uenc);
  tcfree(uenc);
  return path;
}

/* コンストラクタの実装 */
static TCDDB *tcddbnew(void){
  TCDDB *ddb = tcmalloc(sizeof(*ddb));
  ddb->name = NULL;
  return ddb;
}

/* デストラクタの実装 */
static void tcddbdel(TCDDB *ddb){
  tcfree(ddb);
}

/* openメソッドの実装 */
static bool tcddbopen(TCDDB *ddb, const char *name){
  struct stat sbuf;
  if(stat(name, &sbuf) == 0){
    if(!S_ISDIR(sbuf.st_mode)) return false;
  } else {
    if(mkdir(name, 0755) != 0) return false;
  }
  ddb->name = tcstrdup(name);
  return true;
}

/* closeメソッドの実装 */
static bool tcddbclose(TCDDB *ddb){
  tcfree(ddb->name);
  return true;
}

/* putメソッドの実装 */
static bool tcddbput(TCDDB *ddb, const void *kbuf, int ksiz, const void *vbuf, int vsiz){
  bool err = false;
  char *path = makepath(ddb, kbuf, ksiz);
  if(!tcwritefile(path, vbuf, vsiz)) err = true;
  tcfree(path);
  return !err;
}

/* outメソッドの実装 */
static bool tcddbout(TCDDB *ddb, const void *kbuf, int ksiz){
  bool err = false;
  char *path = makepath(ddb, kbuf, ksiz);
  if(unlink(path) != 0) err = true;
  tcfree(path);
  return !err;
}

/* getメソッドの実装 */
static void *tcddbget(TCDDB *ddb, const void *kbuf, int ksiz, int *sp){
  void *vbuf;
  char *path = makepath(ddb, kbuf, ksiz);
  vbuf = tcreadfile(path, -1, sp);
  tcfree(path);
  return vbuf;
}

/* ライブラリを初期化して、実装をオーバーライドする */
bool initialize(ADBSKEL *skel){
  skel->opq = tcddbnew();
  skel->del = (void (*)(void *))tcddbdel;
  skel->open = (bool (*)(void *, const char *))tcddbopen;
  skel->close = (bool (*)(void *))tcddbclose;
  skel->put = (bool (*)(void *, const void *, int, const void *, int))tcddbput;
  skel->out = (bool (*)(void *, const void *, int))tcddbout;
  skel->get = (void *(*)(void *, const void *, int, int *))tcddbget;
  return true;
}

鬼のように簡単ですね。TTはHTTPも喋るので、簡易的な更新機能付きWebサーバとして使えるのは魅力でしょう。ただし、念のため補足しますと、このデータベースの検索性能はファイルシステムのディレクトリ機構に完全に依存しているため、ファイルシステムの種類にもよりますが、レコード数が多くなるとすぐに性能が劣化してしまいます(そりゃもうストップウォッチで計れるほどに)。くれぐれも「少数のでかいデータをネットワーク経由で管理したい場合」にのみ使ってください。

ブラックホールデータベース

今度は、MySQLの “blackhole” ストレージエンジンに相当する機能を作ってみましょう。これは文字通り何もしないスケルトンで、必ずレプリケーションのマスタとして使います。ブラックホールであるマスタは何もせずに更新ログの記録だけ行い、その更新ログを受け取ったスレーブ側で実際のデータベースへの記録を行います。このスケルトンは内部状態を持たず、かつ更新系のみをサポートするので、open, close, put, out, misc のみをオーバーライドします。

#include <tcadb.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

static bool myopen(void *opq, const char *name){
  return true;
}

static bool myclose(void *opq){
  return true;
}

static bool myput(void *opq, const void *kbuf, int ksiz, const void *vbuf, int vsiz){
  return true;
}

static bool myout(void *opq, const void *kbuf, int ksiz){
  return true;
}

static TCLIST *mymisc(void *opq, const char *name, const TCLIST *args){
  return tclistnew2(1);
}

bool initialize(ADBSKEL *skel){
  skel->open = myopen;
  skel->close = myclose;
  skel->put = myput;
  skel->out = myout;
  skel->misc = mymisc;
  return true;
}

ブラックホールエンジンは、マスタにメッセージキューのような役割をさせることで、比較的重い処理を非同期的に行ったり、スレーブを副数台並べて水平分散を行ったりすることが簡単にできるのです。目からウロコですね。こんなバカっぽい仕組みですが、同期性を少し犠牲にするだけでスケーラビリティをものすごく高めることができるので、非常に大規模なデータセットを扱う際には重宝します。また、地理的に離れている拠点間でデータを同期させたい場合にも活用できるでしょう。

プロクシデータベース

最後に、あるTTのサーバに来たリクエストを別のTTにリレーしてそのレスポンスを中継するだけの「プロクシデータベース」を実装してみましょう。これはTTのリモートデータベースAPIを使って、ものすごく簡単に作れます。openの引数にはサーバ名とポート番号をコロン区切りで指定する仕様です。

#include <tcadb.h>
#include <tcrdb.h>
#include <stdlib.h>
#include <stdbool.h>
#include <stdint.h>

bool initialize(ADBSKEL *skel){
  skel->opq = tcrdbnew();
  skel->del = (void (*)(void *))tcrdbdel;
  skel->open = (bool (*)(void *, const char *))tcrdbopen2;
  skel->close = (bool (*)(void *))tcrdbclose;
  skel->put = (bool (*)(void *, const void *, int, const void *, int))tcrdbput;
  skel->putkeep = (bool (*)(void *, const void *, int, const void *, int))tcrdbputkeep;
  skel->putcat = (bool (*)(void *, const void *, int, const void *, int))tcrdbputcat;
  skel->out = (bool (*)(void *, const void *, int))tcrdbout;
  skel->get = (void *(*)(void *, const void *, int, int *))tcrdbget;
  skel->vsiz = (int (*)(void *, const void *, int))tcrdbvsiz;
  skel->iterinit = (bool (*)(void *))tcrdbiterinit;
  skel->iternext = (void *(*)(void *, int *))tcrdbiternext;
  skel->fwmkeys = (TCLIST *(*)(void *, const void *, int, int))tcrdbfwmkeys;
  skel->addint = (int (*)(void *, const void *, int, int))tcrdbaddint;
  skel->adddouble = (double (*)(void *, const void *, int, double))tcrdbadddouble;
  skel->sync = (bool (*)(void *))tcrdbsync;
  skel->optimize = (bool (*)(void *, const char *))tcrdboptimize;
  skel->vanish = (bool (*)(void *))tcrdbvanish;
  skel->copy = (bool (*)(void *, const char *))tcrdbcopy;
  skel->path = (const char *(*)(void *))tcrdbexpr;
  skel->rnum = (uint64_t (*)(void *))tcrdbrnum;
  skel->size = (uint64_t (*)(void *))tcrdbsize;
  return true;
}

たったこれだけです。我ながらよくできてます。まあ、ただ中継してもしょうがないので、実用的にはキーにハッシュ関数をかけて水平分散するとか、接続先の死活管理をしてフェイルオーバーするとかいった付加機能をつけることになるでしょう。あるいはhBaseやMySQLのゲートウェイとして実装しても面白いでしょう。

まとめ

Tokyo Tyrantのストレージ層をダイナミックリンクのプラグインで差し替えて、独自のストレージ層を構築する方法について説明しました。以前からLua拡張によってかなり柔軟なストレージシステムを構築できるTTではありますが、ストレージ層をCレベルでハックできるようになったおかげで、そこそこ経験のあるプログラマにとっては、無制限に何でもできてしまうようになりました。

epollなどによる効率的入出力やレプリケーションなどの実用的な管理機能はTTをコンテナとすることで「再利用」できます。ストレージのプログラマは、ネットワークやユーティリティのことは忘れて、ひたすらアルゴリズムとデータ構造を考えることができるのです。クライアントライブラリを書く必要がないってのも嬉しいですね。

この記事ではサンプルとしてディレクトリを使ったデータベースやプロクシとして動作するデータベースを紹介しましたが、既存のソフトウェアを利用すればいくらでも応用例を考えることができます。Oracle社のBerkeley DB、D.J.バーンスタイン氏のCDB、岡野原大輔氏のTxBep、山田浩之氏のLux IO、Google社のsparsehashなどなど、ネットワーク越しに利用できたりレプリケーションができるようになったりすると嬉しいようなkey-valueストレージは世の中にいっぱいあります。Tokyo DystopiaをTokyo Tyrant経由で使いたいというご意見も国内海外を問わず結構いただいているのですが、このスケルトンデータベース機構を用いることで美しいインテグレーションができると思っています。

ということで、Cを経験済みの紳士淑女はぜひマイストレージ作りに挑戦してみてください。