読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

Lua on Tyrant: DBサーバにLLを組み込む

mixi

遅めの夏休みで那須塩原に行ってきたmikioです。牧場でアルパカに触ってきたのですが、めちゃかわいかったです。さて今回は、Tokyo Tyrant(TT)にスクリプト言語Luaの処理系を組み込んで使う方法について解説します。

つか、Luaって何?

Lua(公式サイトによると「るーあ(LOO-ah)」と発音)という言語の名前は聞いたことがあっても、数あるマイナー言語のひとつと思って特に気にかけていない人も多いと思います。私もそうでした。しかし、今では、C言語使いの第2言語・第3言語として使うにはとても有望な言語だと思っています。

Luaに関する日本語の情報はまだ多くはないのですが、以下のサイトを順に読むとだいたいの雰囲気が掴めると思います。

Luaは言語仕様が小さいので、とても習得しやすいです。上記のリファレンスマニュアルだけ読めばプログラミングに必要な情報はほとんど把握できるくらいです。事実、私もそれだけの知識でこの記事を書けるくらいはLuaを使いこなせるようになっています。

Luaによるプログラミングでは、Perlでいうところのハッシュや配列としての機能を兼ね備える「テーブル」という機構を使いこなすのが要点となります。Luaは手続き型言語ですが、テーブルのメンバに関数を持たせることでオブジェクト指向言語っぽく使うこともできます。

私が新しい言語を覚える時によくやるのが、DBM風のインターフェイスを実装することです。こんな感じになります。下記プログラムを test.lua というファイルに保存したら、lua test.lua コマンドで実行できます。

DBM = {}            -- クラス風に見せるためのオブジェクト
function DBM.new()  -- 上記オブジェクト内にコンストラクタを定義
   -- オブジェクトを作ってインスタンスフィールドを定義
   local obj = {
      data = {},   -- 実際のレコードを入れるテーブル
      iter = nil,  -- イテレータ用のインデックス
      num = 0,     -- レコード数のカウンタ
   }
   -- レコードを格納する関数(上書きモード)の定義
   function obj.put(self, key, value)
      if self.data[key] == nil then
         self.num = self.num + 1
      end
      self.data[key] = value
      return true
   end
   -- レコードを格納する関数(既存保持モード)の定義
   function obj.putkeep(self, key, value)
      if self.data[key] ~= nil then
         return false
      end
      self.num = self.num + 1
      self.data[key] = value
      return true
   end
   -- レコードを削除する関数の定義
   function obj.out(self, key)
      if self.data[key] == nil then
         return false
      end
      self.num = self.num - 1
      self.data[key] = nil
      return true
   end
   -- レコードの値を参照する関数の定義
   function obj.get(self, key)
      return self.data[key]
   end
   -- イテレータを初期化する関数の定義
   function obj.iterinit(self)
      self.iter = nil
   end
   -- イテレータから次のレコードのキーを取り出す関数の定義
   function obj.iternext(self)
      self.iter = next(self.data, self.iter)
      return self.iter
   end
   -- レコード数を取得する関数の定義
   function obj.rnum(self)
      return self.num
   end
   -- 作ったオブジェクトを返す
   return obj
end

-- クラス風オブジェクトのコンストラクタを呼び出してオブジェクトを作る
db = DBM.new()

-- レコードを格納する
db:put("one", "first")
db:put("two", "second")
db:put("three", "third")
db:putkeep("three", "fail")

-- レコードを検索する
print("one: " .. db:get("one"))

-- レコードを消す
db:out("one")

-- イテレータをしばく
db:iterinit()
while true do
   local key = db:iternext()
   if key == nil then
      break
   end
   print(key .. ": " .. db:get(key))
end

コンストラクタ(new)のあたりが少しわかりにくいかもしれませんが、function myfunc(arg){ ... } という書き方は myfunc = function(arg){ ... } と書くのと同義であるという仕様を考えると理解できます。DBMというオブジェクトのメンバnewとして関数を持たせて、その関数の中でdbというオブジェクトを作って、そのメンバとして各種の関数を持たせているということです。

