myfinderの技術や周辺的活動のblog

2009年1月27日火曜日

人生に役立つかもしれないTokyo Tyrantについての知識

タイトルはホッテントリメーカーから頂戴しました。

お仕事でmemcachedとかその周辺プロダクトを利用することとなり、memcached、Tokyo Tyrant、Flareのパフォーマンスを測ってみたときに発覚したことを書いておきます。
(前提条件:memcached client for Javaからmemcachedとかその互換プロダクトを利用する場合)

まずはパフォーマンスの手っ取り早い指標として処理速度があげられるので、上記3プロダクトをセットアップして、下記のようなテストプログラムをそそくさとこさえました。


public static void main(String[] args) {

// memcacheのインスタンス取得
SockIOPool pool = SockIOPool.getInstance();
pool.setServers(new String[] {"hostname:port"});
pool.initialize();
MemCachedClient memcache = new MemCachedClient();

// 計測用
long start = 0;
long finish = 0;

// setのパフォーマンステスト
System.out.println("setのテスト開始...");
start = System.currentTimeMillis();
// setのテスト
for (Iterator<Map.Entry<String, String>> it = TEST_DATA_HASH.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, String> entry = it.next();
memcache.set(entry.getKey(), entry.getValue());
}
finish = System.currentTimeMillis();
System.out.println(TEST_DATA_HASH.size() + "件のsetにかかった時間 : " + (finish - start) + "ms");

// getのパフォーマンステスト
System.out.println("getのテスト開始...");
start = System.currentTimeMillis();
int count = 0;
for (Iterator<Map.Entry<String, String>> it = TEST_DATA_HASH.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, String> entry = it.next();
String value = "";
value = (String)memcache.get(entry.getKey());
// get1万件ごとにprintして確認
if ((count % 10000) == 0) {
System.out.println(value);
}
++count;
}
finish = System.currentTimeMillis();
System.out.println(TEST_DATA_HASH.size() + "件のgetにかかった時間 : " + (finish - start) + "ms");
}

データはおよそ10万件の固定長(32byte)のキーと、100-300byte程度の英数字記号のみの文字列。
テストに使ったマシンはP4 2.8GHzにRAM 3GBです。

プロダクトsetにかかった時間(ms)getにかかった時間(ms)
memcached3412658182
Tokyo Tyrant3617063023
Flare4267462938

値自体はおおよそ想定された内容だった。(TTのsetがこれほど速いとは思ってなかったけど)
のだが、Tokyo Tyrantのgetパフォーマンスを計測している時に異変が起こりました。
さっきsetした文字列データをgetするのだから、文字列がprintされるはずなのに、「null」と出力されてしまっているのです。
(つまりnullが返ってきている)

これはどうしたものかと思い、直接telnetしてmemcacheと対話してみました。

・memcached
user@host:$ telnet host 11211
Trying host...
Connected to host.
Escape character is '^]'.
get keykeykeykeykeykeykeykeykeykeyke
VALUE keykeykeykeykeykeykeykeykeykeyke 32 64
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
END
quit
Connection closed by foreign host.

・Tokyo Tyrant
user@host:$ telnet host 1978
Trying host...
Connected to host.
Escape character is '^]'.
get keykeykeykeykeykeykeykeykeykeyke
VALUE keykeykeykeykeykeykeykeykeykeyke 0 64
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
END
quit
Connection closed by foreign host.

・Flare
user@host:$ telnet host 12121
Trying host...
Connected to host.
Escape character is '^]'.
get keykeykeykeykeykeykeykeykeykeyke
VALUE keykeykeykeykeykeykeykeykeykeyke 32 64
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
END
Connection closed by foreign host.

結果、Tokyo Tyrantだけflagが0で返ってくる。
telnetで確認してみるとデータは返ってくるのに、nullになってしまうのはなぜかとmemcached client for javaのflag処理がどうなっているかを追っていき、そのflagの処理を見てみると

