logo
Published on

HTTPキャッシュ徹底解説

Authors

はじめに

対象読者は、HTTPヘッダーを用いたキャッシュ制御のやり方を知らないバックエンドエンジニアです。
この記事のゴールは、適切なキャッシュ戦略1を自分で考えられるようになることです。

Table of Contents

HTTPキャッシュとは

HTTPキャッシュとは、HTTPヘッダーを用いてキャッシュ2を制御する仕組みです。

HTTPキャッシュの仕組み

以下画像の通り、キャッシュサーバーがキャッシュしているパターンと、クライアントがキャッシュしているパターンがあります3

HTTPキャッシュ
  • キャッシュサーバーがキャッシュしているパターン
    • キャッシュサーバーにキャッシュがある場合
      1. クライアントがキャッシュサーバーにリクエスト
      2. キャッシュサーバーがクライアントにレスポンス
    • キャッシュサーバーにキャッシュがない場合
      1. クライアントがキャッシュサーバーにリクエスト
      2. キャッシュサーバーがオリジンサーバーにリクエスト
      3. オリジンサーバーがキャッシュサーバーにレスポンス
      4. キャッシュサーバーがレスポンスの内容をキャッシュとして保存
      5. キャッシュサーバーがクライアントにレスポンス
  • クライアントがキャッシュしているパターン
    • クライアントにキャッシュがある場合
      1. クライアントがどこにもリクエストを送らずキャッシュを使って画面表示などを行う
    • クライアントにキャッシュがない場合
      1. クライアントがオリジンサーバーにリクエスト
      2. オリジンサーバーがクライアントにレスポンス
      3. クライアントがレスポンスの内容をキャッシュとして保存
      4. クライアントが画面表示などを行う

さらに、キャッシュがあったとしても有効期限が切れている場合は、オリジンサーバーに有効性を確認するといった動作が入ることもあります。

これがHTTPキャッシュの仕組みです。
次に、キャッシュを使うと何が嬉しいのか解説します。

HTTPキャッシュのメリット

大まかに以下のようなメリットがあります。

  • パフォーマンス向上: たとえば重い処理をオリジンサーバーが行っている場合、オリジンサーバーを経由しないのでパフォーマンスが向上する4
  • インフラコスト削減: たとえばオリジンサーバーの裏にDBサーバーがいる場合、これらのサーバーリソースを使わなくて済むのでインフラコストが削減できる。
  • スケーラビリティ向上: キャッシュを読んで返す処理は比較的軽量なので、裏技5を使わずとも大量アクセスに強いシステムになる。

これがHTTPキャッシュのメリットです。
次に、HTTPキャッシュを実現する具体的な方法を解説します。

HTTPキャッシュを実現する方法

ここまで説明してきたようなキャッシュ制御は、HTTPヘッダーを使って実現します。
クライアントとサーバーがHTTPヘッダーを介して、キャッシュについての希望動作やキャッシュの利用可否などをやり取りして、具体的な動作が決定されます。

ここまででHTTPキャッシュの概要については以上です。
次に、具体的にHTTPキャッシュに関連するHTTPヘッダーを網羅的に解説します。

HTTPキャッシュに関するHTTPヘッダー

そこそこ数があるので、以下カテゴリに分けて解説を進めます。

  • キャッシュ制御: キャッシュの挙動を直接指示するためのヘッダー
  • キャッシュ検証: キャッシュの有効期限が切れた後、そのリソースがサーバー上で更新されているかを検証するためのヘッダー
  • その他: キャッシュの挙動に影響を与えたり、キャッシュに関する情報を提供したりするヘッダー

HTTPヘッダー: キャッシュ制御

キャッシュの挙動を直接指示するためのヘッダーです。
主にサーバーがレスポンスで指定します。

Cache-Control

ディレクティブと呼ばれるコマンドを指定することによってキャッシングの動作を指定します。
リクエストとレスポンス両方で指定できます。
ディレクティブにはパラメーターがあるものとないものがあり、複数のディレクティブを指定する場合には、カンマ,で区切ります。

Cache-Control: private, max-age=0, no-cache

リクエストで指定できるディレクティブは以下です。

ディレクティブパラメーター説明
no-cacheなしオリジンサーバーへの強制的な再検証
no-storeなしキャッシュはリクエスト、レスポンスの一部分を保存してはならない
max-age=[秒]必須指定した値よりキャッシュされたリソースが新しい場合には、それを受け入れることができる
max-stale(=[秒])省略可能期限切れのレスポンスを受け入れる。値を指定した場合、有効期限が切れてから指定時間内であれば受け入れられることを伝える。
min-fresh=[秒]必須指定した時間は新鮮なレスポンスを望む。たとえば60秒と指定されている場合、60秒以内で有効期限が切れるリソースをレスポンスとして返すことはできない。
no-transformなしプロキシはメディアタイプを変換してはならない
only-if-cachedなし目的のリソースがキャッシュサーバーのローカルキャッシュにある場合のみレスポンスを返すよう要求
cache-extension-新しいディレクティブのためのトークン