後半の、オブジェクトを利用するところも面白いですね。メンバーを参照する「.」演算子の代わりに「:」演算子を使うことで、呼び出した関数のレシーバを暗黙の第一引数に指定してくれるところがニクい演出です(Perlのblessみたいなノリ)。

Luaを組み込む

DBMがよく「組み込み用データベース」などと称されるように、Luaもよく「組み込み用スクリプト言語」などと称されます。ここでいう「組み込み」とは、PC以外の小型ハードウェアに組み込むという意味ではなく、アプリケーションのプロセス内に処理系を組み込むという意味です。LuaはC言語用のAPIが美しく整備されているので、アプリケーションに組み込む作業がとても容易なのです。

余談ですが、TTに最初に組み込もうとしたのはRubyの処理系でした。同じくC言語用のAPIがきちんと整備されており、ユーザ人口やライブラリの充実度で優れているからです。しかし、動き出すところまで作って気づいたのは、現状の処理系(ruby-1.8.x)では、OSネイティブのスレッド機構(POSIX thread)を使うプロセスでRubyのAPIを呼び出すと、GCが走ったところで落ちてしまうことでした。今のところRubyは組み込み用途には向いていないようですが、今後の新しい処理系でこの問題が解決されれば、Ruby on Tyrantも公開しようと思います。

一方で、Luaの処理系はネイティブスレッドと併用して使うことができます。グローバル変数や静的変数を一切使っていないのでLuaのAPIの関数群はリエントラントです。ただし、Lua自体はANSI Cのみで実装されており、したがってネイティブスレッドに起因するレースコンディションの回避手段を自身では備えないので、ネイティブスレッドの排他制御はアプリケーション側の責任で行う必要があります。TTではワーカスレッド毎に別々のLuaの処理系のインスタンスを持たせることで、レースコンディションを解決しています。

上記で処理系のインスタンスと言っているものは、CのAPIでは lua_State という構造体で表現されます。Luaに対するすべての操作は lua_State へのポインタを介して行います。ということで、CのAPIによるHello Worldです(ビルドは、gcc -I/usr/include/lua5.1 luatest.c -o luatest -llua5.1 とかやってください)。

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

int main(int argc, char **argv){

  /* Luaのインスタンスを生成する */
  lua_State *lua = luaL_newstate();

  /* 標準ライブラリを読み込む */
  luaL_openlibs(lua);

  /* 文字列のLuaプログラムをチャンクとしてスタックに載せる */
  luaL_loadstring(lua, "print('hello world')");

  /* スタック上のチャンクを実行する */
  lua_pcall(lua, 0, 0, 0);

  /* Luaのインスタンスを削除する */
  lua_close(lua);

  return 0;
}

激烈に簡単ですよね! 唐突に「チャンク」とか「スタック」とかいう用語が出てきて何だかむかつくという以外は、とてもわかりやすい構成になっていると思います。Luaの世界では全ての値はLuaスタックという入れ物に入れられることになっていて、Luaスタックから外された値はガベージコレクタによって消去されてしまいます。ただし、Luaスタックの値を、名前をつけた変数や、その変数がテーブルである場合はそのフィールドに、退避することもできます(つまり変数に値を代入するということ)。

さて、チャンクというのは、実行される文の塊のことで、関数と同じように扱うことができます。関数を実行する際には、関数をLuaスタックのトップに載せ、その上に関数に渡す引数を載せた状態で、lua_pcallというAPIを呼びます。つまり、print('hello world') というLuaのチャンクを読み込むのは、printという変数に代入されている関数をLuaスタックに載せてから、その上に 'hello world' という文字列を載せるという操作とほぼ同義です。実際にやってみましょう。

#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"

