メインコンテンツまでスキップ

一般的なキャッシュパターン

キャッシュは高速です。

インメモリでキーバリューのアクセスに最適化されているので、クライアントでの計測でも 1ミリ秒以下の p99 レスポンスタイムを得られます。とても高速です。そして、これだけ高速なので、、、

キャッシュは楽しいです。

遅いウェブサイトは誰も好みません。遅いウェブサイトはユーザーを飽きさせ、売上を損ねます。開発者は遅いウェブサイトや楽しんでいないユーザのために働きたくはありません。キャッシュは複雑なリクエストのレイテンシを下げたり、データベースの負荷を下げることで、遅いウェブサイトを助けてくれます。しかし、キャッシュは注意して使う必要があります、なぜなら、、、

キャッシュはフットガンになり得ます。

キャッシュは効率的な使い方がある一方で、非効率的な使い方もあります。ただ、それより質が悪いのが、痛みを伴うキャッシュ戦略 -- ユーザを混乱させる古いデータや一貫性の無いデータ、もしくはアプリケーションの可用性を損なうような使い方です。

世の中にはたくさんの異なるキャッシュの実装方法があり、キャッシュの戦略はいくつもの要素に依存して決まります。

この投稿では、皆さんのアプリケーションで上手く動くキャッシュ戦略の設計方法について学んでいきます。まず、キャッシュ戦略を設計する際に決めなければならないいくつかの選択肢について見ていきます。それから、いくつかの一般的なキャッシュ戦略と、どういう時にそれらを使うべき・使わないべきかを見ていきましょう。

キャッシュの選択肢

特定のキャッシュパターンについてお話する前に、キャッシュを追加する際に決めなければならないいくつかの選択肢について見ていきましょう。

3つの中心となる選択肢はこちらです:

  • どこにキャッシュするか -- ローカル vs. リモート

  • いつキャッシュするか -- 読み出し vs 書き込み

  • どの様にキャッシュするか -- インライン vs アサイド

この順番で一つずつ見ていきましょう。

どこにキャッシュするか -- ローカル vs. リモート

まず最初に決めなければならない選択肢は、データをどこにキャッシュするかです。

キャッシュを考える際に、高速で耐久性の低いデータベースの様な、中央集権のリモートキャッシュを皆さんまず最初に想像しがちです。しかし、キャッシュは独立したインフラである必然性はありません。バックエンドサーバーや、ユーザーのブラウザ上にローカルにキャッシュを追加することも可能です。ここで"ローカル"キャッシュと言った場合には、何かしらのコンピュートに対してローカルで、他のコンピュートインスタンスからはアクセスできないものを指します。

一般的には、ローカル vs. リモートの問いは、利便性 vs. 簡潔さに行きつきます。ローカルキャッシュは新しいインフラを追加するよりも簡単に導入できます。加えて、新しいインフラを追加するということは、可用性やアプリケーションのアップタイムに関する新しい課題をもたらすということですが、ローカルキャッシュでは一般的にそれは回避できます。

一方で、ローカルキャッシュは中央集権のキャッシュと比べてそれほど有効的ではありません。バックエンドサーバーでキャッシュする場合だと、過去にキャッシュされたデータがあるマシンでリクエストが処理される確率は、フリートのインスタンス数が増えるほど減っていきます。これは現代的なクラウドベースアプリケーションの揮発性においては、より顕著です。サーバーレス関数、コンテナ、またはインスタンスは、アプリケーションが需要に応じて動的にスケールアップやスケールダウンするに従って、より短命になっていきます。新しいインスタンスはローカルキャッシュを持っていないので、最初のリクエストの処理には全く利点がありません。

最後に、ローカルキャッシュは古いデータの管理がより難しくなります。データが更新されたり削除された時、中央集権のリモートキャッシュであれば、関連するキャッシュデータを更新するのは簡単です。分散されたローカルアプリケーションやクライアントブラウザに対して更新をかけるのはもっと大変です。そのため、ローカルキャッシュは限られたタイプのキャッシュデータや、短い生存期間 (TTL) の設定でしか上手く動作しないでしょう。

リモートで、中央集権のキャッシュではこうした短所はありません。処理を行うどのサーバーからも利用可能で、アプリケーション内で幅広く有効活用できます。さらに、リモートキャッシュでは一般的にデータを必要な時に期限切れにする方法があって、書き込み処理の中でデータを更新した直後にパージすることができます。リモートキャッシュの欠点は、独立したインフラをどうやって管理するかという運用的な課題です。

いつキャッシュするか -- 読み出し vs 書き込み

2つ目に考えなければならないキャッシュの考慮点は、データをいつキャッシュするかです。ここでも、2つの選択肢があります -- 初めてデータが読まれた時にキャッシュする ("遅延読込"とも呼ばれます)か、データを書き込む時にキャッシュするかです。

