キャッシュシステムの Thundering Herd 問題とは、
通常、キャッシュに格納されるデータは、それぞれ単一の生存時間をもっています。問題は、頻繁にアクセスされるキャッシュデータがエクスパイアした際に発生します。データがエクスパイヤした瞬間から、並行に走る複数のアプリケーションロジックがミスヒットを検知し、いずれかのプロセスがキャッシュデータを格納するまでの間、同一のリクエストが多数、バックエンドに飛んでしまうのです。
という問題。クエリが重かったりするとそれだけでシステムに致命的な負荷を与えてしまい、キャッシュがあるにも関わらずキャッシュが切れたタイミング全体が停止することも考えられます。memcachedでこの問題に対応するため、次のような手段を考えてみました。
まず、保存時に通常のキャッシュと、それよりも指定した秒数Expiresが短いキャッシュを2つmemcachedに対してsetします。getする時はランダムに指定した確率でexpiresが短いキャッシュを見に行きます。これで一定の確率でキャッシュが早く切れるので一斉にバックグラウンドへリクエストが飛ぶことをさけることができます。Expiresの秒数や確率はシステムによって適正値がかなり異なると思われます。キャッシュを削除するときは両方削除するのをお忘れなく。
2カ所に書くので、もちろん、キャッシュの容量は2倍必要になります。
Perlで書くとこんな感じ
package EarlyExpiresCache;
use Class::Accessor::Lite (
new => 1,
rw => [ qw(cache ratio early_expires) ],
);
sub set_ee {
my ($self, $key, $value, $expires) = @_;
$self->cache->set($key, $value, $expires);
$self->cache->set($key . "::ee", $value, $expires - $self->early_expires);
}
sub get_ee {
my ($self, $key) = @_;
if ( int(rand($self->ratio)) == 0 ) {
return $self->cache->get($key."::ee");
}
$self->cache->get($key) // $self->cache->get($key."::ee")
}
sub delete_ee {
my ($self, $key) = @_;
$self->cache->delete($key);
$self->cache->delete($key."::ee");
}
package main;
my $eecache = EarlyExpiresCache->new(
early_expires => 60, #sec
ratio => 10, # 1/10
cache => Cache::Memcached::Fast->new(...)
);
my $val = $eecache->get_ee('foo');
if ( !$val ) {
.. heavy sql ..
$eecache->set_ee('foo', $value, 3600);
}
*追記
せっかく2つ書いているので、get時に、通常のkeyで取れなかったら早くexpiresするkeyで取る様にした。これでデータが冗長化されるので、memcachedが落ちても(落としても)多少大丈夫
*/追記
memcachedの空間効率を気にする場合は、cacheの値にexpiresを入れ込むのもありですね。
某ブログサービスの某ブログが重いのでその対策になるかなぁと。