int main(int argc, char **argv){

  /* Luaのインスタンスを作る */
  lua_State *lua = luaL_newstate();

  /* 標準ライブラリを読み込む */
  luaL_openlibs(lua);

  /* グローバル変数 "print" の中身をスタックに載せる */
  lua_getglobal(lua, "print");

  /* 文字列 "hello world" をスタックに載せる */
  lua_pushstring(lua, "hello world");

  /* 引数1個の関数を呼び出す */
  lua_pcall(lua, 1, 0, 0);

  /* Luaのインスタンスを解放する */
  lua_close(lua);

  return 0;
}

激烈に簡単ですよね! もはやCだけでLuaの処理系を操作できるようになっています。とにかく何をするにもLuaスタックに載せてから、Luaスタック上のレコードの数や番号を指定してその処理のためのAPIを呼び出すというスタイルを覚えてください。APIの詳しいことはリファレンスマニュアルに書いてあります。

Tokyo TyrantとLua

さて、いよいよ本題です。なぜTTにスクリプト言語を組み込んだのかと言えば、自分で使いたくなったりユーザの皆さんからの要望をいただいたりした機能の各々を提供する度に、TTの実装とプロトコルを拡張するのが面倒だからです。サーバ側にLuaで記述した任意の関数を登録できるようにして、クライアント側から関数名を指定して呼び出せるようにすれば、TT自身の実装を拡張する必要はなくなります。また、「キーと値を引数として渡して、実行結果を返す」というインターフェイスはどのデータベース操作にも共通しているので、「メソッド名のstringと、キーのstringと、値のstringを送信すると、その名前のメソッドにキーと値を渡して実行し、戻り値のstringが返信される」というプロトコルだけ用意しておけば、プロトコルを都度定義する必要もなくなります。

実際に使ってみましょう。まずはインストールです。Tokyo Cabint 1.3.10以降と、Lua 5.1以降を予めインストールした上で、Tokyo Tyrant 1.1.2以降をインストールします。TTのソースパッケージを展開したディレクトリの中で、以下の作業を行ってください(もし--enable-luaを指定しないとLuaが組み込まれないので注意)。

./configure --enable-lua
make
su
make install

次に、Luaスクリプトを作ります。myfunc.lua というファイルに以下のプログラムを記述してください。単にキーと値をコロンで区切った文字列を返す関数です。

function echo(key, value)
   return key .. ":" .. value
end

Luaスクリプトを読み込ませてデータベースサーバを起動します。

ttserver -ext myfunc.lua

別の端末を開いて、先ほど定義した「echo」関数を呼んでみます。キーとして「foo」、値として「bar」を渡します。

tcrmgr ext localhost echo foo bar

「foo:bar」が表示されたら成功です。激烈に簡単ですよね! では次は、こんなスクリプトを読み込ませて起動してみてください。キーに対応した値を10進数値として加算する関数です。

function incr(key, value)
   value = tonumber(value)
   if not value then
      return nil
   end
   local old = tonumber(_get(key))
   if old then
      value = value + old
   end
   if not _put(key, value) then
      return nil
   end
   return value
end

ソースコードを見ればどんな処理をするのかだいたい検討つきますよね。実際に呼び出して使ってみましょう。

$ ./tcrmgr ext localhost incr foo 1
1
$ ./tcrmgr ext localhost incr foo 2
3
$ ./tcrmgr ext localhost incr foo 3
6
$ ./tcrmgr ext localhost incr foo 4
10

TTによるビルトイン関数

上述のサンプルでは、「データベースから古い値を取り出して、付与の値と加算して、結果の値をデータベースに格納するとともに、それを返す」という処理を行っています。データベースの操作は、「_」で始まるビルトイン関数によって行います。ビルトイン関数としては以下のものが提供されています。引数keyやvalueには文字列でも数値でも指定できます。

  • _put(key, value) : レコードを格納する(上書き)
  • _putkeep(key, value) : レコードを格納する(既存値保持)
  • _putcat(key, value) : レコードを格納する(既存値連結)
  • _out(key) : レコードを削除する
  • _get(key) : レコードの値を取得する
  • _vsiz(key) : レコードの値のサイズを取得する
  • _iterinit() : イテレータを初期化する
  • _iternext() : イテレータから次のレコードのキーを取り出す
  • _vanish() : 全レコードを削除する
  • _rnum() : レコード数を取得する
  • _size() : データベースのサイズを取得する

