memcached 1.4の到来

第4回memcachedのCASとmixiでの運用動向

今まで簡単に触れてきたmemcachedのCAS(Compare and Swap)機能ですが、今回はその具体的な使用例や、プロトコルの違いによる特徴を紹介します。また、mixiでの今後のmemcached運用動向を紹介します。

CASの概要

memcachedには特定のデータに対してアトミックな更新を試みる機能が存在します。この機能の仕組みは単純で、クライアントは特定のコマンド(テキストプロトコルの場合は⁠gets⁠⁠)を実行することにより、サーバから特定のレコードとその状態を表すユニークな識別子を与えられます。 この識別子はレコードが何らかの手段によって更新されると変更され、クライアントが保持している識別子とは別の値になります。したがって、クライアントは与えられた識別子を更新命令と一緒に送信することで、サーバはレコードをアトミックに更新できるかを確認することができます。もし識別子が適合しなければ、memcachedはCASベースの更新リクエストに対してエラーを返します。 以下がテキストプロトコルとtelnetを使ってCAS値を取得する例です。

まずレコードをセットする
set gihyo 0 0 8
original
STORED

getsコマンドでレコードとそのCAS値を取得する
gets gihyo
VALUE gihyo 0 8 1
original

テキストプロトコルでは、getsコマンドに対するレスポンスのVALUE行の最後の数値がCAS値であると定められています。したがって、クライアントが取得したCAS値は⁠1⁠ということになります。次にそのCAS値とCASコマンドを使ってレコードを更新してみます。

CASコマンドでレコードを更新する
cas gihyo 0 0 7 1
updated
STORED

再びgetsコマンドで更新されたCAS値を取得する
新しいCAS値は "2"
gets gihyo
VALUE gihyo 0 7 2
updated
END

getsのレスポンスを見てのとおり、CAS処理によりCAS値とレコードの値が更新されました。次は故意に無効になったCAS値を使ってレコードを更新してみます。

無効なCAS値 "1" でレコードを "updated2" に更新してみる
cas gihyo 0 0 8 1
updated2
EXISTS

通常のgetコマンドでレコードを取得する
レコードが更新されていないところに着目
get gihyo
VALUE gihyo 0 8
updated
END

無効なCAS値で更新を試みた結果、⁠EXISTS⁠というエラーメッセージがサーバから返され、レコードが更新されていない事がわかります。

CASとバイナリプロトコルの関係

テキストプロトコルでは、CAS値はgetsコマンドを発行することにより取得しますが、バイナリプロトコルではほぼすべてのコマンドに対してレコードのCAS値を取得できる仕様になっています。バイナリプロトコルでは24バイト固定長リクエスト・レスポンスヘッダ内の8バイトをCAS値に割り当ており、その領域をつかってCAS値が送受信されます。 以下がlibmemcached-0.31とバイナリプロトコルを使って単体レコードのCAS値を取得する例です。

バイナリプロトコルを適用する
memcached_behavior_set(handle, MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);

レコードをセットする
memcached_return rv;
const char *key = "some_key";
const char *value = "a value";

rv = memcached_set(handle, key, strlen(key), value, strlen(value), 0, 0);

if (rv != MEMCACHED_SUCCESS) {
  /* エラー処理 */
}

レコードを取得する
memcached_result_st *results;
memcached_result_st *results_obj;
size_t key_length;
int num_records;

/* 構造体にメモリを割り当てる */
results = memcached_result_create(memc, &results_obj); 

if (results == NULL) {
  /* エラー処理 */
}

key_length = strlen(key);
num_records = 1;

/* GETリクエストを発行する */
rv = memcached_mget(memc, (char**)&key, &key_length, num_records);

if (rv != MEMCACHED_SUCCESS) {
  /* エラー処理 */
}

results = memcached_fetch_result(memc, &results_obj, &rv);

if (results == NULL) {
  /* エラー処理 */
}

/* CAS値を結果オブジェクトから取得する */
uint64_t cas_id = memcached_result_cas(results);

/* stdoutに値を出力 */
printf("cas id: %lld\n", (long long int) cas_id);

/* 結果オブジェクトのメモリを解放する */
memcached_result_free(&results_obj);

CAS機能を無効にしてサーバのメモリ使用量を減らす

CASの値は64ビット整数によって表現されており、このデータは今までは個々のレコードを表現する構造体のメンバー変数として記録されていました。memcached-1.4では構造体からCAS値用の変数が取り除かれ、CAS値のデータがレコードの隣にアラインされます。この仕組みを開発したことにより、memcachedはスタートアップ時に-Cオプションを与えることで、CAS機能を無効にし、1レコードに対して8バイトのメモリ領域を節約することができます。この機能の詳細はミクシィエンジニアブログの ⁠memcached-1.4 RCを使ってみよう」というエントリをご覧ください。

