はじめに
Memcachedと、一般的なクライアントライブラリを用いた時の問題を
本当は怖いMemcached
としてまとめました。
その時の結論としては「お手軽にMemachedを冗長化したいんだったらKyotoTycoonの相互レプリケーション機能を使うのがいいよ。」でしたが、多少のパフォーマンス劣化を許容できるのであれば、Memcachedでも信頼性を担保出来るやり方がありましたので、その記事になります。
この方式は過去に実際に利用したことのある方式に少し手を入れてデータ削除時の不整合発生を抑制する方式になります。
前回の記事の補足について
ちなみに、前回の記事に関して、以下の様なコメントをいただいております。それぞれ大変有り難いご指摘ではあるのですが、ここではいちいち反論しておきます。
Q. そもそもキャッシュなんだから信用出来ないよね。
A. マスタデータ更新時に自分でキャッシュデータの削除をしていたとしても信用出来ないパターンでの記事になります。
Q. そもそもキャッシュなんだからノード障害時には代替ノードを探さずに、マスタデータを都度問い合わせにいけばいいだけじゃないの?
A. キャッシュノードの障害が一時的である場合は特に問題ないように見えますが、キャッシュノードの障害が長引いた場合にはキャッシュのヒット率の些細な低下はマスタデータにとっては非常に大きな回数の問い合わせになる可能性があります。
また、例えキャッシュノードの障害が一時的であったとしても、キャッシュノード障害からの復帰時に古いデータが削除されておらずその間違ったデータを読み込んでしまうという本質に変わりはありません。
Q. では、障害復帰時には自動的にサーバをクラスタに追加しなければいいのでは?
A. 障害の検知、復旧はMemcachedのクライアントライブラリが動的に行っており、そちらからは一時的な障害か、長期の障害かなどを判定できません。一時的な障害は高負荷時には頻発しますので、その障害が発生したからといって切り離したままで元に戻さないという運用をしてしまいますとシステム全体としての信頼性は最低レベルとなってしまいます。
それでも俺はMemcachedを利用したい!!
利用しましょう。
その場合はMemcachedを完全に別系統で2台立てて、それぞれに書き込んだ上で整合性のチェックをするようにすることで障害発生率を減らします。
利用の流れ
ご利用中の言語で以下の処理をするMemcachedクライアントヘルパクラスを準備してしまいましょう。
コネクションの取得
クライアントからそれぞれのMemcachedノードにコネクションを張る。
→どちらか片方のコネクションを張ることができなかった場合はコネクション取得が成功したMemcachedノードのみを利用する
データ書き込み
それぞれのMemcachedノードに、同一の書き込み日時を付加したデータを書き込む
データ取得
それぞれのMemcachedノードからデータを読み出し、データの書き込み日時が新しい方を利用する。
※同一時刻でデータが異なっていた場合は両方破棄する
ただし、どちらか片方にデータが存在しなかった場合にはデータ読み出し失敗とする。
※データ削除を試みた後である可能性があるため
データ削除
それぞれのMemcachedノードからデータの物理削除を行う
スケールアウトをする場合
接続方法は全く同じで、各接続オブジェクトをMemcachedクラスタに置き換えるだけで、スケールアウト対応が可能になります。
この時、各クラスタの中では前回記事本当は怖いMemcachedで触れたのと同じデータ不整合問題が発生します。しかしながら、その結果を別のノードクラスタから取得した結果と付き合わせることでデータの整合性の担保をします。
メリット・デメリット
メリット
- クライアントライブラリの改修がなく、既存のライブラリを利用する事ができます。
→このメリットとして、複数の言語からのアクセスをするときにそれぞれの言語用のクライアントライブラリ全てに手を入れる必要がなくなります。 - ElastiCacheのMemcachedサービスをそのまま利用することができます。
- 各ノードクラスタにノードを追加することでスケールアウトも可能です。
- アベイラビリティゾーンを超えた構成にすることでより冗長性が確保できます。
デメリット
- 常に倍の回数の読み書きを行う必要がありますので、クライアントアプリケーションから見たパフォーマンスは1/2になります。
- 冗長度が2ですので、Memcached全体のパフォーマンスも1/2になります。
補足:データ信頼性評価についての雑な思考実験
ある大きな負荷をかけた時にノード一つ構成の時のエラー率が1/1000だったと仮定する
また、このエラー率はノードへのアクセス数に比例して増加すると仮定する。
それぞれのクラスタに1台ずつノードがあった場合
各クラスタへのアクセス数は同じなので、クラスタ単位のエラー率は1/1000のまま
両方のノードがエラーを返答した時にエラーデータを取得することになるので、特定のデータのエラー率は(1/100万)まで下がる
それぞれのクラスタにn台ずつノードがあった場合
各クラスタへのアクセス数は同じなので、それぞれのノードには1/nのアクセスが発生する。
この時の単体のノードのエラー率は1/(1000n)となる
ここで、クラスタ全体のエラー率は(1/(1000n))n = 1/1000になるが、クラスタ上の特定のデータがエラーに巻き込まれる確率は1/(1000n)となる
両方のノードがエラーを返答した時にエラーデータを取得することになるので、特定のデータの最終的なエラー率は1/(100万nn)まで下がると考えられる
例、各クラスタが2ノードで構成されていた場合のエラー率は1/400万