データベース操作ではありませんが、数値とバイナリ文字列を相互変換するためのビルトイン関数も提供されています。_packは引数がテーブル(配列)である場合はその要素を対象として処理を行い、引数が数値である場合はそのものを対象とします。引数自体を複数指定して配列を表現してもOKです。

  • _pack(format, array, ...) : フォーマットに基づいて数値リストをバイナリ文字列に変換
  • _unpack(format, binstr) : フォーマットに基づいてバイナリ文字列を数値リストに復元

フォーマットには以下の変換文字が利用できます。PerlやRubyやPythonのpack/unpackとはちょっと違うので注意してください。

  • c : 符号付き8ビット値
  • C : 符号なし8ビット値
  • s : 符号付き16ビット値
  • S : 符号なし16ビット値
  • i : 符号付き32ビット値
  • I : 符号なし32ビット値
  • l : 符号付き64ビット値
  • L : 符号なし64ビット値
  • n : 符号なしネットワークバイトオーダ16ビット値
  • N : 符号なしネットワークバイトオーダ32ビット値
  • M : 符号なしネットワークバイトオーダ64ビット値
  • w : 非負整数のBERエンコード

Perl等と同じように変換文字の後ろに繰り返し回数やワイルドカードを指定することは可能です。例えば、変数arrayの配列に入っている数値を、1個の符号なし8ビット数値を先頭につけ、その後ろに4個の符号付きネットワークバイトオーダ32ビット数値をつけ、その後ろに残り全部の数値をBERエンコードでつなげたい場合は、binstr = _pack("CN4w*", array) とします。もちろん、それを復元するには、array = _unpack("CN4w*", binstr) とすればよいのです。データベースに入れるデータは文字列(バイナリでもOK)に直列化する必要があるのですが、「1234」などのASCII文字列で表すよりも、変域に合わせたバイナリ表現で表した方が空間効率がよくなります。ということで、真面目なユースケースにおいては、_pack/_unpack を使いこなすのはかなり重要です(Luaの標準ライブラリにはpack/unpackがないから自分で作るハメになってしまった)。

Luaの処理系のインスタンスをネイティブスレッド毎に持っている関係で、Luaのグローバル変数を動的に生成した場合、セッション毎にそのグローバル変数があったりなかったりする事態となってしまいます。つまりLuaのグローバル変数はロード時に定義して定数としてしか利用できないということです。となると、セッション間で共有すべきデータはデータベースに格納するのが基本となるわけですが、抜け道として、セッション間のちょっとしたデータ共有のためのスタッシュという機構を用意しています。スタッシュはTCのオンメモリデータベースとして実装されています。

  • _stashput(key, value) : スタッシュにレコードを格納する
  • _stashout(key) : スタッシュのレコードを削除する
  • _stashget(key) : スタッシュのレコードの値を取得する

「_begin」および「_end」というユーザ定義関数がある場合、PerlのBEGINブロックやENDブロックのように、サーバの起動時や終了時に引数なしでそれらの関数が呼ばれます。Luaの処理系のインスタンス数(=ネイティブスレッド数)がいくつであっても「_begin」と「_end」は1回しか呼ばれないのがポイントです。これらはスタッシュの初期化処理や終了処理に便利です。

ところで、サンプルプログラムで定義した「incr」関数の処理において、「_get」と「_put」の間に他のスレッドが同じレコードの操作をしてしまったら値の整合性がとれなくなって困ります。つまり「incr」関数はレコードロックで保護して実行する必要があります。レコードロックで保護しつつLua関数を呼ぶには、以下のようにします。

