mixi engineer blog

*** 引っ越しました。最新の情報はこちら → https://medium.com/mixi-developers *** ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

libmemcachedで快速キャッシュ生活

みんな大好きなmemcached。今日はBrian AkerのC言語用クライエントライブラリについて書きたいと思います。日本語の情報がとても少なく、ドキュメンテーションも英語だけという事で興味はあるけど手をつけていないという方のお役に立てれたらなと思います。

本題の前に why libmemcached?

既にlibmemcacheが存在するのに何故、libmemcached?かと言うと理由の一つは最近libmemcacheの開発が止まったからです。本家ではそれが理由でlibmemcacheではなくlibmemcachedを推奨してますね。又、効率的なメモリ使用、Consistent Hashing、様々なハッシュアルゴリズム、新しいオペレータに対応している等という宣伝文句があります。apr_memcacheというライブラリも存在しますが自分は使った事がないためノーコメント。 ただ、推奨されているという理由だけでは大して嬉しくないのは私だけではないはず。ということで私が現在知っているメリットを述べさせて頂きます。

使うと嬉しい理由

嬉しい理由の一つはlibmemcachedを使う事によってmemcachedを分散させたシステムを高速化できるという事です。例えば従来の方法通りに<key, value>をクライエント任せで登録するのではなく、libmemcachedではmemcached_set_by_key(3)という関数を使う事によって<key, value>データを<master_key, key, value>という形式でSETできます。

で、master_keyが何かというとクライエントアプリケーションがmaster_keyとkeyをひもづける事でデータを散らばらせず、特定のサーバに保存するためのkeyです(つまり関連データをクラスタ内で一箇所に集中させられる)。このメカニズムがあるというまでもなくmgetが段違いに速くなると仮定できます。なぜなら無駄に複数のサーバに接続してデータ回収を行うネットワークコストが抑られるから。

分散環境でもう一つ嬉しい点を述べるとconsistent hashingを用いる事によって、ノードのサービスイン・サービスアウトの際に必ず生じるデータのズレが限りなく小さくなるという点ですが、この件に関して書き始めると読者の眠気を誘う記事になってしまうので、これは後日書きたいと思います。

本当に?

喉から手が出るほどmemcached_(set|get)_by_key(3)の性能テストを行いたいのですが、やはり行うにはサーバを10台くらい用意したいので、後日結果を公開したいと思います。 実際に宣伝通りに素晴らしいソフトウェアかを語るには少なくとも自分で手を動かさないといけないのでlibmemcachedを使ってみました。それでもってサンプルアプリを作ったので、興味のある方は目を通して頂けたら光栄です(記事の最後の方)。

libmemcachedを使ってみよう

使い方は人それぞれですが、ここでは単純にデータのSETとGETの方法の一つを紹介します。基本的に使用するパーツは:

  • memcached_st (サーバとのやりとりに使用する構造体)
  • memcached_server_st (サーバ情報やそのbufferを持つ構造体)
  • memcached_return (ほぼ全ての関数が返す返り値)

と以下の関数:

  • memcached_create(3)
  • memcached_servers_parse(3)
  • memcached_server_push(3)
  • memcached_set(3)
  • memcached_get(3)
  • memcached_server_list_free(3)
  • memcached_free(3)

だけです。includeするヘッダーは "memcached.h" だけ。

まず最初に必要なパーツの定義:

struct memcached_st *mymmc;
struct memcached_server_st *myservers;
memcached_return rc;
char *key = "yome";
char *val = "hiiragi_kagami";
char *hostname;

何をするにも必要なmemcached_stを作る:

mymmc = memcached_create(NULL);

予め作ってあるmemcached_stをアサインする事も可能。その構造体へのポインタをmemcached_createに渡すだけで良い。次にサーバ接続への仕込みをします。ドキュメントには「接続する」と書いていますがソースを読んだらここでは接続は発生しないようだから「仕込み」とあえて言います。 サーバ接続の準備・仕込み:

