Kyoto Tycoon+Lua-JIT拡張+MessagePack=無敵
はじめに
http://fallabs.com/blog-ja/promenade.cgi?id=100
まとめ
データベースサーバ上でのスクリプティング用途ではLuaは最強の一角であり、そしてKCのVisitorインターフェイスとの相性も抜群である。TTの時代にもかなり面白いことができたが、KTになってからさらに便利に使えるようになった。ぜひぜひ、お試しあれ。
2年前の作者の記事ですが,まさに面白くてたまりません.
今回は,さらに簡単に性能を上げる実験をやってみましょう.
LuaとLua-JITでは,どうもLua-JITの方が性能が良いようなので,
Lua v.s. Lua-Jit - なぜか数学者にはワイン好きが多い
Lua-JIT対応のKyoto Tycoonを作ります.これは,Luaの代わりにLua-JITをリンクするだけで作ることができます.
その上で,Kyoto Tycoonを簡単に拡張してみます.
Kyoto Tycoonでは,Luaスクリプティング拡張をかぶせるだけで,基本的なKVSのSet/Getすらあっさりと機能拡張できます.
まずは手早く実験するために,性能は余り気にせず生産性の高さのためにRubyを使います.
RubyのKyoto Tycoonバインディングは,オフィシャルにサポートされています.というのはウソで,ソケット通信やHTTP通信でアクセスができるので,Rubyじゃなくてもオフィシャルにサポートされていると言えます.
Fundamental Specifications of Kyoto Tycoon Version 1
次に,MessagePackのRubyバインディング.これはいくつか発表されていますが,一番シンプルなgemでインストールできるものを使いました.MessagePackの作者によるものですね.
msgpack | RubyGems.org | your community gem host
Luaに対するMessagePackも沢山ありますが,Lua-JIT対応で他ライブラリへの依存が少ないものを探しました.
幸い,まとめ記事を書いてくれている人がいました.
The state of MessagePack in Lua · GitHub
The state of MessagePack in Lua
So, to sum things up:
Lua-JITで依存が少ないということで,私はluajit-msgpack-pureを使いました.
Kyoto TycoonのSet/Get命令のスクリプトによる乗っ取り
Kyoto Tycoon(バックエンドはKyoto Cabinet)は,基本的にキーもバリューも文字列だろうがバイナリデータだろうが何でも突っ込めます.単純な文字列-文字列のKVSじゃなくて,配列やハッシュなどの構造を持ち込みたい,通常はJSONなんかにシリアライズして突っ込むのですが,何分,JSONは空間効率も時間効率も悪いです.JSONにバイナリを持ち込んだBSONも,特に効率が良いわけでは無かったというのが私の結論.JSON/BSON/MessagePack 処理速度・データサイズ完全比較 - なぜか数学者にはワイン好きが多い
そこで,getもsetもLuaで書いてしまいます.
通信プロトコルも,頑張ってHTTPじゃなくてバイナリプロトコルで行きます.
とは言っても,途中のプロトコルはgetやsetを処理するLuaスクリプトには関係が無いので,まず順にget/setルーチンをさらけ出してみます.
- 通常のKyoto Tycoon Serialize/Deserializeによるget/set
-- set a record by Kyoto serializer function set(inmap, outmap) -- Bulkではなく,Key/Valueのペアを一組ずつsetすることを仮定 local key, value = next(inmap) local err = false local serial = kt.mapdump(inmap) -- visitor function local function visit(key, value, xt) return serial end -- perform the visitor atomically if not db:accept(key, visit) then kt.log("error", "inserting a record failed") err = true end if err then return kt.EVEINTERNAL end return kt.RVSUCCESS end -- get a record by Kyoto deserializer function get(inmap, outmap) -- getは,固定文字列keyに対するvalueがkey名と仮定 local key = tostring(inmap.key) if not key then return kt.RVEINVALID end local serial = db:get(key) if not serial then return kt.RVELOGIC end local rec = kt.mapload(serial) for rkey, rvalue in pairs(rec) do outmap[rkey] = rvalue end return kt.RVSUCCESS end
普通にHTTPのASCIIプロトコルでアクセスすることもできます.上記のプログラムをkt_ex1.luaというファイル名で保存するとすると,Kyoto Tycoonサーバの立ちあげはこんな感じです.
ktserver -ls -scr kt_ex1.lua data.kch
cURLでアクセスしてみます.setはこんな感じ.
> curl "http://192.168.0.100:1978/rpc/play_script?name=set&_abcde-key1=abcde-value1"
「abcde-key1」がキーで,「abcde-value1」がバリューです.getはこんな感じ.
> curl "http://gauss:1978/rpc/play_script?name=get&_key=abcde-key1" _abcde-key1 abcde-value1
はい,引数で渡したキー「abcde-key1」と,それに対するバリューがタブ区切りで飛んできました.
-- set a record by String serializer function setraw(inmap, outmap) -- Bulkではなく,Key/Valueのペアを一組ずつsetすることを仮定 local key, value = next(inmap) if not key then return kt.RVEINVALID end local err = false local serial = value -- visitor function local function visit(key, value, xt) return serial end -- perform the visitor atomically if not db:accept(key, visit) then kt.log("error", "inserting a record failed") err = true end if err then return kt.EVEINTERNAL end return kt.RVSUCCESS end -- get a record by String deserializer function getraw(inmap, outmap) -- getは,固定文字列keyに対するvalueがkey名と仮定 local key = tostring(inmap.key) if not key then return kt.RVEINVALID end local serial = db:get(key) if not serial then return kt.RVELOGIC end local rec = serial outmap.value=tostring(rec) return kt.RVSUCCESS end
普通にHTTPのASCIIプロトコルでアクセスすることもできます.上記のプログラムをkt_ex2.luaというファイル名で保存するとすると,Kyoto Tycoonサーバの立ちあげはこんな感じです.
ktserver -ls -scr kt_ex2.lua data.kch
cURLでアクセスしてみます.setはこんな感じ.
> curl "http://192.168.0.100:1978/rpc/play_script?name=setraw&_abcde-key1=abcde-value1"
「abcde-key1」がキーで,「abcde-value1」がバリューです.getはこんな感じ.
> curl "http://gauss:1978/rpc/play_script?name=getraw&_key=abcde-key1" _abcde-key1 abcde-value1
はい,引数で渡したキー「abcde-key1」と,それに対するバリューがタブ区切りで飛んできました.
-- set a record by MessagePack function setmp(inmap, outmap) -- Bulkではなく,Key/Valueのペアを一組ずつsetすることを仮定 local key, value = next(inmap) if not key then return kt.RVEINVALID end local err = false local serial = value -- visitor function local function visit(key, value, xt) return serial end -- perform the visitor atomically if not db:accept(key, visit) then kt.log("error", "inserting a record failed") err = true end if err then return kt.EVEINTERNAL end return kt.RVSUCCESS end -- get a record by MessagePack deserializer function getmp(inmap, outmap) -- getは,固定文字列keyに対するvalueがkey名と仮定 local key = tostring(inmap.key) if not key then return kt.RVEINVALID end local serial = db:get(key) if not serial then return kt.RVELOGIC end local rec = serial outmap[key]=tostring(rec) return kt.RVSUCCESS end
実はMessagePackの処理は,クライアント側で行うことを想定しているので,setraw/setmpとgetraw/getmpは,全く同一です.
通常のget/setのみが,シリアライズ/デシリアライズの処理をサーバ側で行なっています.
バイナリプロトコルによるソケット通信アクセス
- 通常のKyoto Tycoonアクセスの場合
Rubyによるプログラムは,例えば次のようになるでしょう.
require "socket" # setするバリューデータを作る b = []; (1..4000).each{|item| b << item; } start = Time.now(); (1..10000).each{|i| s = TCPSocket.open("192.168.1.100", 1978); # Set by Kyto Tycoon serializer out = [0xb4].pack("c*") + [0, "set".size(), 1].pack("N*") + "set"\ + ["key#{i}".size(), Marshal.dump(b).size()].pack("N*") + "key#{i}" + Marshal.dump(b); # Marshalで配列をシリアライズする s.write(out); s.close(); # Get by Kyoto Tycoon deserializer out = [0xb4].pack("c*") + [0, "get".size(), 1].pack("N*") + "get"\ + ["key".size(), "key#{i}".size()].pack("N*") + "key" + "key#{i}"; s = TCPSocket.open("192.168.1.100", 1978); s.write(out); g = s.read(1); if g.getbyte(0) == 0xb4 then rnum = s.read(4).unpack('N*')[0]; ksize = s.read(4).unpack('N*')[0]; vsize = s.read(4).unpack('N*')[0]; key = s.read(ksize); value = Marshal.load(s.read(vsize)); # Marshalで配列をデシリアライズする s.close(); # たまに目視確認する if i % 1000 == 0 then puts "set value=#{b}" puts "get value=#{value}"; end end; } puts "time = #{Time.now()-start}";
require "socket" # setするバリューデータを作る b = []; (1..4000).each{|item| b << item; } start = Time.now(); (1..10000).each{|i| s = TCPSocket.open("192.168.1.100", 1978); # RubyのStringクラスは,JavaのbyteやCのcharのように,バイナリを扱えるので全部Stringにしちゃう # Set by String out = [0xb4].pack("c*") + [0, "setraw".size(), 1].pack("N*") + "setraw"\ + ["key#{i}".size(), b.to_s.size()].pack("N*") + "key#{i}" + b.to_s s.write(out); s.close(); # Get by String out = [0xb4].pack("c*") + [0, "getraw".size(), 1].pack("N*") + "getraw"\ + ["key".size(), "key#{i}".size()].pack("N*") + "key" + "key#{i}"; s = TCPSocket.open("192.168.1.100", 1978); s.write(out); g = s.read(1); if g.getbyte(0) == 0xb4 then rnum = s.read(4).unpack('N*')[0]; ksize = s.read(4).unpack('N*')[0]; vsize = s.read(4).unpack('N*')[0]; key = s.read(ksize); value = s.read(vsize); s.close(); # ここの目視確認ではset valueとget valueは同一に見えるが,get valueは文字列なので, # 実際に配列として使う場合は,value.splitなどで配列に変換する必要がある if i % 1000 == 0 then puts "set value=#{b}" puts "get value=#{value}"; end end; } puts "time = #{Time.now()-start}";
- MessagePackしちゃうバージョン
require "msgpack"; require "socket" # setするバリューデータを作る b = []; (1..4000).each{|item| b << item; } start = Time.now(); (1..10000).each{|i| s = TCPSocket.open("192.168.1.100", 1978); # Set by MessagePack serializer out = [0xb4].pack("c*") + [0, "setmp".size(), 1].pack("N*") + "setmp"\ + ["key#{i}".size(), MessagePack::pack(b).size()].pack("N*") + "key#{i}" + MessagePack::pack(b) s.write(out); s.close(); # Get by MessagePack deserializer out = [0xb4].pack("c*") + [0, "getmp".size(), 1].pack("N*") + "getmp"\ + ["key".size(), "key#{i}".size()].pack("N*") + "key" + "key#{i}"; s = TCPSocket.open("192.168.1.100", 1978); s.write(out); g = s.read(1); if g.getbyte(0) == 0xb4 then rnum = s.read(4).unpack('N*')[0]; ksize = s.read(4).unpack('N*')[0]; vsize = s.read(4).unpack('N*')[0]; key = s.read(ksize); value = MessagePack::unpack(s.read(vsize)); s.close(); # たまに目視確認する if i % 1000 == 0 then puts "set value=#{b}" puts "get value=#{value}"; end end; } puts "time = #{Time.now()-start}";
ベンチマーク結果
3つは本当に良く似ています.通常バージョンとMessagePackバージョンは,RubyのMarshalを使うかMessagePackを使うかの違いでシリアライズ/デシリアライズしてgetしたら配列としてすぐに使える形ですし,StringバージョンとMessagePackバージョンはサーバ側プログラムが同一です.また,StringバージョンのクライアントプログラムをKyoto Tycoonシリアライズのサーバプログラムに突っ込んでもそのまま動きます.
にも関わらず,性能差が出ますので紹介します.
比較は,バリューの長さを変化させたときの,実行時間とHash KVSファイルのサイズを見ることにします.
まずは実行時間です.
MessagePackバージョンがダントツで速いです.というからには,原因はファイルサイズによるのでしょうと思って見てみますと...
やはりMessagePackバージョンのファイルサイズが小さいです.