キャッシュシステムの 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を入れ込むのもありですね。

某ブログサービスの某ブログが重いのでその対策になるかなぁと。

このブログ記事について

このページは、Masahiro Naganoが2010年12月16日 18:50に書いたブログ記事です。

ひとつ前のブログ記事は「LIKE節におけるエスケープ文字とDBIx::PrintfもしくはDBIx::Printf::Named」です。

次のブログ記事は「キャッシュシステムの Thundering Herd 問題への対策案。その2 排他制御」です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 4.27-ja