tcrmgr ext -xlr localhost incr foo bar

なお、「-xlr」の代わりに「-xlg」とするとグローバルなロックをかけることもできます。レコードロックもグローバルロックもアドバイザリロックにすぎないことに注意してください。つまり全てのクライアントが該当のロックをかけて関数を呼び出すということが整合性の前提となります。

この解説ではクライアントとしてtcrmgrコマンドを使っていますが、もちろんTTのAPI(リモートデータベースAPI)でも同様の操作はできます。PerlやRubyやJavaのクライアントライブラリも気が向いたら作っていく所存です。

High and Low

突然ですが、High and Lowという古典的なゲームをご存知でしょうか。次に出される数値が今の数値より大きいか小さいかを当てるゲームです。それをTTとLuaで実装してみましょう。以下のようなフローを考えます。

  • 「start」という関数で、ユーザ名と所持金を登録するとともに、今の値を返す。キーはユーザ名、値は所持金。
  • 「high」または「low」という関数で、大小と掛金を表明するとともに、結果と今の値を返す。キーは名前、値は掛金。掛金が0以下になるか、セッションが5ラウンド過ぎたらゲームオーバー。
  • 「over」という関数で、セッションを破棄する。キーはユーザ名、値は無視。

セッションはユーザ名をキーとして管理し、キーに「:r」を接尾させたレコードでラウンド数を、キーに「:n」を接尾させたレコードで今の値を、キーに「:m」を接尾させたレコードで所持金を記録します。ここまで決まれば、あとは実装するだけです。

NUMRANGE = 100
MAXROUND = 5
math.randomseed(os.time())

function start(key, value)
   value = tonumber(value)
   if not value or value <= 0 then
      return "error: invalid value"
   end
   if not _putkeep(key .. ":r", 1) then
      return "error: already started"
   end
   local num = math.random(NUMRANGE)
   _put(key .. ":m", value)
   _put(key .. ":n", num)
   return "Welcome, " .. key .. ".\\n" ..
      "The current number is " .. num .. ".\\n" ..
      "Your money is " .. value .. ".\\n" ..
      "Round 1 Bet!\\n"
end

function high(key, value)
   return do_bet(key, value, true)
end

function low(key, value)
   return do_bet(key, value, false)
end

function over(key, value)
   if _vsiz(key .. ":r") < 0 then
      return "error: not started"
   end
   _out(key .. ":r")
   _out(key .. ":m")
   _out(key .. ":n")
   return "Good Bye!"
end

function do_bet(key, value, ishigh)
   value = tonumber(value)
   if not value or value <= 0 then
      return "error: invalid value"
   end
   local round = tonumber(_get(key .. ":r"))
   if not round then
      return "error: not started"
   end
   local money = tonumber(_get(key .. ":m"))
   if round > MAXROUND or money < 1 then
      return "error: already finished"
   end
   if value > money then
      value = money
   end
   local num = tonumber(_get(key .. ":n"))
   local newnum = math.random(NUMRANGE)
   local cmp = "even"
   local res = "lost"
   if newnum > num then
      cmp = "high"
      if ishigh then
         res = "won"
      end
   elseif newnum < num then
      cmp = "low"
      if not ishigh then
         res = "won"
      end
   end
   round = round + 1
   if res == "won" then
      money = money + value
   else
      money = money - value
   end
   _put(key .. ":r", round)
   _put(key .. ":m", money)
   _put(key .. ":n", newnum)
   local call = "Round " .. round .. " Bet!\\n"
   if round > MAXROUND or money < 1 then
      call = "Game Over!\n"
   end
   return "The currnet number is " .. newnum .. ".\\n" ..
      newnum .. ":" .. num .. " (" .. cmp .. ") ... You " .. res .. "!\\n" ..
      "Your money is " .. money .. ".\\n" ..
      call
end