myservers = memcached_servers_parse((char*)hostname);
rc = memcached_server_push(mymmc, myservers);
assert(rc == MEMCACHED_SUCCESS);

上記のコードは一つのサーバでも複数のサーバでも関係なく動作します。

他にも色々と接続手段がありますので気になる方はlibmemcachedのインストール後、 "man 3 memcached_server_add" に目を通してみてください。実際に接続が行われるのは"lib/memcached_do.c"の中のmemcached_do関数の模様 (もっと正確には"lib/memcached_connect.c"のmemcached_connectを使って)。

次にお好みで色々なサーバの設定が可能です。これはmemcached_behavior_set(3)で行います。例えば使うハッシュアルゴリズムを以下から選べます:


    • MEMCACHED_HASH_DEFAULT

    • MEMCACHED_HASH_MD5

    • MEMCACHED_HASH_CRC

    • MEMCACHED_HASH_FNV1_64

    • MEMCACHED_HASH_FNV1A_64

    • MEMCACHED_HASH_FNV1_32

    • MEMCACHED_HASH_FNV1A_32

    • MEMCACHED_HASH_KETAMA

他に何が設定できるかなどはlibmemcachedのインストール後に "man 3 memcached_behavior_set" で読む事ができます。 で、次にデータのSET:

/* key = "yome", val = "hiiragi_kagami" */
rc = memcached_set(mymmc, key, strlen(key), val, strlen(val),
                   (time_t)0, (uint16_t)0);

if(rc != MEMCACHED_SUCCESS) {
  fprintf(stderr, "failed to SET\\n");
}

KeyでGET:

char *ret; /* 返ってきたデータへのポインタ */
size_t val_len;
ret = memcached_get(mymmc, key, klen, &val_len, (uint16_t)0, &rc);

if(rc != MEMCACHED_SUCCESS) {
  fprintf(stderr, "failed to GET\\n");
}

free(ret); /* 使い終わったらfree(3)しよう */

Multi Getしたい場合はmemcached_mgetとmemcached_fetchを使って行います。詳しくは "man 3 memcached_mget" に記載されています。 使い終わったmyserversとmymmcはきちんと開放:

memcached_server_list_free(myservers);
memcached_free(memc);

とまあ見ての通り使い方は簡素です。

簡単なプログラムを書いてみた

正直な話、私は何かデモを書かないとライブラリ等を理解できない不器用なプログラマです。だからlibmemcached-0.12の感触をつかむために簡単なmemcached単体・クラスタのパフォーマンスをテストするツールを書いてみました。

memstorm-0.6.zip
追記: libmemcached-0.13でエラーが生じる件は原因を判明しパッチを送りましたので、memstormのお試しにはlibmemcached-0.12をお使いください。

ビルド&インストールはアーカイブを展開した後にディレクトリに入って:

./configure
make
make install

で行います(インストールまでする必要はないかも)。あ、それと当然ながら最初にlibmemcachedをシステムにインストールしていないとビルドできません。

memstormを使ってみよう

使い方は超簡単、例えばtokyoとkyotoというサーバにそれぞれmemcachedをデフォルトポートに立ち上げていると仮定しましょう。

memstorm -s kyoto,tokyo -n 10000 -k 64 -l 10240

上記を実行したらmemstormはkyotoとtokyoに10000回、10240バイトのデータを64バイトのKeyでSETしてレコードを同じ回数だけGETします。つまり合計100MB近いテストデータを使ったテストです。 デフォルトポートを使っていない場合、例えばkyotoはデフォルトポートだけれどtokyoでは11311をlistenしている場合は以下のようにmemstormを実行します:

memstorm -s kyoto,tokyo:11311 -n 10000 -k 64 - 10240

普段はあまり意味がないけど複数のサーバと通信しているかを確認するために同一サーバでmemcached 1.2.4のインスタンスを二つ立ち上げた環境ではこんな結果でした: memstormの実行:

memstorm -s tokyo:11211,tokyo:11311 -n 10000 -k 64 -l 10240

結果:

Num of Records      : 10000
Non-Blocking IO     : 0
TCP No-Delay        : 0

