🐥

Cache Eviction と 2つのTTL (Edge と Browser)

2023/10/05に公開

先日とある場所で、Cache Eviction について説明する必要があり、よくわからないので文章でまとめて欲しいと言われたので、ブログでまとめます。Cache Eviction は CDN 専門でやっている人にとっては良く知られた言葉ですが、一般的には意識するケースはあまり多くないため、良い機会だと考えい私の知識のおさらいを兼ねてまとめます。

CDNによるキャッシュの基礎について

WEBシステムでCDNを用いるメリットは主に2つです。高速化とセキュリティです。セキュリティは本記事からは割愛しますが、簡単に言うと、「攻撃者は常に勤勉である」ということです。攻撃者は常に最新の知識でシステムをたゆまなく狙います。そのための防御も、本質的には同じレベルの知識を持っている人を配置する必要がありますが、これは一般企業には大きな負担ですし、防御できる識者は、全企業に配置できるほど存在していません。このため、攻撃者とWEBサーバの間に専門家が責任を持つ防御システムを配置する必要があります。
本題に戻ります。高速化の最大の要因はキャッシュです。

Edgeと言われるCDNのサーバがコンテンツをキャッシュし、ユーザーのリクエストに対して、一般的にはよりNetwork的に近しいデータセンターからコンテンツを配信してあげることで通信を高速化させます。一般的にという点が重要です。時にはユーザーとしてオリジンの方がよりNetwork的に近いケースがあります。ではこの場合CDNは意味をなさないかといえばそうではありません。
WEBサーバはとても多くのことを一度に処理しますが、メインはプログラムの実行に多くのコンピュートリソースを費やします。これと同時並行でユーザーからのリクエストへの対応(TLS暗号化処理含む)を行っています。これを分業制にして、ユーザーからのリクエストの処理に特化した基盤を用意することで、ウェブサーバは本来行うべき仕事に集中させることが出来、リクエストの処理に特化した基盤は、それに特化している分より高速に動作します。
これによりCDNはWEBサイト全体を高速化させます。

キャッシュのTTLについて

TTL = Time To Live です。
キャッシュされたコンテンツをどの期間有効とさせるか?を秒数で設定する物です。この値が残っている間は(1秒につき1減っていくと想定して)オリジンが保有するコンテンツは使用される、CDNからキャッシュがユーザーのリクエストに対して配信されます。

キャッシュを運用する際にはキャッシュTTLの設定がつきものです。デフォルト値というものは存在しますが、配信するコンテンツ事、Webサイトのユーザー数毎で最適な値は異なるのでメトリクスを見ながらTTLをチューニングしていく必要があります。なぜなら、オリジンのコンテンツは更新されているにも関わらず、CDNが古いコンテンツを配信し続けるケースがあるからです。

キャッシュ の Purge

この際意図的にAPI経由などで今あるキャッシュを捨てる必要性が出てきます。これを行うのがCacheのPurgeです。これによりCDN側のキャッシュを捨てて、ユーザーからのリクエストの際は再度オリジンから新しいコンテンツを配信し、そのコンテンツを新しいTTLでCDN側でキャッシュさせます。例えばニュースサイトの場合、トップページは頻繁に変わりますので、そのページだけキャッシュさせない、などのチューニングが必要となります。

2種類のTTL Edge と Browser

コンテンツのキャッシュには2種類存在します。今まで説明を行ってきたのはEdgeです。一方ブラウザもCDNとは無縁で単独でキャッシュの機能を持ちます。よく読み込むサイトや画像などはブラウザ内部でキャッシュを行い、読み込みを行わなくてもWEBサイトが表示される仕組みです。
CloudflareではCDN側のキャッシュ(Edge)とブラウザのキャッシュ両方に個別のTTLをセットすることが出来ます。しかしながらこのBrowser Cacheは時として課題を引き起こします。

キャッシュTTLが無視されるケース

著作権に厳しいコンテンツなどは意図的にTTLを0にして、ブラウザにコンテンツをキャッシュさせないケースもあります。しかし、Browser CacheBrowserはユーザーの環境で動作します。デベロッパツールやMod Header等ブラウザの挙動を書き換えるツールを用いることでキャッシュの挙動を変更させることは実は難しくはありません。このため、Browser TTLはあくまで目安であり、それを保証するものではないことに注意してください。一方Edge TTLは可能な限りOriginから出される指示(キャッシュを行う可否)などに従い、指定された TTLの間コンテンツを維持するように努めます。しかしそれでもキャッシュが削除されてしまうケースがあります。

Cache Eviction

それが本題のCache Evictionという現象です。管理者が能動的にCacheを削除するのがPurge、意図せず削除されてしまうのがEvictionです。Cloudflareに限らずすべてのCDNでCacheを保存するストレージは有限であり、これを複数のユーザーで共有します。このためTTLの間キャッシュを保持することが出来ないケースもあります。CDNによっては、ユーザーアカウント毎にキャッシュストレージ容量を購入することでその現象を回避させることが出来ます。Cloudflareには有償でそのオプションが存在しています。
キャッシュは利用されればされるほど(HIT)、WEBサイト全体のパフォーマンスは向上します。キャッシュが捨てられてしまった後ユーザーからのリクエストが来た場合、キャッシュの利用ができないため(MISS)、オリジンへ取りに行くため配信は遅延します。このためなるべくキャッシュはEdgeに残しておき高いHIT率を維持したいのがユーザーの本音です。

