Quantcast
Channel: Planet MySQL
Viewing all articles
Browse latest Browse all 1081

更新頻度の多いデータのキャッシュ

$
0
0

@methane です。

ISUCON 7 本戦で最大のスコアアップできたポイントが、 status と呼ばれる重い計算の結果となるJSONのキャッシュでした。

近年のISUCONによくある、「更新が成功したら以降のレスポンスにはその更新が反映される必要がある」(以降は「即時反映」と呼びます)タイプの問題だったのですが、今回のように更新頻度の高くかつ即時反映が求められるデータをキャッシュする方法について、より一般的に解説しておきたいと思います。

即時反映が不要な場合

まずは基本として、即時反映が不要な場合のキャッシュ方法からおさらいします。この場合、一番良く使われるのは参照時に計算した結果を Memcached などにキャッシュし、時間で expire する方法です。

このタイプのキャッシュには、参照元が分散している場合(Webサーバーが複数台あるなど)に Thundering Herd という問題がつきものです。参照頻度が非常に高く、かつ並列して行われる場合に、キャッシュが無効化した瞬間に(キャッシュで回避しているはずの)重い処理が並列で実行されてしまうという問題です。

Thundering Herd を避ける方法としては、memcached や MySQL を使って排他制御をするとか、 expire 時間が到達する前にランダムな時間をずらして投機的に再計算するという方法があります。

また、少しサーバー構成を複雑にしていいのであれば、Webサーバー以外でバックグラウンドに処理を実行して、そこで定期的にキャッシュを更新するという方法もあります。

最初のISUCONはブログがお題で、最新のブログコメントの一覧がサイドバーとしていろんなページに表示されていました。このサイドバーを作るケースなどにはバックグラウンド処理が非常に有効なはずです。

即時反映が必要&更新頻度が低い場合

即時反映が求められる場合は、更新処理の最後(例えばPOSTリクエストに対する処理のうち、MySQLにコミットした後、HTTPレスポンスを返す前)にキャッシュの無効化か更新をしてしまうという手があります。

無効化と更新のどちらがいいかは参照頻度で決まります。参照頻度が更新に比べて十分に高い場合、キャッシュを更新することで Thudering Herd を回避する事ができます。一方で参照頻度が十分高くない場合、一度も参照されないキャッシュを計算するのに時間とメモリを使ってしまう危険もあります。

即時反映が必要&更新頻度が高い場合

さて、本題です。結果をキャッシュしたい計算の重さに対して、参照頻度も更新頻度も非常に高い場合はどうすれば良いでしょうか?

もちろん、計算回数が更新回数よりも少なくなるようにしなければなりません。更新のたびにキャッシュを無効化したり再計算するのはダメです。

即時反映が不要な場合の方法を振り返ってみると、参照時に(Thundering Herdを避けつつ)計算する方法でも、バックグラウンドで非同期にキャッシュを更新する方法でも、計算頻度は更新頻度に影響されませんでした。キャッシュの再計算が1秒おきなら、その1秒の間に何千回の更新処理が走っていても関係ありません。なので、あとは即時反映の要求を満たすようになにか工夫するだけです。

たとえば、更新のたびに単調増加するバージョン番号のようなもの(MySQLのAUTO INCREMENTなIDなど )を用意します。参照時に計算をするなら、まず現在のバージョン (v0) を取得してからロックを取得し、ロックを取得できた時点で得られたキャッシュについているバージョンが v0 より新しければそのまま利用する、古ければ再度バージョン (v1) を取得し直して計算し、結果に v1 を付けてキャッシュするという手がとれます。

バックグラウンドで計算するなら、 redis の pubsub などを使って新しいバージョンのキャッシュが作られるのを待つという手も利用できるでしょう。

今回のISUCONでは、同じ status を共有するクライアントを同じプロセスに誘導できるようになっていたので、 Redis や MySQL を使わずにもっと楽に実装することができたはずです。

「まとめて処理」を「待つ」パターン

複数の更新処理に対して1度にまとめて重い処理を実行するという考えかたは、キャッシュに限らず広く有効なものです。例えば「日次バッチ」などと呼ばれる処理は大抵そうでしょう。最近のMySQLは複数のトランザクションのコミットを一度のディスクへの sync でまとめて実行すること(グループコミット) ができます。Fluentdが高いスループットを出せるのも、ある程度の量のイベントをバッファに貯めて一括で転送する設計が寄与していると思います。

そしてこの「まとめて処理」パターンには待ち時間がつきものです。日次バッチならそのバッチが終わるまで結果は見えませんし、トランザクションのコミットはグループコミットを待たされます。 「即時反映が必要でかつ更新頻度が高い」問題でのキャッシュが難しいのは、他のケースのキャッシュではこの「待ち時間」が必要ない、あるいは意識することがないからだと思います。

@tagomoris さんいわく "ISUCON参加前と参加後に最も多くのものを持ち帰った人こそが勝者と言えるでしょう。" (引用元) ということですが、参加チームの方にこの「まとめて処理」を「待つ」パターンを持ち帰って将来何かに役立ててもらえたら幸いです。


Viewing all articles
Browse latest Browse all 1081

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>