Table of Contents [expand]
ほとんどの本番 Web サイトで頼りになるスケーリングソリューションであるにもかかわらず、Memcache が、その潜在能力を十分に発揮できる使われ方をしていない場面はよくあります。ほとんどの開発者は get
、set
、delete
操作のことしか理解していません。しかし Memcache には、より広範な一連の操作が用意されており、それらは開発者が、より高度なアプリを、より少ないコードで、パフォーマンスも向上させた上で構築するために役立ちます。
この記事では、一連の現実的なユースケースを通じて、さらに役立つ高度な Memcache 操作の概要を示し、アプリの実装とパフォーマンスにそれらの操作が及ぼす影響を示します。
前提条件
この記事は、次のものが用意されていることを前提としています。
- Heroku アカウント。無料ですぐにサインアップできます
- Heroku CLI がインストールされていること。
- Memcache の基本的な知識があること。
テスト環境
次の Rails アプリを Heroku にデプロイすることから始めます。
ソースコードまたは
これにより、MemCachier アドオン とシンプルなコマンドラインを備えたサンドボックス環境が用意され、高度な Memcache 操作を自分自身で実行できるようになります。 説明を読むだけでなく実際にコマンドを入力してみると、コマンドの構文や機能の理解が深まります。
テストアプリのコピーが、ご使用の Heroku アカウントにデプロイされました。
新しいアプリの URL をメモしてください。これは http://serene-mesa-2821.herokuapp.com/
のような形式で、serene-mesa-2821
は Heroku 上のアプリケーションの名前です。この名前は、対話型シェルを確立するために必要です。
対話型シェル
ターミナルから heroku run console
を実行し、アプリ名を指定して、Heroku で実行されているアプリのインスタンスに接続します。Memcache クライアントライブラリをロードし、基本的な set
および get
コマンドを実行して設定を確認します。
$ heroku run console -a app-name
irb> cache = Dalli::Client.new
irb> cache.set("foo", "bar")
=> true
irb> cache.get("foo")
=> "bar"
この例では、対話型 Ruby シェルと Dalli クライアントを使用して Memcache をロードし、Memcache と対話しています。これは単に例示目的であり、 どの言語の Memcache ドライバーも同様のコマンドが サポートされます。
このガイドではこれ以降、このシェルが実行され、クライアントライブラリがロードされていることを前提に説明を進めます。
キャッシュの期限切れ
Memcache を使用するときの最大の課題は、キャッシュが古くなるのを回避しつつ、記述するコードのクリーンさを保つことです。ほとんどの開発者は、データを Memcache に保存し、変更があったときにデータを削除または更新します。この戦略は、アプリケーション全体が Memcache 関連のコードだらけになって早々に破綻する可能性があります。Rails では Sweepers によって この問題を解決できますが、他の言語やフレームワークには同様の代替策がありません。
コードの複雑さを回避するシンプルな戦略の 1 つは、有効期限を付けて Memcache にデータを書き込むことです。有効期限のあるデータは、期限が来ると自動的に期限切れになります。ほとんどのアプリケーションで、静的アセット、ヘッダー、フッター、ブログ投稿などの頻繁に変更されないコンテンツには、時間ベースのキャッシュ有効期限が効果的です。
サンドボックスシェルで、次のコマンドを実行して、10 秒後に期限切れになる値を設定します。
有効期限を “0” に設定すると、値は期限切れになりません。
irb> cache.set("expires", "bar", ttl=10.seconds)
irb> cache.get("expires")
=> "bar"
.. wait 10 seconds ..
irb> cache.get("expires")
=> nil
特定のコンテンツを明示的に期限切れにするためにアクションが不要であることがわかります。ttl
値で指定された時間の経過後は、キーを get
しても nil の結果が返されるだけです。
秒数で指定された有効期限が 30 日以上に相当する場合、 Memcache では、指定された秒数を Unix エポック日付に変換することによって、 有効期限を絶対的な日付として扱います。40 日を秒数で指定すると、 有効期限が 1970 年の日時に設定されて予測不能な結果を 発生させるため注意してください。
キャッシュのクリア
新しいキャッシュコードを記述する過程で、開発者がキャッシング戦略を何回となく変更することはよくあります。キャッシュ戦略の拙速な変更により、ダーティキャッシュが生成されてデバッグが困難になります。開発中にキャッシュ戦略を変更するたびに、flush
コマンドを Memcache に発行して、すべての値のキャッシュをクリアするようにしてください。
サンドボックスコンソールで、次のコマンドを実行してフラッシュをテストします。
irb> cache.set("foo", "bar")
irb> cache.get("foo")
=> "bar"
irb> cache.flush
irb> cache.get("foo")
=> nil
flush
は本番環境へのデプロイ時にも使用できます。ただし、アプリケーションがキャッシュフラッシュに対応できない場合があるので注意してください。キャッシュを多用する高トラフィックのアプリケーションでは、本番環境で flush
を
発行しないか、キャッシングなしでも処理できる程度にトラフィックが少ないときにのみ発行することをお勧めします。
MemCachier Heroku アドオンには、開発者の代わりにフラッシュコマンドを発行できる
Web ダッシュボードがあります。MemCachier ダッシュボードにアクセスするには、
Heroku ダッシュボードからアドオンをクリックするか、CLI から
heroku addons:open memcachier
を実行します。
軽量カウンター
Memcache に保存される軽量のカウンターは、アプリで特定のイベントが発生する頻度を、アプリのパフォーマンスを低下させずに追跡するために役立ちます。カウンターはデバッグ、プロファイリング、使用状況追跡に使用できます。
たとえば、サードパーティの API に依存するアプリでは、サードパーティの API が利用不能になったり、正しくないデータを返したりする頻度を把握することが必要な場合があります。Memcache カウンターは、ページのロード時間にほとんど影響せず、データベースの負荷を増やすこともないため、理想的なソリューションです。
サンドボックスコンソールで、次のコマンドを実行して incr
(インクリメント) と decr
(デクリメント) をテストします。
irb> cache.incr("my_counter", 1, nil, 0)
irb> cache.get("my_counter")
=> "0"
irb> cache.incr("my_counter")
irb> cache.get("my_counter")
=> "1"
irb> cache.incr("my_counter", amt=5)
irb> cache.get("my_counter")
=> "6"
irb> cache.decr("my_counter")
irb> cache.get("my_counter")
=> "5"
incr
および decr
は、get
と
set
による手動での変更よりも優先して使用してください。incr
および decr
はスレッドセーフであり、必要な TCP ラウンドトリップも少なくなるからです。incr
の引数の説明は、Dalli のドキュメントを参照してください。
リスト管理
Memcache に保存される単純なリストは、正規化されていない関係の管理に役立つことがあります。たとえば、e コマース Web サイトで、最近の購入の小さなテーブルを保存したい場合があります。シリアル化されたリストを Memcache に保持し、新しい購入が行われたときにリストを再計算する代わりに、append
と prepend
を使用して非正規化データを保存し、データベースクエリを回避することができます。
従来の set
操作を使用して顧客の最近の購入のリストを更新する代わりに、次のようにします。
cache.set("user_1_recent_purchases", Purchases.recent)
prepend
は新しいデータにしか使用できませんが、効果は同じです。
cache.prepend("user_1_recent_purchases", product.name + "||")
このアプローチにより、Memcache のフットプリントが削減され、ユーザーの最近の購入をすべて取得するデータベースクエリが回避されます。
サンドボックスコンソールで、次のコマンドを実行して append
と prepend
をテストします。
ttl=0
はキーの有効期限がないことを示します。:raw => true
は、
値が raw バイトとして保存されることを指定します。これは、append
と
prepend
を使用するために必要です。
irb> cache.set("my_list", "foo", ttl=0, options={:raw => true})
irb> cache.get("my_list")
=> "foo"
irb> cache.prepend("my_list", "bar||")
irb> cache.get("my_list")
=> "bar||foo"
irb> cache.append("my_list", "||baz")
irb> cache.get("my_list")
=> "bar||foo||baz"
この例では、任意の区切り文字 (||
) を使用しています。区切り文字は、
リスト項目に区切り文字列が含まれることがないよう、リスト内の
各項目の期待される値に基づいて選択する必要があります。
さらに、アプリケーションによっては、固定長文字列の実装のほうが適している
場合があります。
append
と prepend
はスレッドセーフなため、get
と set
による手動での変更よりも優先して append
と prepend
を使用してください。
Memcache でサポートされる値の最大サイズは 1 MB です。値のサイズの
許容上限を超える可能性があるリストを作成しないよう注意して
ください。Dalli などの一部のクライアントでは圧縮がサポートされています。Dalli では、
Dalli に接続するときに :compress
オプションを true
に設定します。
スレッドセーフセット
Memcache に保存される単純な JSON ハッシュは、頻繁にアクセスされる設定の管理に役立ちます。たとえば Web サイトで、どの機能が現在有効になっているかや、どの AB テストが実行中であるかを追跡することが必要な場合があります。多くの場合、都合の良いことに、これらの設定は JSON ハッシュにまとめて保存されます。
ハッシュは順序付けがされないため、append
と prepend
は JSON ハッシュには使用しません。また、JSON ハッシュに対する 2 つの変更を同時に行うと片方の変更が失われる場合があるため、set
は危険です。
cas
演算子を使用した比較とスワップでは、元の値を新しい値と比較し、古い値が別のライターによって変更されていない場合にのみ値を入れ替えます。言い換えれば、cas
はスレッドセーフな set
です。
サンドボックスコンソールで、次のコマンドを実行して cas
をテストします。
この例の cache.cas
では、Ruby Memcache クライアントとして
選択した Dalli の要求どおり、ブロックを期待します。他のクライアントでもこのアプローチが
通用するとは限りません。
irb> cache.set("my_json", "{}")
irb> cache.get("my_json")
=> "{}"
irb> cache.cas("my_json") { {key: "val"}.to_json }
irb> cache.get("my_json")
=> "{\"key\":\"val\"}"
Memcache プロトコルは、cas
操作を直接実装するわけではありません。
代わりに、バージョン番号付きの set
をサポートします。この場合の set
は、
Memcache に保存されているキーのバージョンとバージョン番号が一致する
場合にのみ実行されます。プロトコルで直接 cas
を実装する代わりに
バージョンを使用することで、古くからの ABA 問題に対処します。
クライアントによっては、バージョンを set
に渡して cas
を手動で実装することが必要な場合があります。
リファレンス
次に示すのは、この記事で扱った Memcache の各操作のクイックリファレンスです。API はクライアントおよび言語ごとに異なるため、API の詳細は示していません。
操作 | 説明 |
---|---|
有効期限付きで設定 | 秒単位の有効期限付きでキーと値を設定します。有効期限が切れたキーはキャッシュから削除されます。30 日までの有効期限は現在時刻からの時間として解釈される一方、30 日以上の有効期限は Unix の絶対日付として解釈されます。 |
フラッシュ | すべてのデータをキャッシュから削除します。本番環境でのこのコマンドの使用には注意が必要です。空のキャッシュがデータベースに負荷をかけすぎると、本番環境アプリでダウンタイムが発生する場合があります。 |
インクリメント | 指定された数だけ整数値をインクリメントします。スレッドセーフ。 |
デクリメント | 指定された数だけ整数値をデクリメントします。スレッドセーフ。 |
アペンド | 値の末尾に追加します。一部のクライアントでは、元の値が raw バイトとして保存されている必要があります。スレッドセーフ。 |
プリペンド | 値の先頭に追加します。一部のクライアントでは、元の値が raw バイトとして保存されている必要があります。スレッドセーフ。 |
CAS (またはバージョン付きの設定) | 値が別のプロセスによって変更されていないことを条件に、新しい値を設定します。スレッドセーフ。 |