Cache Eviction の仕組み

私の知る限り、Cache Evictionの詳細メカニズムが開示されているケースはなく、Cloudflareも同様ですが、一般的にはアクセス数が少ないもの、より古いもの(TTLが短い=残り保存秒数が少ない)ものから削除されます。Cloudflareではこれを防ぐ仕組みを3つ提供しています。
1.Tiered Cache
2.Cache Reserve
3.WorkersによるPre-Fetch (暖気)

0.まず最初に、テスト

Cache Evictionの影響がユーザーパフォーマンスに強く影響を与えているかを確認してください。以下のサイトなどでパフォーマンスを確認できます。
https://pagespeed.web.dev/
影響が限定的である場合気にする必要はありません。
またテストを行う際、コンテンツに対してある程度のアクセス数を発生させてください。観測範囲では数件程度のアクセスではCache Evictionが発生するケースがあるため、例えば負荷ツールなどを用いて、数十/秒でアクセスしてみて下さい。

1. Tiered Cache


まずCDNのEdge数は非常に多く、それぞれのセンターが個別にCache用ストレージを持っていることに留意してください。このため、一度コンテンツがキャッシュされたとしても、複数回のアクセスが別のEdgeセンターにルーティングされた場合CacheはMISSとなります。これは仕様通りの動作です。この問題を緩和させるのがTiered Cacheです。複数Cacheを束ねる親玉、大きいストレージを持ちそこから各Edgeにキャッシュを配信させてあげる存在です。末端のEdgeはキャッシュがMISSした場合、まずTiered Cacheにキャッシュを取りに行きます。そこでもキャッシュが存在しない場合、末端のEdgeの代理としてキャッシュをオリジンに取りに行きます。これはOriginへの接続回数を減らす効果も持つため、全体をパフォーマンス向上させることが期待できます。Cloudflareでは東京に複数のEdgeを束ねるTiered Cacheが存在しています。
各EdgeセンターでEvictionが発生したとしてもその影響を軽減できます。

2. Cache Reserve


Cache Reserveはアカウント専用にCacheを保存する専用ストレージを作成するオプションです。これを用いることでその容量の範囲内ではEvictionが発生しなくなります。
注意点があります。Cache ReserveはR2というCloudflareが提供するオブジェクトストレージ上に作成されます。専用の領域を確保するためEvictionの問題解決には役に立ちますが別の複合化したパフォーマンスの課題に対しては効果の検証が必要です。
オリジンにR2を使用しており、Edge→R2への通信も高速化したいという要件がある場合、同じ場所に専用Cacheストレージが出来るため、そのパフォーマンスは改善しない可能性があります。

3. Workers による Pre-Fetch (暖気)

例えばビデオ配信のように、ある特定の時間から一斉配信を開始させるがその前にキャッシュをさせておきたい場合、Workersを例えば5秒cronで起動して定期的に読み込ませておく手法も有効です。

export default {
  async scheduled(controller, env, ctx) {
    const url = 'https://connpass.com/api/v1/event/?keyword=cloudflare';
    async function gatherResponse(response) {
      const { headers } = response;
      const contentType = headers.get('content-type') || '';
      const text = await response.text();
      return text;
	    }
	 }
     return new Response("暖気");
}

cron triggerを用いた場合、特定のEdgeでのみ実行されるケースがあり、その場合コンテンツは特定のEdgeでのみキャッシュされます。
これを防ぎさらに全面的に行う場合、cron triggerではなくHTTP fetch でWorkersを作成し複数拠点からブラウザでアクセスさせてみて下さい。

export default {
  async fetch(request) {
    /**
     * Example someHost at URL is set up to respond with HTML
     * Replace URL with the host you wish to send requests to
     */
    const someHost = 'https://暖気したいコンテンツのURL';
    const url = someHost + '/ja-jp/';

    /**
     * gatherResponse awaits and returns a response body as a string.
     * Use await gatherResponse(..) in an async function to get the response body
     * @param {Response} response
     */
    async function gatherResponse(response) {
      const { headers } = response;
      const contentType = headers.get('content-type') || '';
      if (contentType.includes('application/json')) {
        return JSON.stringify(await response.json());
      } else if (contentType.includes('application/text')) {
        return response.text();
      } else if (contentType.includes('text/html')) {
        return response.text();
      } else {
        return response.text();
      }
    }

    const init = {
      headers: {
        'content-type': 'text/html;charset=UTF-8',
      },
    };

    const response = await fetch(url, init);
    const results = await gatherResponse(response);
    return new Response(results, init);
  },
};

Discussion