レスポンスで指定できるディレクティブは以下です。

ディレクティブパラメーター説明
publicなし他のユーザーにも返すことができるキャッシュを行ってもよいと明示する
privateなし特定のユーザーに対してのみのレスポンス
no-cache(=[ヘッダーフィールド名])省略可能有効性の再確認なしではキャッシュは保存してはならない。ヘッダーフィールド名が指定された場合、そのフィールドだけキャッシュできない。
no-storeなしキャッシュはリクエスト、レスポンスの一部分を保存してはならない
no-transformなしプロキシはメディアタイプを変換してはならない
must-revalidateなしキャッシュ可能であるが、オリジンサーバーにリソースの再確認を要求する
proxy-revalidateなし中間キャッシュサーバーに対し、キャッシュしたレスポンスの有効性の再確認を要求する
max-age=[秒]必須キャッシュサーバーが有効性の再確認を行わずにリソースをキャッシュに保持しておける最大時間
s-maxage=[秒]必須max-ageと同様。違いは、共有キャッシュサーバーにだけ適用されるという点。
cache-extension-新しいディレクティブのためのトークン

Expires

レスポンスに指定されます。
リソースの有効期限の日時を伝えます。
Cache-Control: max-age=[秒]が指定されている場合には、このディレクティブは無視されます。

Pragma

リクエストに指定されます。
HTTP/1.0時代の古いヘッダーです。
Pragma: no-cacheという形で使われ、Cache-Control: no-cacheと同義です。
下位互換性のために残されています。

HTTPヘッダー: キャッシュ検証

キャッシュの有効期限が切れた後、そのリソースがサーバー上で更新されているかを検証するためのヘッダーです。

ETag

レスポンスに指定されます。
リソースを一義的に特定するための文字列を伝えます。
リソースが更新されるとETag値も更新する必要があります。

後述のIf-None-Matchヘッダーと組み合わせて使います。

If-None-Match

リクエストに指定されます。
クライアントが前回取得したETagの値をサーバーに送り、「このETagと一致しない場合のみ、リソースを送ってください」と要求します。
一致した場合はキャッシュが有効であることがわかります。

Last-Modified

レスポンスに指定されます。
リソースの最終更新日時を示します。

後述のIf-Modified-Sinceヘッダーと組み合わせて使います。

If-Modified-Since

リクエストに指定されます。
クライアントが前回取得したLast-Modifiedの日時をサーバーに送り、「この日時以降に更新されている場合のみ、リソースを送ってください」と要求します。
指定日時以降に更新されていない場合は、キャッシュが有効であることがわかります。

HTTPヘッダー: その他

キャッシュの挙動に影響を与えたり、キャッシュに関する情報を提供したりするヘッダーです。

Vary

レスポンスに指定されます。
キャッシュをコントロールするために使います。
オリジンサーバーがプロキシサーバーに対して、ローカルキャッシュの使い方の指示を伝えます。

オリジンサーバーからVaryで指定されたレスポンスを受け取ったプロキシサーバーは、以後はキャッシュしたときのリクエストと同様のVaryに指定されているヘッダーフィールドを持つリクエストに対してのみキャッシュを返すことができます。

以下例で考えます。

Vary: Accept-Language

この場合、同じ言語(Accept-Language)のリクエストにのみキャッシュからレスポンスします。
すでにキャッシュがあるURIであってもAccept-Languageが異なるリクエストに対してはキャッシュを返しません。

Age

レスポンスに指定されます。
どれぐらい前にオリジンサーバーでレスポンスが生成されたかを伝えます。
レスポンスをしたのがキャッシュサーバーの場合、キャッシュしたレスポンスが再実証されてから検証した時間になります。

Clear-Site-Data

レスポンスに指定されます。
Clear-Site-Data: cacheとすることで、そのサイトのHTTPキャッシュを強制的にクリアさせることができます。

ここまででHTTPキャッシュに関するHTTPヘッダーについては以上です。
次に、これらの情報を元にどうキャッシュ戦略を立てていけばいいか解説します。

キャッシュ戦略を立てるための観点