Successful   [SET]  : 10000
Failed       [SET]  : 0
Total Time   [SET]  : 13.61078s
Average Time [SET]  : 0.00136s

Successful   [GET]  : 10000
Failed       [GET]  : 0
Total Time   [GET]  : 12.63989s
Average Time [GET]  : 0.00126s

非同期モードで同じテストを実行した結果、バグった?と思わされるほどSETの性能が飛躍的に向上したのが解ります: memstormの実行:

memstorm -s tokyo:11211,tokyo:11311 -n 10000 -k 64 -l 10240 -b

結果:

Num of Records      : 10000
Non-Blocking IO     : 1
TCP No-Delay        : 0

Successful   [SET]  : 10000
Failed       [SET]  : 0
Total Time   [SET]  : 1.56849s
Average Time [SET]  : 0.00016s

Successful   [GET]  : 10000
Failed       [GET]  : 0
Total Time   [GET]  : 22.10626s
Average Time [GET]  : 0.00221s

SETは確かに早くなっていますが引き換えに私の環境ではGETが遅くなっている模様。ただし遅いと言ってもAverage Timeを見ると爆速でデータの出し入れが行われているのが解ります。

普通に1インスタンス(同期モード)だけ立ち上がっていても2インスタンス(同期モード)とパフォーマンスは変わらないっぽい:

memstormの実行:

memstorm -s tokyo:11211 -n 10000 -k 64 -l 10240

結果:

Num of Records      : 10000
Non-Blocking IO     : 0
TCP No-Delay        : 0

Successful   [SET]  : 10000
Failed       [SET]  : 0
Total Time   [SET]  : 13.62225s
Average Time [SET]  : 0.00136s

Successful   [GET]  : 10000
Failed       [GET]  : 0
Total Time   [GET]  : 12.14061s
Average Time [GET]  : 0.00121s

では非同期通信だとどうなるのでしょう?

memstorm -s tokyo -n 10000 -k 64 -l 10240 -b

結果:

Num of Records      : 10000
Non-Blocking IO     : 1
TCP No-Delay        : 0

Successful   [SET]  : 10000
Failed       [SET]  : 0
Total Time   [SET]  : 1.53818s
Average Time [SET]  : 0.00015s

Successful   [GET]  : 10000
Failed       [GET]  : 0
Total Time   [GET]  : 22.08139s
Average Time [GET]  : 0.00221s

なるほど、先ほどと同様にSETは早くなってGETが遅くなっていますね。ただ、このテストだけではあまり有力な判断材料にはならないためConcurrency(並列)テストも出来るように書こうかなと考えたのですが、これから紹介するmemslapはそれが既に出来るので今回はノータッチ。

memslap

memslapとはlibmemcachedと一緒に配布されている負荷テスト・ベンチマークツールです。生みの親が同じである事からmysqlslapの兄弟です。これを使って何が嬉しいかというと並列してサーバに負荷をかけられる事です(mysqlslapはエンジンを変えたりもっと色々できます)。 で、memslapを使うとmemcachedを普段から使っている人の疑問である:

  • thread有効オプションでコンパイルしたら実際に嬉しいのか?
  • やっぱロックのコストで何も変わらない、もしくは性能が劣化するのか?

などの判断材料になります。ただしkey/valueのサイズを自由に変えたり、テスト進捗が見えないのが個人的に不便だと感じました(これ、実はmemstormを書いた理由でもあります)。 ちなみに作者がいうにはmemslapは未完成との事です。実際にlibmemcached-0.12のmemslapは自分の環境だとなぜかreadが失敗しまくるんですよね。といってもこれはmemslapより私側の否であるかもしれないのでODFの日に調査してみようと思います。PerlのWrapperはDBIでお馴染みのTim Bunce氏が開発中

まとめ

今回はlibmemcachedを紹介し利用例とテストツールを書いてみました。memcachedは弊社ではかなりの台数のサーバで使い込んでいるのでこれから面白い発見などがあればブログに書いていきたいと思います。

またお会いしましょう!