memstored 0.1 = memcached + mpio + Tokyo Cabinet
memstored は memcached のバイナリプロトコルをサポートしたハッシュストレージサーバーです。IO戦略ライブラリmpio の信頼性と性能をテストするために開発しました。
IOに mp::iothreads を使用し、バックエンドには Tokyo Cabinet の抽象データベースAPIを利用しているため、高速でスケーラビリティが高く、かつ柔軟性の高いアーキテクチャになっています。プログラムの大部分はライブラリによって実現されているため、プログラム全体の見通しが良く、行数で見ても非常に小さく収まっています。
- SVN (memstored): http://svn.coderepos.org/share/lang/c/memstored/trunk
- SVN (mpio): http://svn.coderepos.org/share/lang/c/mpio/trunk
- パッケージ: memstored-0.1.tar.gz
svn co http://svn.coderepos.org/share/lang/c/mpio cd mpio/trunk && ./bootstrap && ./configure && make && make install && cd ../../ svn co http://svn.coderepos.org/share/lang/c/memstored cd memstored/trunk && ./checkout && ./bootstrap && ./configure && make && make install && cd ../../
IOアーキテクチャ
マルチコア時代の高速サーバーの実装で紹介したサンプルプログラムと同様に mp::iothreads を使っています。memstored はスレッド間で共有するリソース(=排他処理が必要)が少ないので、すべてマルチスレッドで記述しています。
新しいクライアントを accept(2) するためのスレッドが走っており、コネクションが確立されるとWorkerスレッド*1にソケットが受け渡されます。
Workerスレッドはクライアントからデータを受け取り、memcached プロトコルのパースとデータベースの操作を行います。そのスレッドでそのまま返信を返します。ここで返信のサイズが非常に大きかったときに書き込みがブロックしてしまうのを防ぐため、一度に書き込みきれなかった場合は Output 用のスレッドに受け渡します。
返信は writev(2) を使って送信しています。アドレスが連続していなくてもカーネル内で効率よく処理してくれるので、ヘッダ + データ という形式のプロトコルを効率よく送信できます。memstored はデータベースからデータを取り出してから返信が完了するまでの間に一度もデータをコピーしません*2。
この最適化はデータのサイズが大きいときに効果を発揮しそうです。
ストレージ
ストレージには Tokyo Cabinet の抽象データベースAPIを利用しています。
抽象データベースAPIはオンメモリデータベースAPIとハッシュデータベースAPIとB+木データベースAPIの共通のインターフェイスで、関数 `tcadbopen' でデータベースを開く際のデータベース名で具体的にどの種類のデータベースを扱うかを指定することができます。ハッシュデータベースの名前には接尾辞として ".tch" をつけ、B+木データベースの名前には接尾辞として ".tcb" をつけることで区別されます。チューニングパラメータは、名前の後に "#" で区切って "name=value" の形式で指定します。例えば "casket.tch#bnum=1000000#apow=10" などとします。
memstored の引数に渡すデータベース名によって、実際に利用されるデータベースの種類が決定されます。ストレージは性能に非常に大きな影響を与えるので、最適なデータベースを選ぶことが重要です。
"db.tch"と指定すればハッシュデータベース、"*#capsiz=1024m"と指定すれば128MBのオンメモリデータベース、"db.tcb#opts=d"と指定すればDeflateによる圧縮が有効になったB+木データベースになります。
マイクロベンチマーク
memcached のベンチマークツールである memslap を使って、memstored と memcached、Tokyo Tyrant の性能を比較してみました。
- サーバー環境:Linux vcore.local 2.6.22.9-vcore16 #1 SMP Sun Oct 14 22:13:32 JST 2007 x86_64 AMD Athlon(tm) 64 X2 Dual Core Processor 5000+ GNU/Linux
- クライアント環境:Linux vix1.local 2.6.22.9-vcore19 #1 SMP Mon Oct 22 22:37:21 JST 2007 x86_64 AMD Athlon(tm) 64 X2 Dual Core Processor 4800+ GNU/Linux
- memstored-0.1
- tokyocabinet-1.3.11
- tokyotyrant-1.1.4
- tmaesaka-memcached-8b03e781cf885c177d1c4b7ef0d846f893e53eec
- libmemcached-0.23
memslap --test=get --concurrency=128 [--binary]
あらかじめ1万個のエントリを書き込んでおいて、128本のスレッドでそれぞれ1万回GETを行ったときの経過時間を測定しています。キーのサイズは100バイトで、値のサイズは400バイトです。
server | storage | protocol | elapsed |
---|---|---|---|
memstored | TC Hash DB (#bnum=20011) | binary | 16.3 sec |
memstored | TC on Memory DB | binary | 15.3 sec |
memstored | TC B+Tree DB (#bnum=20011) | binary | 16.8 sec |
memcached | SLAB | binary | 15.6 sec |
memcached | SLAB | text | 16.8 sec |
Tokyo Tyrant | TC Hash DB (bnum=20011) | text | 25.0 esc |
とりあえずこのテストでは十分な性能が得られていることが分かります。
値のサイズがもっと大きい場合のテストを行ってみたかったのですが、memcached は1MB以上のキー+値に対応していない & テキストプロトコルは1MB以上の値を許可していない ため、比較ができませんでした。memstored は2^32バイトまでのキー+値に対応しています。
Usage
Usage: memstored [options] -s <database> -l <[addr:]port> options: -h --help show this message -s --store database name. see tcadb(3) for details -l <[addr:]port> --listen listen address -B <number=2048> --initial-size allocate this size of buffer first -N <number=1024> --reserve-size reserve at least this size of buffer -W <number=2> --write-threads number of threads for asynchronous writing -R <number=4> --read-threads number of threads for asynchronous reading