memcachedを知り尽くす

第3回memcachedの消去メカニズムと今後の動向

memcachedはキャッシュなので、特定のデータが常にサーバに存在しないことが前提でシステムに導入されます。今回はmemcachedのデータ削除メカニズム、そしてmemcachedの最新動向であるバイナリプロトコルと外部エンジンサポートをご紹介いたします。

memcachedはデータ削除もリソースを有効活用する

memcachedから実際にデータは消えない

前回の記事で紹介させていただきましたが、memcachedは確保したメモリを解放しません。レコードはtimeoutが過ぎたらクライエントから見えなくなる(invisible・透明になる)だけで、その領域は再利用される仕組みです。

Lazy Expiration

memcachedは内部的にレコードがexpireしたかの監視を行いません。替わりにgetする際にレコードのtimestampを見ることで、そのレコードがexpireしたかをチェックします。このテクニックをlazy(なまけた)expirationと呼びます。したがって、memcachedはexpireの監視にCPUタイムを消費しません。

LRU: 有効的にキャッシュからデータが消える仕組み

memcached はtimeoutしたレコードの領域を優先的に再利用しますが、それでも新しいレコードを追加する領域がなかった場合はLeast Recently Used(LRU)という仕組みを使って、領域を確保します。LRUはその名の通り、⁠最近もっとも使っていない」レコードを削除対象にする仕組みです。したがってmemcachedがメモリ不足になった場合slab classから領域を取れなかった場合⁠⁠、最近参照されていないレコードを検索して、その領域を新しいレコードに割り当てます。キャッシュの実用性の観点から見てこのモデルは理想的ではないでしょうか。

ユースケースによっては、LRUの仕組みが邪魔になることもあり得ます。そういった場合のために、memcachedにはスタートアップでLRUを無効化する⁠-M⁠オプションがあります。以下が起動例です:

$ memcached -M -m 1024

スタートアップ時に気をつけないといけない点は、小文字の⁠-m⁠オプションは最大メモリサイズの指定だということです。値が指定されていなければデフォルトの64MBで起動します。

“-M⁠オプションでスタートアップしてメモリを使い切ったら、memcachedはエラーを返します。ただ、やはりmemcachedはストレージではなくキャッシュなので、LRUを使うことがコミュニティから推奨されています。

memcachedの最新動向

現在、memcachedのロードマップには二つ大きな目標があります。一つはバイナリプロトコルの使用策定と実装、そしてもう一つは外部エンジンのロード機能です。

バイナリプロトコルに関して

バイナリプロトコルを採用した理由は、テキストプロトコルのパース処理を省き、既に高速なmemcachedのパフォーマンスをさらに向上させることが狙いです。また、テキストプロトコルならではの脆弱性を減らす目的もあります。実装は実はかなりできていて、開発レポジトリが既にウェブに公開されています。レポジトリへのリンクはmemcachedのダウンロードページに載っています。

バイナリプロトコルの形式