public static Object decode( byte[] b, int flag ) throws Exception {
if ( b.length < 1 )
return null;
if ( ( flag & MemCachedClient.MARKER_BYTE ) == MemCachedClient.MARKER_BYTE )
return decodeByte( b );
if ( ( flag & MemCachedClient.MARKER_BOOLEAN ) == MemCachedClient.MARKER_BOOLEAN )
return decodeBoolean( b );
if ( ( flag & MemCachedClient.MARKER_INTEGER ) == MemCachedClient.MARKER_INTEGER )
return decodeInteger( b );
if ( ( flag & MemCachedClient.MARKER_LONG ) == MemCachedClient.MARKER_LONG )
return decodeLong( b );
if ( ( flag & MemCachedClient.MARKER_CHARACTER ) == MemCachedClient.MARKER_CHARACTER )
return decodeCharacter( b );
if ( ( flag & MemCachedClient.MARKER_STRING ) == MemCachedClient.MARKER_STRING )
return decodeString( b );
if ( ( flag & MemCachedClient.MARKER_STRINGBUFFER ) == MemCachedClient.MARKER_STRINGBUFFER )
return decodeStringBuffer( b );
if ( ( flag & MemCachedClient.MARKER_FLOAT ) == MemCachedClient.MARKER_FLOAT )
return decodeFloat( b );
if ( ( flag & MemCachedClient.MARKER_SHORT ) == MemCachedClient.MARKER_SHORT )
return decodeShort( b );
if ( ( flag & MemCachedClient.MARKER_DOUBLE ) == MemCachedClient.MARKER_DOUBLE )
return decodeDouble( b );
if ( ( flag & MemCachedClient.MARKER_DATE ) == MemCachedClient.MARKER_DATE )
return decodeDate( b );
if ( ( flag & MemCachedClient.MARKER_STRINGBUILDER ) == MemCachedClient.MARKER_STRINGBUILDER )
return decodeStringBuilder( b );
if ( ( flag & MemCachedClient.MARKER_BYTEARR ) == MemCachedClient.MARKER_BYTEARR )
return decodeByteArr( b );
return null;
}

となっていました。
この処理では、受け取ったデータをどのクラスにキャストするかをflagsで判断しているようなのですが、flagsに0が返ってきてしまうTokyo Tyrantではどのflagにも該当しません。
よってreturn nullされて、データはgetできているにも関わらずnullとなってしまうという問題が起こっていたことが見て取れます。

これって大丈夫なのかな〜。
全部文字列(Stringなど)として受け取るという前提でTTは実装されているのかも。
(myfinderの認識が間違っていたら申し訳ないのですが)

型に緩いLLならそもそもキャストする処理が不要なのでそもそもクライアントライブラリがflagを見ていないのかもしれない。
(mixiはPerlから利用しているみたいだし)

というかクライアントライブラリも、あり得ないflagが返ってきたなら例外をスローしたほうがいいんじゃないかというツッコミを入れたいところ。

少なくともmemcached client for JavaからTokyo Tyrantを利用するときは、そういうことがあるということは覚えておかないと、setできるけどgetするとnullが返ってくるという、ブラックホール状態になってしまうので注意です。

結論としては、Tokyo Tyrantをmemcached client for Javaから使う場合、改修をする必要があり、それがいやならmemcachedクライアントを自作するなどの対策が必要ですよというオチでした。
パフォーマンス計測結果とその計測プログラムは参考程度にお使いください。

では。

3 件のコメント:

ふじもと さんのコメント...

Flareも混ぜていただいてありがとうございますm(_ _)m (作者でございます)

setは実は裏でデータバージョニングしてたりするのでやっぱり遅くなるなぁ、というのがありつつ、getのパフォーマンスはそこそこいってて嬉しかったです。

また機会がありましたらFlareさんも使ってやってください。

tokuhirom さんのコメント...

TokyoTyrant は flags と expires を無視してくれるので、perl でも、「文字列しかセットしない」or「なんかしらのシリアライザで文字列化したものをいれる」という使い方をしている場合が多いかとおもいます。

myfinder さんのコメント...

tokuhiromさん

情報ありがとうございます。

続報の
http://blog.myfinder.jp/2009/01/tokyo-tyrant_28.html
でその辺について触れています。

そのときはclientを改修して対応しましたが、そういった仕様は事前にちゃんと確認しないといけませんね。。。