dog pile effect という現象についてRailsで解消するにはrace_condition_ttlを利用する

Ruby
Photo credit: WoofBC

RailsのキャッシュストアであるActiveSupport::Cache::Storeには:race_condition_ttlというオプションが存在する。
筆者はmemcachedを対象としたキャッシュまわりのライブラリを書いている。そのため、このオプションがmemcachedのキャッシュストアであるActiveSupport::Cache::MemCacheStoreにも登場する。これはなんのためのオプションなのだろうか?と気になっていた。
このオプションについて、RailsのドキュメントであるRailsGuidesを参照すると「dog pile effect」として知られていることを回避するためのオプションとなっている。

:race_condition_ttl – This option is used in conjunction with the :expires_in option. It will prevent race conditions when cache entries expire by preventing multiple processes from simultaneously regenerating the same entry (also known as the dog pile effect). This option sets the number of seconds that an expired entry can be reused while a new value is being regenerated. It’s a good practice to set this value if you use the :expires_in option.

そこで「dog pile effect」とはどういったことなのか?日本語での解説資料が見つからなかったので、記事にすることにした。

スポンサーリンク

dog pile とは?

まずはこの画像を見てください。

Photo credit: WoofBC

Photo credit: WoofBC


これが dog pile という状態です。語源は犬が積み重なって寝ることを指しているようです。人間が積み重なっていることも指しているようで、野球の乱闘状態などもdog pileというようです。

dog pile effect とは?

「dog pile」というのはここまででなんとなくわかりましたが、WEBアプリケーションの世界での「dog pile effect」とはどういったことなのでしょうか?これについて解説してみたいと思います。
「dog pile effect」はmemcachedやredisといったexpireを設定するキャッシュストアを利用しているWEBアプリケーションで発生します。

dog pile effect が発生する構成

dog pile effect が発生する状況について、以下の様なシステムがあると想像してください。
WEBアプリケーションが1つのページをキャッシュを利用して表示されているとします。このページに表示するデータをMySQLなどから取得して、memcachedにキャッシュしている場合、アプリケーションはmemcachedから表示に必要なデータを取得するため、MySQLへSQLクエリーを発生することはありません。

memcachedにキャッシュがある場合

memcachedにキャッシュがある場合


キャッシュのexpireを5分などと設定していれば、5分間キャッシュを利用することができます。一般的にMySQLなどのRDBMSよりもキャッシュデータを入れているmemcachedなどの方が高速にデータを取得でき、MySQLなどへアクセスする必要もないことから大量のアクセスを処理することができるようになります。

dog pile effect が発生する状況

dog pile effect はこのキャッシュが消える5分後に発生する現象です。
このキャッシュがない状態で、アクセスの多いサイトだと同時に100のリクエストが来た場合、100のリクエストは同時にキャッシュがないことを確認してしまうため、MySQLに100リクエスト分のSQLクエリーが発生してしまうことになります。これによってWEBアプリケーション全体のパフォーマンスが低下してしまうことを「dog pile effect」と呼びます。

キャッシュが消えて同時に100リクエスト来た場合

キャッシュが消えて同時に100リクエスト来た場合


通常はサーバに届いた最初のリクエストはキャッシュが存在しないことを確認して、MySQLからデータを取得します。そして、新たにキャッシュを作成してページが表示されます。

dog pile effect を回避する方法

回避する方法としては冒頭で述べている通りRailsを使っていれば:race_condition_ttlのオプションを利用することです。

race_condition_ttl を利用する

これを利用することでどういった挙動をするのかActiveSupport::Cache::Store#fetchにあるサンプルコードを参照してみましょう。

# Set all values to expire after one minute.
cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
cache.write('foo', 'original value')
val_1 = nil
val_2 = nil
sleep 60
Thread.new do
  val_1 = cache.fetch('foo', race_condition_ttl: 10) do
    sleep 1
    'new value 1'
  end
end
Thread.new do
  val_2 = cache.fetch('foo', race_condition_ttl: 10) do
    'new value 2'
  end
end
# val_1 => "new value 1"
# val_2 => "original value"
# sleep 10 # First thread extend the life of cache by another 10 seconds
# cache.fetch('foo') => "new value 1"

最初のcache変数への代入でこのキャッシュストアは1分でキャッシュが消えるexpires_inオプションを設定しています。
cache.writefooというキーに'original value'という値を設定しています。
その後、60秒sleepしているためこのキャッシュは消える直前まで時間が進みます。
最初のval_1を取得するスレッドはキャッシュから'original value'という値を取得した瞬間に内部のexpireをrace_condition_ttlで指定された10秒延長させてキャッシュに再度'original valueを書き込みます。そしてsleepの1秒を処理して、'new value 1をキャッシュへ書き込みます。
前述のsleepをしている間にval_2のスレッドが処理されます。この2つ目のスレッドはキャッシュに残っている一番最初にwriteされた値か1つ前のスレッドで再度expireを更新して書き込まれたキャッシュを参照します。よってキャッシュが存在することになるので結果としてval_2"original value"が返り値となります。
val_1に関してはキャッシュが切れた61秒後に更新をするので、新たな値となるキャッシュに書き込んだ"new value 1"が返り値になります。この値はrace_condition_ttlオプションにより10秒expireが伸ばされただけなので、最初に#writeされた時間から70秒後には消えてしまいます。

まとめ

とりあえずrace_condition_ttlをあらゆるところにつけておけば良い。というものではありません。特にアクセスが多く、appサーバ全台から参照され、DBにクエリーが発生してSQLクエリーが刺さってしまう状態よりは古いキャッシュを返した方が良いケースに利用してみてください。数は少ないですが筆者は思い当たるケースがありました。

タイトルとURLをコピーしました