プロトコルのパケット形式は24バイトの固定長フレーム、そしてその後ろにキーと値のUnstructured Dataが続きます。実際の形式は以下になります(プロトコル仕様書から引用⁠⁠:

 Byte/     0       |       1       |       2       |       3       |   
    /              |               |               |               |   
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0/ HEADER                                                        /   
   /                                                               /   
   /                                                               /   
   /                                                               /   
   +---------------+---------------+---------------+---------------+
 24/ COMMAND-SPECIFIC EXTRAS (as needed)                           /   
  +/  (note length in th extras length header field)               /   
   +---------------+---------------+---------------+---------------+
  m/ Key (as needed)                                               /   
  +/  (note length in key length header field)                     /   
   +---------------+---------------+---------------+---------------+
  n/ Value (as needed)                                             /   
  +/  (note length is total body length header field, minus        /   
  +/   sum of the extras and key length body fields)               /   
   +---------------+---------------+---------------+---------------+
  Total 24 bytes

ご覧の通り、パケット形式はかなり簡素な仕様となっています。この形式で気になる、16バイトも占領しているHEADERですが、HEADERはRequest用とResponse用の2種類が存在します。HEADERにはパケットの有効性を示すマジックバイト、コマンドの種類、キーの長さ、値の長さなどの情報が含まれ、以下の形式になっています:

Request Header
 Byte/     0       |       1       |       2       |       3       |
    /              |               |               |               |
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0| Magic         | Opcode        | Key length                    |
   +---------------+---------------+---------------+---------------+
  4| Extras length | Data type     | Reserved                      |
   +---------------+---------------+---------------+---------------+
  8| Total body length                                             |
   +---------------+---------------+---------------+---------------+
 12| Opaque                                                        |
   +---------------+---------------+---------------+---------------+
 16| CAS                                                           |
   |                                                               |
   +---------------+---------------+---------------+---------------+
Response Header
 Byte/     0       |       1       |       2       |       3       |
    /              |               |               |               |
   |0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|0 1 2 3 4 5 6 7|
   +---------------+---------------+---------------+---------------+
  0| Magic         | Opcode        | Key Length                    |
   +---------------+---------------+---------------+---------------+
  4| Extras length | Data type     | Status                        |
   +---------------+---------------+---------------+---------------+
  8| Total body length                                             |
   +---------------+---------------+---------------+---------------+
 12| Opaque                                                        |
   +---------------+---------------+---------------+---------------+
 16| CAS                                                           |
   |                                                               |
   +---------------+---------------+---------------+---------------+

各々の要素に関して詳しく知りたい場合は、memcachedのバイナリプロトコルの開発ツリーをチェックアウトしてdocsフォルダ内のprotocol_binary.txtという仕様書をご覧ください。

HEADERを見て気になる点

HEADERの形式を見て私が思ったことは、キーの限界値が巨大!ということです。現在のmemcachedの仕様ではキーの長さは250バイトまでという制限がありますが、バイナリプロトコルではキーのサイズが2バイトで表現される仕様になっています。したがって、理論上、最大65536バイト(216)までのキーが扱えることになります。250文字以上のキーを扱うユースケースはそう頻繁にないでしょうが、バイナリプロトコルがリリースされると巨大なキーも扱うことが可能になります。

バイナリプロトコルは次世代の1.3シリーズからサポートされます。

外部エンジン対応

実験的に、memcachedのストレージ層をプラガブルにするという改造を私が去年行ってみました。

この改造をMySQLのBrian Akerに見せたら、コードをmemcachedのメーリングリストに投げられ、試みが本家で気に入られロードマップに載せてもらいました。現在は同じmemcachedの開発メンバーのTrond Norbyeと共同開発(仕様策定から実装・テスト)でプロジェクトを進めています。海外との共同開発は時差が大変ですが、意気投合してプラガブルアーキテクチャのプロトタイプを公開することができました。レポジトリへはmemcachedのダウンロードページからいけます。

外部エンジン対応の必要性

世の中に多数存在するmemcachedのfork(派生)の理由は、パフォーマンスを多少犠牲にしてでも、データを永続的に保存したい・冗長性を実現したいなどの理由が述べられます。現に私もmemcachedの開発に関わる以前は、ミクシィのR&Dでmemcachedを再発明しようとしていた時期がありました。

外部エンジンのロードメカニズムでは、memcachedがネットワークやイベントハンドリングなどの複雑な処理を吸収してくれます。したがって、今まで力技やフルスクラッチでストレージエンジンをmemcachedと連携させていた苦労がなくなり、楽に色々なエンジンを試すことが可能になります。

簡素なAPI設計が成功の鍵

このプロジェクトで我々がもっとも重要視したことはAPIの設計です。ファンクションの数が多すぎるとエンジンデベロッパーに面倒な思いをさせてしまったり、複雑すぎるとエンジンを実装する敷居が高まってしまうという懸念があります。そこで第一バージョンのインターフェイスは13個のファンクションに留めました。詳しい内容は、長くなりすぎるので省略しますが、以下がエンジンに要求されるオペレーションです:

  • エンジン情報(バージョンなど)
  • エンジンの初期化
  • エンジンのシャットダウン
  • エンジンの統計情報
  • 容量的に特定のレコードを保存する事が可能かの評価
  • item(レコード)構造体のメモリ確保
  • item(レコード)のメモリ解放
  • レコードの削除
  • レコードの保存
  • レコードの回収
  • レコードのタイムスタンプを更新
  • 数値演算系の処理
  • データのFLUSH

詳しい仕様に興味のある方はengineプロジェクトのコードをチェックアウトしてengine.hというファイルを覗いてみてください。

現状のアーキテクチャを見直す

memcachedを外部ストレージ対応にさせる点で難しいことは、ネットワークやイベントハンドリングを行うコード(コアサーバ)と、メモリストレージ系のコードが密着していることです。この現象をtightly coupledともいいます。現状のメモリストレージのコードをコアサーバから独立させられないと、外部エンジンのスマートな対応を行うことができません。したがって、我々は設計したAPIを基にmemcachedを以下のようにリファクタしました:

プラガブルなデザイン
プラガブルなデザイン

リファクタ後にバージョン1.2.5やバイナリプロトコル対応のビルド相手にベンチマークを取ってみたところ、パフォーマンスに影響がないことを確認しました。

外部エンジンのロードをサポートする過程で、concurrency controlをmemcachedに任せるソリューションが最も楽でしたが、エンジンにとってパフォーマンスの真髄はconcurrency controlにあるため、マルチスレッド対応は完全にエンジンが責任をもって行う設計にしました。

これらのハックによりmemcachedの可能性が広がればと思います。

まとめ

memcachedのタイムアウトの仕組みや、内部的にどうデータを削除しているかなどに加え、バイナリプロトコルや外部エンジン対応といったmemcachedの最新動向をご紹介しました。これらの最新機能が世に出るのはバージョン1.3からで、まだまだ先の話ですが、是非ご期待ください。

さて、今回が本連載で私の最後の出番です。ここまでお付き合いしてくださって誠にありがとうございます!

次回からは長野がmemcachedの運用ノウハウや互換アプリケーションなどを紹介します。

おすすめ記事

記事・ニュース一覧