最も人気のあるキャッシュパターンは、リードアサイドパターンでしょう。このパターンでは、アプリケーションはリクエストの中でまずキャッシュからデータを読んで返そうと試みます。もしその時点でキャッシュ内にデータがなければ、アプリケーションはデータベースからデータを読み出す様にフォールバックします。その際に、レスポンスを返す前にデータをキャッシュするので、次に同じデータが必要なリクエストが来た時にはその取得済のデータがキャッシュ内で利用可能となります。

対照的なパターンが、書き込みが成功した後にキャッシュに読み込ませる方法です。書き込みの成功後に、すぐに必要になるであろうと予想してデータを積極的にキャッシュにプッシュします。

読込時にデータをキャッシュする利点は、柔軟性と空間効率です。遅延読込はほとんどどんなデータセットでも使える柔軟なパターンです。個別のオブジェクトでも、複数オブジェクトの集合でも、集計した値でもなんでもキャッシュできます。データベースから直接取れる結果をキャッシュする場合でも、計算を行った結果をキャッシュする場合でも、クライアントに返す前に最終レスポンスを単純にキャッシュするだけなので、リードアサイドキャッシュは実装が簡単です。

これが、書き込み時に積極的にキャッシュする時にはより難しくなります。個別の項目を書き込み時にキャッシュするのは素直ですが、結果の集合や集計された値を積極的にキャッシュするのは、どんな読み出しパターンがあってそれらがどの様に書き込みに影響するのかを深く理解している必要があるため、だいぶ難しくなります。

加えて、遅延読込はキャッシュに使われる空間効率に優れています。実際に読み出されるかどうか分からないデータを書き込み時にキャッシュにも書くのではなく、読み出し時に一度キャッシュするだけです。多くのアプリケーションにおいて、個別のデータの読み出しのタイミングは時間と関連性があります。一度読み出されたデータは、そのすぐあとにも読み出される可能性が高いです。少なくとも一回リクエストされたデータだけをキャッシュすることで、より頻繁にアクセスされるデータに対してキャッシュを最適化することになります。

読み出し時にキャッシュする欠点は、初回読込が遅いことと、古いデータを返してしまう可能性です。データを読み出し時にキャッシュするので、各リクエストのデータは少なくとも一度は、遅くてキャッシュされていないパスを通る必要があります。アプリケーションによっては、これが局所最適な場合があります。

更に、読み出し時にしかキャッシュしないパターンでは、クライアントに古いデータを返してしまう可能性があります。もし関連するキャッシュデータの消去をせずにその下にあるデータが変わってしまった場合、ユーザーには混乱する結果が見えてしまうでしょう。アプリケーションはこれを緩和するためにキャッシュ時間を短くすることが可能ですが、そうするとキャッシュミス時に起こる上述の欠点を悪化させてしまうことになります。

どの様にキャッシュするか -- インライン vs アサイド

キャッシュ戦略を選ぶ際の最後の考慮点は、インラインキャッシュにするかアサイドキャッシュにするかです。

前のセクションで、リードアサイドキャッシュについてお話しました。アサイドキャッシュは最も素直なタイプのリモートキャッシュで、皆さんのサービスから明示的に指示されたデータを保存します。一般的には単純な get と set を持ち、どんなデータでも保存できる柔軟性がありますが、データを明確に保存する必要があります。もしキャッシュ内にデータがなければ、皆さんのサービスがどこかから元になるデータを探してきて、キャッシュを更新する責任があります。

一方で、インラインキャッシュはデータを取得する皆さんのサービスからは透過的なキャッシュになります。皆さんのアプリケーションは項目の取得をする際にはインラインキャッシュを直接叩きます。もしリクエストされたデータがキャッシュに無ければ、キャッシュ自身が上流のデータソースからデータを取得する処理を行ってくれます。

以下のアーキテクチャ図を見ると、これらのキャッシュがどうしてその名前をしているのかが分かるでしょう。アサイドキャッシュはアプリケーションの横に(アサイド)座っていて、データソースとは別に呼び出されます。一方、インラインキャッシュはデータソースへのリクエストの中で(インライン)使われています。

アサイドキャッシュの方が、ほとんどどんなユースケースでも使える柔軟性のために人気があります。加えて、最終のデータストアから疎結合で、キャッシュレイヤーでどの様に失敗を処理するかを選択することができます。

インラインキャッシュの利点は、皆さんのアプリケーションが簡素化できることです。キャッシュミスの際にデータベースにフォールバックするための複数のストアや関連するロジックについて、アプリケーションは心配する必要がありません。

インラインキャッシュの欠点はアプリケーションの可用性を下げる点です。この方式では、単純なキャッシュ機能だけでなくデータベースとの通信の責任を担う新しいインフラを追加することになります。もしキャッシュが落ちてしまうと、キャッシュ自身がデータベースと会話していたので、データベースへフォールバックすることが難しいでしょう。

もう一つのインラインキャッシュの欠点はそういうサービスが存在するかどうかです。インラインキャッシュは使っている下流のデータソースと密な連携をします。そのため、そのデータソースと連携するためには誰かが特別なキャッシュを構築する必要があります。従って、インラインキャッシュはデータベースに依存しないプロトコルか、特定のデータベースの独自のアドオンとして一般的には利用可能です。