こんにちは。バックエンドエンジニアの id:kasei-san です
最近は映画「リベリオン」を見たいのですが、配信しているサービスがTSUTAYAとU-NEXTしかなく、加入すべきか悩んでいます
本日はHTTP キャッシュについて解説します
経緯
LCLではキャッシュサーバにVarnishを使用していますが、現在Fastlyへの移行を検討中です
移行についての作業を任されたため、HTTP キャッシュについて勉強し直そうと思いこの記事を書きました!
HTTPキャッシュとは
そもそもHTTPキャッシュとは何でしょうか?
HTTP上でのキャッシュについてのRFC RFC 7234 — HTTP/1.1: Caching (日本語訳) の序論によりますと...
HTTP キャッシュ は、応答メッセージの局所的な格納域であり,その中に格納されるメッセージたちの[ 蓄積, 検索取得, 削除 ]を制御する下位システムである。 キャッシュは、未来の等価な要請に対する[ 応答時間やネットワーク帯域幅の消費量 ]を抑制するために,キャッシュ可能な応答を格納する。 どのクライアント/サーバもキャッシュを使役してよい
上記にあるように、ネットワーク帯域幅やサーバの応答時間(ひいてはリソース)の節約のために、HTTPキャッシュは利用されます
- ユーザ側は、レスポンスが早くなってうれしい
- 運営側は、サーバや帯域の負荷が減ってうれしい
HTTPキャッシュは、ユーザ、運営側それぞれにメリットをもたらしてくれます
HTTPキャッシュのための戦略
さて、そのようなHTTPキャッシュですが、通信量を減らすために2つの戦略を用意しています
- いかにHEADリクエストだけで済ませるか
- そのHEADリクエストの回数を減らせるか
それぞれについて解説します
いかにHEADリクエストだけで済ませるか
クライアント(ブラウザおよびキャッシュサーバ)は、サーバに2回目のリクエストを投げる時に、コンテンツに変更がなかったか? を問い合わせることができます
そして、変更がなかった場合、サーバはコンテンツを返す代わりに「更新がなかった」という意味のレスポンスコード 304(Not Modified) を返します
上記のやり取りを 検証 と呼びます
検証 のしくみにより、コンテンツが更新されるまでは、コンテンツの本体へのリクエストは発生せず、クライアント/サーバ双方の負荷が低減されます
(コンテンツを生成するコストと、それを通信する帯域の負荷が発生しなくなるため)
そのHEADリクエストの回数を減らせるか
しかし、上記の仕組みだけでは更新の問い合わせ自体を減らすことはできません
そのために、HTTPでは キャッシュの有効期限 も設定することが可能です
キャッシュの有効期限のあいだは、検証は行わず、クライアント内にあるキャッシュに保存されているコンテンツを表示します
そのため、サーバへの通信は一切発生せず、クライアント/サーバ双方の負荷はさらに低減されます
以降では、上記の具体的なしくみを解説いたします
LastModified
と Etag
LastModified
と Etag
は、コンテンツの更新の可否をチェックするためのものです
それぞれがレスポンスヘッダの値で、「最終更新日時」と「コンテンツの識別子」を表します
LastModified
最終更新日時が格納されます
例
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
ブラウザがこれを受け取った場合、次にリクエストするときに、最終更新日時をリクエストヘッダに加えます
具体的には If-Modified-Since
の値に最終更新日時を格納してリクエストします
サーバ側は If-Modified-Since
の値を見て、
コンテンツの最終更新日時 = If-Modified-Since の値
ならば、304(Not Modified) を返しますコンテンツの最終更新日時 > If-Modified-Since の値
ならば、200 と、コンテンツ本体を返します
これにより、更新が無い限り、コンテンツの生成と通信のコストは発生しなくなります
Etag
エンティティタグの略で、コンテンツの変化をチェックをするための値が格納されます
コンテンツの更新が行われるとETagの内容が変化します。
ただし、値の内容については具体的にRFCでは定められていません
現実には、コンテンツのmd5値が使われることが多いようです → AmazonS3の例
例
ETag: "686897696a7c876b7e"
ブラウザがこれを受け取った場合、次にリクエストするときに、Etagの値をリクエストヘッダに加えます
具体的には If-None-Match
の値にEtagの値を格納してリクエストします
サーバ側は If-None-Match
の値を見て、
Etag = If-None-Matchの値
ならば、304(Not Modified) を返しますEtag ≠ If-None-Matchの値
ならば、200 と、コンテンツ本体を返します
これにより、更新が無い限り、コンテンツの生成と通信のコストは発生しなくなります
LastModified
と Etag
はどちらを使うべきなの?
RFC2616の13.3.4 によると両方入れることが推奨されています
HTTP/1.1 オリジンサーバにとってより望まれる動作とは強いエンティティタグと Last-Modified 値の両方を送る事である
これは(恐らくですが)どちらかしかサポートしていないブラウザが過去にあったためと思われます
Expires
と Cache-Control
Expires
と Cache-Control
は、HEADリクエストの回数を減らすためのレスポンスヘッダで、キャッシュの寿命についての定義が行われます
それぞれ以下のような特徴を持っています
Expires
: 古い仕組み。単純にキャッシュの寿命そのものしか定義できない。モダンなブラウザではCache-Control
が優先されるCache-Control
: キャッシュの寿命について、細かい制御が可能
最近のブラウザでは無視される Expires
は割愛して Cache-Control
のみ解説をいたします
Cache-Control
Cache-Control
レスポンスヘッダは細かいキャッシュの制御のために命令を格納することが可能です
RFC7234によると以下の9つがあります
これらの組み合わせでcacheのルールが表現されます
Cache-Control: no-cache Cache-Control: no-store Cache-Control: no-transform Cache-Control: public Cache-Control: private Cache-Control: max-age=<seconds> Cache-Control: s-maxage=<seconds> Cache-Control: must-revalidate Cache-Control: proxy-revalidate
また、MDN によると、拡張として以下の3つがあります
Cache-Control: immutable Cache-Control: stale-while-revalidate=<seconds> Cache-Control: stale-if-error=<seconds>
以下にて、それぞれ解説していきます!
Cache-Control: no-store
キャッシュサーバもブラウザにもcacheを禁止する要求です
Cache-Control: no-cache
こんな名前ですが、cacheを禁止するのではなく
cacheした後も、更新がないかの問い合わせは常に行ってほしい という要求です
ややこしいですね
Cache-Control: no-transform
キャッシュサーバはオリジンサーバ(元データが格納されているサーバ)からのレスポンスの改変をしてはならないという要求です
キャッシュサーバによっては、画像を圧縮するなどオリジナルのデータを改変することがあります
Cache-Control: public
不特定向けの情報のため、キャッシュサーバでのキャッシュを許可するという要求です
Cache-Control: private
個人向けの情報のため、キャッシュサーバでのキャッシュを許可しないという要求です
たとえばログインが必要なページなど
Cache-Control: max-age=
キャッシュの残り寿命(秒)を表します
この時間が経過した後は検証が行われ、 304 Not Modified
が返るようならば、引き続きキャッシュされます
Cache-Control: s-max-age=
キャッシュサーバにおいて、max-age
や Expiresヘッダよりも優先的に使用されるキャッシュの残り寿命を表します
ブラウザでは無視されます
Cache-Control: must-revalidate
キャッシュの寿命が切れた場合、必ず検証をしてほしいという要求です
そのため、 no-cache
と、 must-revalidate, max-age=0
は等価 で、キャッシュさせたくないレスポンスにつけるヘッダとして、それぞれよく使われています
Cache-Control: proxy-revalidate
must-revalidate
と同様ですが、キャッシュサーバに対してのみ適用されます
Cache-Control: immutable
変更されないため、検証が不要であるという要求です
実験的な機能のため、Chromeにはまだ実装されていないようです
Cache-Control: stale-while-revalidate=
検証が必要となった時に、バックグラウンドで検証をしている間はキャッシュの使用を許可する秒数を表します
普通は検証が必要となった場合、その場で検証を行いますが、これが設定されている場合、検証はバックグラウンドで行い、コンテンツはキャッシュを返すようになります(結果、検証しつつ素早いレスポンスが可能になる)
Varnishのgrace modeの挙動はこれに近い印象ですね
なお、stale-while-revalidate
は、現在ブラウザに実装されてませんが、fastlyでは後述の stale-if-error
とともに実装されています
Cache-Control: stale-if-error=
検証が失敗した場合に、キャッシュの使用を許可する秒数を表します
cacheの寿命が切れた後、オリジンサーバが死んでいた場合に、ここで設定された時間の間だけはコンテンツの表示が可能となります
頼もしい機能ですね
Cache-Control: max-age
が未定義でも、ブラウザが勝手にキャッシュすることがある
RFC2374 によると、
サーバが明確な失効時刻を返さない場合、ブラウザは LastModified
などを頼りに経験的な失効時刻を決定して良い とのことです
ここで言う 明確な失効時刻 とは Expires
や Cache-Control: max-age
のことですね
これらの値が未定義でもクライアント側の判断で、勝手にキャッシュされてしまうことがあるようです
そのため、 キャッシュされたくない場合は、明示的に Cache-Control: max-age=0
を定義する必要があります
まとめ
- キャッシュは、クライアント/サーバ間の通信を減らし、帯域やサーバの負荷を削減してくれる
- 検証 とは、コンテンツの更新がなかったかサーバに問い合わせる行為
- 検証 を行うために
LastModified
とEtag
がある - キャッシュの寿命を定義し、リクエストの回数を減らすために、
Cache-Control
がある (Expires
はもう古い) -
Cache-Control: no-cache
は、 cacheの禁止ではなく、検証の強制 Cache-Control: max-age
が未定義でも勝手にキャッシュされる場合がある- そのため、キャッシュしたくない場合は、
no-cache, must-revalidate, max-age=0
でキャッシュを明示的に禁止する
さいごに
LCLではサーバ負荷を減らし、ユーザ体験を向上させるためエンジニアを募集しています。 ご興味ありましたら以下のリンクよりお気軽にエントリください!