mixiでのmemcached動向

memcachedとリサイクル

memcachedはmixiのインフラでRead処理のスケーラビリティ向上と高速化の重要な役目を担うコンポーネントとなっています。memcached専用として稼働している物理サーバはおよそ200台(2009年9月時点)にのぼります。mixiではデータベース用の物理サーバを定期的にスケールアップしており、引退した旧型の物理サーバをmemcached専用に再利用するというリサイクル方式でキャッシュクラスタを構築しています。この辺りの話は、以前にミクシィ 運用グループの長野が執筆した、 ⁠memcachedの運用と互換アプリケーション」をご覧ください。

ソフトウェア面における最近の課題

2009年9月時点でmixiではCache::Memcached::Fastという内部がC言語で記述されているCPANモジュールをクライアントライブラリに使っています。このCache::Memcached::Fastですが、最近の0.0.7アップデートでConsistent Hashingの実装が変更され、アプリケーションサーバ上のライブラリを容易にアップデートできないという問題が生じました。容易にアップデートするとシステム全体のキャッシュヒット率が低下し、mixiのデータベースインフラに予測以上の負荷を与えてしまうからです。 この問題に対してミクシィ運用グループは独自に古いConsistent Hashingの実装をライブラリに組み込み、オプションで両方どちらかの実装を選択できるオプションを追加しました。これによりmemcachedクラスタへの書き込みは新しい実装と古い実装を両方使って二重に書き込み、3日間のウォームアップを行いました。 ウォームアップ後はアプリケーションサーバ群のライブラリを正規のCache::Memcached::Fastの最新版に置き換えてアップデートを完了しました。あえて冗長なデータを書き込むという行為はリソースを無駄に使ったり、後片付けが面倒に聞こえるかもしれませんが、古いデータを放置してもexpireされるか、LRUによって排除されるため、memcachedならではの解決法といえます。

スケールアップとスケールアウトのバランス

ミクシィ運用グループではmemcachedに割り当てている物理サーバの台数や電力使用量の増加を抑えるソリューションを模索しています。現状稼働している多くのサーバは旧型のPentium 4(メモリ容量4Gバイト)であり、これらのサーバを低電力でメモリ容量が、ほどよく大きい新型のサーバにスケールアップすることを検討しています。目標はデータセンタのサーバラックを減らすことですが、サーバ単体の容量が増大するだけでは次に紹介する問題が生じるため、運用グループではこの課題へのソリューションを慎重に模索しています。

mixiでは総容量だけではなく稼働性も重視する

memcachedは高速性に着目されがちですが、memcachedの真価はスケールアウトの容易さにあります。Webサービスにとって、キャッシュノードがn台落ちても、ユーザに強いストレスを感じさせることなくサービスを稼働し続けることが重要です。memcachedはサーバをn台並べる事によって、データ回収の負荷だけではなく、サービスが稼働不能になる「リスクを分散」できることに価値があるのです。 ここ1年ほどで巨大キャッシュ容量サーバが市場に出回りはじめました。ここで仮定として、今まで200台で運用していたクラスタを4台のサーバに置き換えたとします。ラックの賃料や人的メンテナンスの設備投資の低下を考慮に入れると素敵な話ですが、もし万が一にサーバが落ちてしまったら、それだけ膨大なリクエストがデータベース層に直撃します。こうなった場合、サービスの体感速度が劇的に悪化するか、最悪の場合はコンテンツの表示に失敗してしまうなど、サービスプロバイダとしてあってはならない状況を生んでしまいます。 これらの巨大デバイスはコンポーネントを階層化することにより、完全には落ちない設計をしているものもあるそうですが、mixiでは高価な機器でも「いずれ壊れるか、なにか周りのものが壊れる」という思想の基にインフラを構築しています。 また、現実的な問題としてネットワーク帯域の制限を考慮にいれると単に台数を減らせばよいという問題ではありません。Map/Reduceでも同様の話がありますが、計算量やリソースを分散するだけではなく、ネットワークトラフィックをいかにスマートに分散するかというノウハウもクラスタ運用で重要な要素です。

まとめ

memcached 1.4に新しく追加された機能や通信プロトコルを紹介させていただいた本連載ですが、今回で終了となります。最後まで読んでいただきありがとうございます。もしmemcachedや本連載に関して疑問や質問がございましたら、 memcached Users Group :: Japanまたは 私の方にメールを頂けたらなと思います。なるべく速く疑問にお答えいたします。 memcachedはこれからも成長し続け、ストレージ抽象化などの新たな可能性を開いていきます。それらの最新動向をこれからも逐一に紹介させていただきたいと思います。 あらためて、ありがとうございました。

おすすめ記事

記事・ニュース一覧