🚀

Web配信のキャッシュ戦略についてまとめてみた

2024/04/07に公開

はじめに

こんにちは、23卒でバックエンドエンジニアをしています。@putcho01
普段の業務では新規サービスの開発を行なっています。
最近チーム内でAkamai CDNの配信設定・インフラ構築をすることがあったのですが、あまりにもキャッシュについて理解していませんでした。
ProxyやCDNを用いてキャッシュすることで、負荷耐性を向上させたり、より高速・安全なWebサービスを提供できるんだなくらいの知識しかなく、具体的にどのような観点でキャッシュを検討するのかや配信をすれば良いかは説明できませんでした。
そこで、以下の本を読みつつ調べたことをもとにProxy/CDN・HTTPでのキャッシュの行い方についてをまとめてみました。少しでも参考になれば幸いです。
https://gihyo.jp/book/2021/978-4-297-11925-6

配信のとらえ方

Webサイトの配信、スマートフォンゲームでのアセット配信、ライブ配信など、配信と呼ばれるものは数多くあります。

内容
Webサイトの配信 サーバーからHTML, CSS, Javascript, 画像などのファイルをブラウザに送る
ゲームアセット配信 サーバーから画像・音声などのアセットファイルをゲームアプリケーションに送る
ライブ配信 サーバーから細切れにした動画ファイルと更新されるプレイリストをアプリケーションに送る

配信をサーバーにあるコンテンツをクライアントに送ることと定義すると、これらはどれも配信と言えます。

VOD(動画配信)やスポーツのライブ配信など、規模やサービスの特性によって配信に求められる用件が変われば使う技術や構成・考慮すべき点も変わりますが、今回は基礎となる部分のキャッシュについて考えます。

キャッシュの分類1 / 格納場所による分類

配信において、キャッシュすることは重要です。
ブラウザでWebサイトを閲覧する場合、大まかに3ヶ所のキャッシュ格納場所があります。

  1. クライアントでのキャッシュ(ローカルキャッシュ)
  2. 配信経路上のキャッシュ
    • CDN でのキャッシュ
  3. オリジンのキャッシュ
    • Proxyのキャッシュ

それぞれのキャッシュの特徴を見てみます。

種類 特徴 用途
クライアントのローカルキャッシュ(ブラウザキャッシュ) CSS/Javascript/画像などをキャッシュすることで共通コンテンツを毎回ネットワークを通して取得する必要がなくなる CSS/Javascript/画像などをキャッシュ
経路上のキャッシュ(CDN) クライアントからのリクエストをエッジサーバーからレスポンスする 経路を最適化・大きなトラフィックをさばく・オリジンの負荷軽減
ゲートウェイ(オリジン側)のキャッシュ Proxyを用いてキャッシュ。オリジンサーバー近くに展開することが多い。 CDNを利用してもキャッシュの一貫性を維持しやすい

キャッシュの分類2 / キャッシュ対象の性質による分類

private/shared の分類

private - ユーザー情報を含んだAPIなど特定のクライアント向けキャッシュ
shared - サイトロゴやCSSなど複数クライアントで共有できるキャッシュ

キャッシュの位置とprivate/shared

ローカル、経路、ゲートウェイとキャッシュの位置によってprivate/sharedキャッシュをどのように保管すべきかは異なります。

キャッシュの位置 private shared 説明
ローカルキャッシュ ---
Proxy/CDNでのキャッシュ 複数クライアントからの参照を避けるため基本的にprivateキャッシュは保管しない

HTTPヘッダ・設定

配信において、HTTPヘッダは重要です。適切なHTTPヘッダの設定を行うことで、ローカルキャッシュを使いリクエストを削減することができます。また、ヘッダの解釈の差異によるキャッシュ事故を防ぐためにも重要です。

キャッシュ事故の事例

本来キャッシュされるべきでない情報がCDN側にキャッシュされてしまい、Web版のメルカリにおいて一部の個人情報が他者から閲覧できる状態になっていた。
CDN切り替え作業における、Web版メルカリの個人情報流出の原因につきまして

HTTPとキャッシュ

HTTPのキャッシュの仕様は RFC 9111 で決められています。
このRFCのセクション2では次の文があります。

Although caching is an entirely OPTIONAL feature of HTTP, it can be assumed that reusing a cached response is desirable and that such reuse is the default behavior when no requirement or local configuration prevents it.

つまり、 キャッシュはデフォルトの動作なので、キャッシュを防ぐ場合はCache-Controlで制御する必要があります。

Cache-Controlによるキャッシュ管理

Cache-Controlは、コンテンツの有効期限などキャッシュポリシーを設定するヘッダです。[1]

キャッシュの保存方法を指定する

Cache-Controlは保存の仕方を指定するオプションとして、未指定・public・private が存在します。それぞれの動作を次の表にまとめます。

指定 キャッシュ対象 経路上への格納 ローカルへの格納 通常キャッシュできないパターンもキャッシュ
未指定 shared
public shared
private private

Cache-Control:public の指定はsharedキャッシュとして扱いつつ、通常キャッシュができない場合でもキャッシュを行う指定です。そのため、sharedキャッシュとすることだけのためにpublicを指定するのは間違いです。

キャッシュの使われ方を指定する

no-storeとno-cacheの違い

--- 説明
no-store リクエスト・レスポンスのいかなる部分もキャッシュしてはいけない
no-cache キャッシュを使う際はオリジンの問い合わせを行い、キャッシュが有効でない限り使用してはいけない

no-cacheの動作
初回リクエスト

次回リクエスト

no-cacheは、例えば、頻繁に更新されるコンテンツに設定すれば毎回更新確認をし、更新がなければキャッシュを利用、更新があれば再取得されます。