キャッシュ戦略を立てるのに役立つ、5つの観点を紹介します。
それぞれについて自問自答することで、自然と最適な戦略が立てられるはずです。

  • 可視性: 全員に共通の内容か、個人向けか
  • 鮮度: どれくらいの頻度で更新されるか
  • 検証方法: 期限切れの確認をどう効率化するか
  • 厳密度: 古い情報のレスポンスを許容するか
  • 文脈: 特定の文脈における挙動をどうすべきか

可視性: 全員に共通の内容か、個人向けか

レスポンスが全ユーザーで全く同じ内容かどうかを判断します。

  • 全ユーザーで全く同じ内容: Cache-Control: publicを指定。たとえば、一般公開されている記事一覧/api/articlesなど。
  • 特定のユーザーだけの内容: Cache-Control: privateを指定。たとえば、ユーザープロフィール/api/meなど。

鮮度: どれくらいの頻度で更新されるか

データの寿命を定義して max-age の値を決定します。

  • 変更頻度がときどき〜ほとんど不変: 適切なCache-Control: max-age=[値]を設定する。
  • リアルタイム性が高い: Cache-Control: no-cacheを指定して常に再検証されるようにする。
  • そもそもキャッシュすべきではない: Cache-Control: no-storeを指定して一切の保存を禁止する。たとえば、秘匿性の高いトークンなど。

検証方法: 期限切れの確認をどう効率化するか

キャッシュの有効期限が切れたとして、キャッシュが確かに古いかどうか確認する方法を決定します。
この場合、通常はオリジンサーバーにリソースを再度取得することになります。
仮にキャッシュとオリジンサーバーのリソースが同一であったとしても、です。

この挙動で問題ないならいいのですが、もしもっと通信を効率化したい場合は以下の実現を検討します。

  • ETagとIf-None-Matchを用いて検証: キャッシュが古くない場合、新しいリソースを丸ごとダウンロードしなくて済むので通信量が減る。基本的には下記よりこちらを推奨。
  • Last-ModifiedとIf-Modified-Sinceを用いて検証: 同上のことができるが、タイミングによっては更新を検知できないことがある。

厳密度: 古い情報のレスポンスを許容するか

キャッシュが古くなったとして、古い情報をレスポンスしていいかどうかを判断します。
たとえば、オリジンサーバーにアクセスできない場合、キャッシュの内容をレスポンスしていいか、それともきちんとエラーを返してほしいかを決定するということです、

  • エラーにするくらいなら古い情報がレスポンスされてもいい: 特に追加で指定するHTTPヘッダーはない
  • 少しでも古い情報は絶対にNG: Cache-Control: must-revalidateを指定してオリジンサーバーへの再検証を必須にする。検証できない場合は504 Gateway Timeoutを返す。たとえば、銀行の口座残高など。

文脈: 特定の文脈における挙動をどうすべきか

CDNの利用やレスポンスの圧縮など、特定の文脈に応じてキャッシュの挙動を微調整します。

  • CDNのキャッシュ期間を指定したい: Cache-Control: s-maxage=[秒]を指定します。
  • リクエスト内容によってキャッシュを使い分けたい: Varyヘッダーを指定します。例えばVary: Accept-Encodingと指定すると、キャッシュサーバーは同じURLでも「gzipで圧縮されたレスポンス」と「非圧縮のレスポンス」を別物として管理してくれます。

まとめ

  • HTTPキャッシュは、リソースのコピーを一時的に保存する仕組みであり、パフォーマンス向上、インフラコスト削減、スケーラビリティ向上に貢献します。
  • キャッシュの制御は、クライアントとサーバーがやり取りするHTTPヘッダーによって実現されます。
  • 最適なキャッシュ戦略を立てるには、以下の5つの観点からデータやリソースの性質を分析することが有効です。
    • 可視性: 全員に共通の内容か、個人向けか(public, private
    • 鮮度: どれくらいの頻度で更新されるか(max-age, no-cache
    • 検証方法: 期限切れの確認をどう効率化するか(ETag
    • 厳密度: 古い情報のレスポンスを許容するか(must-revalidate
    • 文脈: 特定の文脈における挙動をどうすべきか(s-maxage, Vary

参考文献

Footnotes

  1. この記事ではHTTPキャッシュを用いたものに限ります。キャッシュはHTTPキャッシュ以外にもあります。全部は扱いません。扱えません。

  2. プロキシサーバーやクライアントのローカルディスクに保存されたリソースのコピーのことです。現金のことではありません。

  3. 両方が複合された構成が現実に近いですが、説明を簡単にするためここでは分けました。"分ける"と"分かり"やすいのです。

  4. 厳密に言えば「パフォーマンスが向上するかもしれません」ですね。とはいえ、冗長性を廃するためにここでは言い切りの形を取りました(マサカリをつきつけられながら)。

  5. サーバースペックの向上など札束で殴る力技のことです。