cache-asideを実装しヒット率を測る
目標
cache-asideを自分で実装して、ミスとヒットのコストの差を数字で確認し、ネガティブキャッシュとバージョンキーによる無効化まで付けます。
なぜ重要なのか
cache-asideは最も一般的なパターンなので、コードはどこからでもコピーできます。本当に難しいのは細部です。書き込み時にキャッシュを更新するか削除するか、存在しないデータをキャッシュするか、そして全体を一度に無効化する必要があるとき何をするかです。1つ目。更新ではなく削除が正解である理由は、2つの書き込みがすれ違うとき、更新は古い値を残すことがありますが、削除はそうではないからです。2つ目。存在しないキーを繰り返し取得するトラフィックは、キャッシュを素通りして原本をそのまま叩きます。これをキャッシュ貫通といい、短いTTLのネガティブキャッシュで防ぎます。3つ目。商品カテゴリが丸ごと変わったとき、関連するキーをすべて探して消すのは、依存関係追跡の地獄です。バージョンキーは、それをバージョン番号を1つ上げる作業に変えます。
ステップ
/opt/app/slowdb.pyを127.0.0.1:8151で起動します。GET /item/1が200を返し、GET /statsが{"queries":<n>}を返します。/root/ca/cache.pyにget_item(id)を作ります。キャッシュミスなら原本を取得して、item:<id>キーに300秒で保存します。/root/ca/miss.outにsource=origin ms=<정수>を書きます(プレースホルダーは整数です)。msは100以上である必要があります。- 同じ項目をもう一度取得します。
/root/ca/hit.outにsource=cache ms=<정수> origin_queries_delta=0を書きます(プレースホルダーは整数です)。msは50未満である必要があります。 /root/ca/update.pyは、原本を更新したあとにキャッシュキーを削除します。ソースに、更新値をキャッシュに書き直すコードがあってはいけません。実行後、EXISTS item:1が0である必要があります。- 存在しない
item:9999を取得すると原本が404を返すので、その結果を60秒の寿命でキャッシュします。2回目の取得で原本の取得数が増えてはならず、TTL item:9999が1以上60以下である必要があります。 /root/ca/bench.pyで100回の取得(ユニークなキー20個)を行い、/root/ca/hitratio.txtにhits=<n> misses=<n> ratio=<소수>を書きます(プレースホルダーは小数です)。ratioは0.75以上である必要があります。- キャッシュキーを
v<버전>:item:<id>の形に変えて(プレースホルダーはバージョンです)、バージョンをver:itemキーで管理します。バージョンを上げたあとに取得すると、原本の取得数が増える必要があります。/root/ca/version.outにold_ver=<n> new_ver=<n> refetched=trueを書きます。
参考
- 原本の取得数の確認:
curl -s http://127.0.0.1:8151/stats - ネガティブキャッシュのTTLは、正常なキャッシュよりはるかに短くします。データがすぐできるかもしれないからです。
- よくある間違い1は、書き込み時にキャッシュを更新することです。同時書き込みで、古い値が残ることがあります。
- よくある間違い2は、ネガティブキャッシュをせず、存在しないIDを投げるリクエストが原本をそのまま叩くことです(キャッシュ貫通)。
遅い原本を起動する
/opt/app/slowdb.pyを127.0.0.1:8151で起動してください。GET /item/1が200を返し、GET /statsが{"queries":<n>}を返します。
/opt/app/slowdb.pyは、取得のたびにわざと時間をかけます。自分が受けた取得の数も教えてくれます。
キャッシュミスの経路を作る
/root/ca/cache.pyにget_item(id)を作ってください。キャッシュミスなら原本を取得して、item:<id>キーに300秒で保存します。/root/ca/miss.outにsource=origin ms=<정수>を書き(プレースホルダーは整数です)、msは100以上である必要があります。
キャッシュになければ原本に行って、キャッシュに入れて返します。最初の取得は遅くなるしかありません。
2回目の取得をキャッシュで処理する
同じ項目をもう一度取得してください。/root/ca/hit.outにsource=cache ms=<정수> origin_queries_delta=0を書き(プレースホルダーは整数です)、msは50未満である必要があります。
同じキーをもう一度取得したときは、原本に行ってはいけません。原本の取得数が増えていないか確認してください。
更新時にキャッシュを削除する
/root/ca/update.pyは、原本を更新したあとにキャッシュキーを削除してください。ソースに、更新値をキャッシュに書き直すコードがあってはいけません。実行後、EXISTS item:1が0である必要があります。
更新ではなく削除です。その理由は読み物にあります。次の取得が自然に最新を埋めます。
存在しないキーをネガティブキャッシュする
存在しないitem:9999を取得すると原本が404を返すので、その結果を60秒の寿命でキャッシュしてください。2回目の取得で原本の取得数が増えてはならず、TTL item:9999が1以上60以下である必要があります。
存在しないキーを繰り返し取得すると、毎回原本に行きます。短い寿命で「ない」もキャッシュしてください。
ヒット率を測る
/root/ca/bench.pyで100回の取得(ユニークなキー20個)を行い、/root/ca/hitratio.txtにhits=<n> misses=<n> ratio=<소수>を書いてください(プレースホルダーは小数です)。ratioは0.75以上である必要があります。
ヒットとミスをそれぞれ数えて、比率を出します。リクエストの分布によって値が変わります。
バージョンキーで全体を無効化する
キャッシュキーをv<버전>:item:<id>の形に変えて(プレースホルダーはバージョンです)、バージョンをver:itemキーで管理してください。バージョンを上げたあとに取得すると、原本の取得数が増える必要があります。/root/ca/version.outにold_ver=<n> new_ver=<n> refetched=trueを書きます。
キー名にバージョンを埋め込めば、バージョンを上げるだけで、古いキーを誰も探さなくなります。削除を自分でしなくてもよいのです。