Ruby
Rails
Memcached
Cache

キャッシュをいい感じに分散して期限切れを軽減するgemを作った

More than 1 year has passed since last update.

導入

初めてgem (millas)を作って公開しました。
そのgemを紹介したいと思います。
gemはこちら

millas はその存在を意識することなく、cacheを分散し、有効期限にグラデーションをつけることでキャッシュ切れを軽減することができます。

由来

gemの名前は millas です。
Millasはフランスのランド地方の伝統的なお菓子のことで、日本では「魔法のケーキ」として人気に火がつきました。
生地は1つなのに、1度焼くだけで3層になるという、フォトジェニックなケーキです。

動機

私は現在delyに所属するソフトウェアエンジニアで、主にサーバーサイドを担当しています。
delyでは「70億人に1日3回の幸せを届ける」をミッションにkurashiru (クラシル)というレシピ動画サービスを運営しています。クラシルはローンチから1年とまだ若いサービスですが、TVCMを打ったり、急激に伸びているサービスです。

クラシルは一部CGM機能もあるのですが、主にレシピ動画メディアであるため、同じページ、つまり同じリクエストに多くのユーザーからアクセスがあります。
その度にDBにアクセスが走ると、DBに負荷がかかりすぎるため、頻度が高いレシポンスはキャッシュに載せています。

このキャッシュには次の問題がありました。

  • レスポンスをキャッシュしたいけど、そんなに長くキャッシュできない
  • TVCMなど急激なアクセスがある時に、主要なキャッシュの有効期限が切れるとまずい
  • リクエストの頻度に偏りがあるため、キャッシュをある程度分散させたい

それらを詳しく要素分解したいと思います。

キャッシュを頻度高く更新したい

クラシルでは月に1,000本以上のレシピ動画が公開されています。
そのため、新着や検索結果を頻度高く更新したいという要望がありました。
しかし、DBへの負荷を考えれば有効期限が長ければ長いほど良いわけで、トレードオフの関係にありました。
その場合、バッチなどで意図的にキャッシュを更新したいと当初考えていましたが、キーの一覧を取得することが困難であったり、アクションを追加する毎に想定されるキーを登録することは業務上困難なため、その方法を取ることはできません。

急激なアクセス時にキャッシュ切れちゃう問題

上記の理由からクラシルのキャッシュはそんなに長くキャッシュしていません。
そうすると、TVCMなど急激なアクセスが長時間ある時に、例えば新着のキャッシュが切れてしまったら、再度キャッシュに載せる数100msの時間に行われた同アクセスは全て、キャッシュをすり抜けて、DBへ処理が流れてしまいます。
小さなことかも知れませんが、キャッシュ切れが重なると死活問題に繋がります。
また、僕は怯えながら土日を過ごしたくありません。

キャッシュ偏っちゃう問題

複数ノードでクラスターを形成しているMemcachedなどのキャッシュサーバーに、dalli などでキャッシュを載せる場合、通常、アプリケーション側はどのノードにキャッシュを載せるかを意識することはありません。
dalli のコードを読むとキャッシュキーに一意にノードを決定していることがわかります。
そのため、リクエストが多いキーが載っているノードのI/Oやコネクション数に偏りが生じてしまうという問題があり、これはスケールアウトすることができません。対処としてはサーバータイプを上げるなど方法はありますが、いつか限界がきます。
そのため、取れる方法としてはキャッシュをできるだけ分散させて配置することですが、それでも上記のキャッシュ切れの問題を解決することはできません。

どのノードにキャッシュが載っているのかをアプリケーション側から知る方法は以下の通りです。

ActiveSupport::Cache::DalliStore.new.instance_variable_get("@data").send(:ring).server_for_key(ここにキー).hostname

良い感じキャッシュにする方法

解決したい問題は以下です。

良い感じにキャッシュを分散させたいけど、キャッシュ切れも防ぎたいし、できるだけ楽をしたい

Millasは上記の問題を丸っと解決してくれると考えています。
その概念を図にすると以下の通りです。

分散

有効期限のグラデーション

要は語尾にランダムな数字をつけることで、キャッシュを分散することができ、有効期限にグラデーションをつけることでキャッシュ切れのリスクを最小限にし、切れたアクセスが他のキャッシュを上書きすることで、該当する全てのキャッシュを更新することができます。

※ 注意点
分散させるということは、クラスター全体のキャッシュ量が単純にn倍になるということです。
そのため、クラスターを形成するノード単位でメモリやCPUのチューニングをする必要があります。
また、分散数が多すぎるとwrite処理の時に書き込みが重くなり、アプリケーションサーバーの負荷に繋がります。

具体的な使い方などはGitHubをご覧いただければと思います。

gemとして公開した理由

このキャッシュ機構をgemとして公開することにしたのは、僕自身gemを作って公開したことがないことが、喉に刺さった魚の骨としてずっといたためというのが大きいです。
幸いにも、Millasはとてもシンプルな仕組みであるため、gemに落としやすく、公開までにそんなに時間もかからないと思ったからです。
この仕組み自体、世界のどこかでやってたり、あるいは日本でもすでにgemがあるかも知れません。
探したのですが、同じことをしているgemや記事が見当たらなかったため、公開にいたりました。
すでにある場合その作者の方はご一報いただければと思います。
僕自身、初めてgemを公開したので、お作法などは他のgemを探り探り参考にさせていただきました。不足している点や間違いなどはどこでも良いので指摘していただけると幸いです。