エラー処理なども織り込んでいるのでちょっと長いソースになりましたが、フローは単純なので追いやすいと思います。また余談ですが、Luaのソースを書いていると、JavaScriptとRubyを混ぜたような感じがしてきます。C、C++、Perl、Ruby、Java、JavaScriptあたりに慣れた人ならばとっつきやすいですよね。個人的には、EmacsのLuaモードの標準インデントが3なのに萌えました。それでは、実際に遊んでみましょう。

$ tcrmgr ext -xlr localhost start mikio 10000
Welcome, mikio.
The current number is 30.
Your money is 10000.
Round 1 Bet!

$ tcrmgr ext -xlr localhost high mikio 5000
The currnet number is 54.
54:30 (high) ... You won!
Your money is 15000.
Round 2 Bet!

$ tcrmgr ext -xlr localhost low mikio 3000
The currnet number is 9.
9:54 (low) ... You won!
Your money is 18000.
Round 3 Bet!

$ tcrmgr ext -xlr localhost high mikio 15000
The currnet number is 65.
65:9 (high) ... You won!
Your money is 33000.
Round 4 Bet!

$ tcrmgr ext -xlr localhost low mikio 10000
The currnet number is 75.
75:65 (high) ... You lost!
Your money is 23000.
Round 5 Bet!

$ tcrmgr ext -xlr localhost low mikio 10000
The currnet number is 65.
65:75 (low) ... You won!
Your money is 33000.
Game Over!

$ tcrmgr ext localhost over mikio
Good Bye!

ちゃんと動きましたよね? 冗談のようなプログラムですが、ロバストでスケーラブルな作りになっています(本当に真面目にやるなら、start関数で秘密のセッションキーを生成して返すべきですが)。いずれも処理能力に定評のあるTokyo TyrantとTokyo CabinetとLuaで構成されたシステムですから、おそらく10万人が同時にプレーしても大丈夫でしょう。

足あとデータベース

もうちょっと実用的な例も考えてみました。mixiの足あとデータベースです。単純なIDのリストを記録するだけならTTのputrttコマンドを用いるだけで実現できるのですが、mixiの足あとデータベースはもうちょっと小賢しい仕様なので、サーバ側でLuaが使いたくなります。その仕様とは、「同じユーザが同じ日に何度も訪問してきた場合、その日の最終の訪問時刻のみを記録し、それ以前の時刻のレコードは消去する」というものです。このようなad hocな要求をいちいちサーバの実装とプロトコルに反映していたら私の身が持ちません。そこでLuaの登場なのです。以下のようなフローを考えます。

  • 「add」という関数で、被訪問ユーザと訪問ユーザのIDを指定して、足あとを記録する。キーは被訪問ユーザのID、値は訪問ユーザのID。
  • 「list」という関数で、被訪問ユーザのIDを指定して、足あとのリストをTSV形式で取得する。キーは被訪問ユーザのID、値は最大取得件数。

足あとは、ユーザIDとタイムスタンプのペアを各々int型で表現し、それを最大60個分連結したバイナリデータとして保存します。引数や戻り値には10進数の文字列を使うが、保存はバイナリデータで行うことによって、効率性と保守性を両立させる作戦です。ここまで決まれば、あとは実装するだけです。

MAXPRINT = 60