キャッシュの更新方法を指定する

must-revalidate, proxy-revalidate, immutableの違い

--- 説明
must-revalidate 期限切れの再検証は必ずオリジンに問い合わせ。失敗時は504を返す
proxy-revalidate privateキャッシュに適用されない以外はmust-revalidateと同様
immutable 変更されないオブジェクトであることを示す

immutableはキャッシュが変更されることがないため、キャッシュの有効期限を指定したとしても再検証のためのリクエストは行われず保持したキャッシュを利用します。そのためよく考えて使うべきです。
https://datatracker.ietf.org/doc/html/rfc7234#section-5.2.2.1

no-cacheとの違い

Cache-Control指定 動作
no-cache キャッシュを行い、毎回再検証を行う
max-age=10, must-revalidate 10秒間はキャッシュし、期限が切れたら再検証を行う

max-ageとno-cacheは共存できないが、max-ageとmust-revalidate/proxy-revalidateは共存できます。

オブジェクトの取り扱いを指定する

no-transform
中間経路のサービス提供する側以外が、オブジェクトに対して変更を加える場合があります。このような操作を行わないようにしたい場合にno-transformを利用します。[2]

Cache-ControlとExpiresヘッダ

max-ageでは相対的にキャッシュ時間を指定します。一方で、Expiresヘッダは値に時刻を設定することで絶対時間で指定します。

Expires: Thu, 01 Dec 1994 16:00:00 GMT

max-ageと同時に指定された場合はmax-ageが優先されます。[3]

キャッシュさせない場合

Cache-Controlは次のような設定がおすすめ。[4]

CacheControl:private,no-store,no-cache,must-revalidate

一般的にキャッシュで事故が起こる背景にあるのは、経路上でキャッシュが行われること、それが本来見えるべきでないクライアントに見えてしまうことです。事故を防ぐには、まずprivateの設定で経路上のキャッシュを避けることが必要です。キャッシュへ保存させないために、no-storeの設定が必要です。キャッシュを使わないno-cacheも指定します。最後にオリジンがダウンしていた場合にキャッシュが使われることを防ぐため、must-revalidateを指定します。
田中 祥平. Web配信の技術HTTPキャッシュ・リバースプロキシ・CDNを活用する

ヘッダの対策と同時に、コンテンツそのもののサイズ削減は重要ですが、圧縮の話はここでは省略します。

キャッシュ戦略

キャッシュ戦略についてはこちらの記事が参考になりました。
https://aws.amazon.com/jp/builders-library/caching-challenges-and-strategies/

キャッシュキー戦略

Akamai Blog | Using CDN Cache Efficiently
キャッシュキーについて - Amazon CloudFront

キャッシュキーを設定する際、クライアントからのリクエストを組み合わせてキーを生成する必要がありますが、条件が合えば再利用されやすい候補キーを選択する必要があります。
例えば、静的な画像はURLがキャッシュキー。動的なページは、ユーザーIDなどの情報が候補キーとして必要になる可能性があります。
また、キャッシュキーを適切にしないとキャッシュヒット率が落ちてしまう可能性もあります。

そのため、どのようにキャッシュの候補キーを設定するかが重要になりますが、重要なことはキャッシュ事故を防ぎ安全に設定することです。
例えば、フェイルセーフな考えで、ユーザー情報をキャッシュキーに含めない場合はそれが含まれる可能性のあるヘッダ自体を消すのがおすすめめです。他の人のマイページが見れるとった致命的な事故を防ぐことができます。

TTLの決め方

TTLを長くすればキャッシュヒット率は上がりリクエスト数は少なくなりますが、むやみに長くするのではなく適切なTTLを設定する必要があります。
Proxy/CDNとクライアント双方でキャッシュをする場合、それぞれどの程度のTTLが使えるかに注意すべきです。例えば、maxageを1時間と指定してProxy/CDNでキャッシュをして、50分経過後にリクエストしてきたクライアントに残されたTTLは10分になります。すると時間の経過とともにクライアントからのリクエスト数が増えます。
これを避けるための方法の一つにs-maxageを利用する方法があります。[5]
次のように指定すれば、全体で1時間、経路上・ローカルでそれぞれ最低30分のキャッシュができます。

Cache-Control: max-age=3600, s-maxage=1800

最後に

キャッシュを設定することで、WEB高速化させたり・負荷を軽減させ安定したサービス提供ができることが分かりました。しかし、コンテンツの特性に合わせて適切に設定をしなければキャッシュによる事故に繋がったりキャッシュヒット率の低下につながる可能性があります。
キャッシュについて正確に理解し、HTTPキャッシュ、Proxy、CDNを適切に導入できるようにしたいです。

また、画像や動画のファイルサイズ圧縮化などの理解も深めたいと思っています。
最後までお読みいただき、ありがとうございました。

脚注
  1. Cache-Control - HTTP | MDN ↩︎

  2. 「Cache-Control: no-transform で各種メディアの変換(再圧縮等)を防ぐ」https://blog.knjcode.com/cache-control-no-transform/ ↩︎

  3. 「RFC7234 5.3 Expires」https://datatracker.ietf.org/doc/html/rfc7234#section-5.3 ↩︎

  4. 「キャッシュされたくない場合のHTTPヘッダのベストプラクティス」 https://turningp.jp/network_and_security/http_header-cache ↩︎

  5. 「CDNキャッシュ向けレスポンスヘッダーCache-Control:s-maxage を触ってみた」https://dev.classmethod.jp/articles/play-with-shared-cache-response-header-s-maxage/ ↩︎

Discussion