function add(key, value)
   key = tonumber(key)
   value = tonumber(value)
   if not key or not value or key == value then
      return nil
   end
   local ksel = _pack("i", key)
   local time = os.time()
   local date = os.date("*t", time)
   date.hour = 0
   date.min = 0
   date.sec = 0
   local mintime = os.time(date)
   local vsel
   local ary = _unpack("i*", _get(ksel))
   local anum = 1
   if ary and #ary > 0 then
      local nary = {}
      local nidx = 1
      local nidxmax = MAXPRINT * 2 - 1
      for i = 1, #ary, 2 do
         if ary[i] ~= value or ary[i+1] < mintime then
            nary[nidx] = ary[i]
            nary[nidx+1] = ary[i+1]
            nidx = nidx + 2
         end
      end
      vsel = _pack("i*", nary, value, time)
      anum = (#nary / 2) + 1
      if anum > MAXPRINT then
         vsel = string.sub(vsel, MAXPRINT * -8)
         anum = MAXPRINT
      end
   else
      vsel = _pack("ii", value, time)
   end
   if not _put(ksel, vsel) then
      return nil
   end
   return anum
end

function list(key, value)
   key = tonumber(key)
   value = tonumber(value)
   if not key then
      return nil
   end
   if not value or value < 1 then
      value = MAXPRINT
   end
   local result = ""
   local ksel = _pack("i", key)
   local ary = _unpack("i*", _get(ksel))
   if ary and #ary > 0 then
      for i = #ary - 1, 0, -2 do
         if value < 1 then
            break
         end
         result = result .. ary[i] .. "\\t" .. ary[i+1] .. "\\n"
         value = value - 1
      end
   end
   return result
end

バイナリデータとの相互変換にビルトイン関数 _pack/_unpack が活躍します。最大60個の制御のためにstring.subで前方を切り捨てているのもちょっとした工夫ですね。それでは、実際に動かしてみましょう。今回もレコードロックが必要なので「-xlr」をつけて呼び出しています。

$ ./tcrmgr ext -xlr localhost add 1 123
1
$ ./tcrmgr ext -xlr localhost add 1 456
2
$ ./tcrmgr ext -xlr localhost add 1 789
3
$ ./tcrmgr ext -xlr localhost add 2 987
1
$ ./tcrmgr ext -xlr localhost add 2 654
2
$ ./tcrmgr ext -xlr localhost add 2 987
2
$ ./tcrmgr ext -xlr localhost list 1
789     1222150402
456     1222150398
123     1222150392

$ ./tcrmgr ext -xlr localhost list 2
987     1222150419
654     1222150416

このプログラムは冗談でなくかなり実用を意識して作っています。ベンチマークテストを動かしたところ、20000QPS以上の性能は出るようなので、おそらくmixiの本番で運用しても耐えるでしょう(必然性がないのでやらないけど)。TTの起動時にレプリケーションの設定をしている場合、ビルトイン関数「_put」や「_out」などによる更新ももちろん更新ログとして伝播するので、マスタにaddしてスレーブからlistするという使い方もできます。

まとめ

スクリプト言語Luaについて簡単に説明し、それをTTに組み込んで使ってみました。Luaは実用に耐える機能と性能と拡張性を備えた言語なので、ビジネスシーンでもこれからメジャーになってくる予感がします。データベースサーバでスクリプト言語を動かすなんて効率が悪いと考える人も多いかもしれませんが、CPUの並列処理性能がどんどん進化しつつあるこのご時勢では、DBM等のI/O部分(通常ここがボトルネック)のレイテンシに比べれば、スクリプト言語によるレイテンシなんてのは無いみたいなもんです。並列性が高ければたとえパフォーマンスが芳しくなくてもスループットは出ます。しかもLuaのパフォーマンスはスクリプト言語の中ではかなり好成績です(ベンチマーク)。よって、もっとLuaを使いたおしましょう。

Luaを組み込む前のTTは単なるハッシュデータベースのネットワークインターフェイスだったのですが、Luaのおかげで今やアプリケーションコンテナっぽく使うこともできるようになったわけです。mixiのようなWebサイトのバックエンドとしても、例で紹介した足あとデータベースの他に、キューサービスやキャッシュサービスなどとして実用になると思っています。実際にTTとLuaを使ったソリューションができたらまたここで紹介します。

追伸1:Tokyo (Cabinet|Tyrant|Dystopia)のコミュニティをmixi上ではじめました。現状唯一のサポートの場なので、興味のある方はご参加ください。

追伸2:Linuxでしか動かなかったTokyo Tyrantですが、epollのエミュレーション層をkqueueを使って書いたので、FreeBSDとMac OS Xでも動くようになっています(多分)。該当のプラットフォームの方はぜひ使ってみてください。