LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログです。

HTTP キャッシュおさらい

f:id:kasei_san:20190301174624p:plain

こんにちは。バックエンドエンジニアの id:kasei-san です

最近は映画「リベリオン」を見たいのですが、配信しているサービスがTSUTAYAとU-NEXTしかなく、加入すべきか悩んでいます

doga.hikakujoho.com

本日はHTTP キャッシュについて解説します

経緯

LCLではキャッシュサーバにVarnishを使用していますが、現在Fastlyへの移行を検討中です

techblog.lclco.com

移行についての作業を任されたため、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では キャッシュの有効期限 も設定することが可能です

キャッシュの有効期限のあいだは、検証は行わず、クライアント内にあるキャッシュに保存されているコンテンツを表示します

そのため、サーバへの通信は一切発生せず、クライアント/サーバ双方の負荷はさらに低減されます

 

以降では、上記の具体的なしくみを解説いたします

LastModifiedEtag

LastModifiedEtag は、コンテンツの更新の可否をチェックするためのものです

それぞれがレスポンスヘッダの値で、「最終更新日時」と「コンテンツの識別子」を表します

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 と、コンテンツ本体を返します

これにより、更新が無い限り、コンテンツの生成と通信のコストは発生しなくなります

LastModifiedEtag はどちらを使うべきなの?

RFC2616の13.3.4 によると両方入れることが推奨されています

HTTP/1.1 オリジンサーバにとってより望まれる動作とは強いエンティティタグと Last-Modified 値の両方を送る事である

これは(恐らくですが)どちらかしかサポートしていないブラウザが過去にあったためと思われます

ExpiresCache-Control

ExpiresCache-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の挙動はこれに近い印象ですね

techblog.lclco.com

なお、stale-while-revalidate は、現在ブラウザに実装されてませんが、fastlyでは後述の stale-if-error とともに実装されています

www.fastly.com

Cache-Control: stale-if-error=

検証が失敗した場合に、キャッシュの使用を許可する秒数を表します

cacheの寿命が切れた後、オリジンサーバが死んでいた場合に、ここで設定された時間の間だけはコンテンツの表示が可能となります

頼もしい機能ですね

Cache-Control: max-age が未定義でも、ブラウザが勝手にキャッシュすることがある

RFC2374 によると、

サーバが明確な失効時刻を返さない場合、ブラウザは LastModified などを頼りに経験的な失効時刻を決定して良い とのことです

ここで言う 明確な失効時刻 とは ExpiresCache-Control: max-age のことですね

これらの値が未定義でもクライアント側の判断で、勝手にキャッシュされてしまうことがあるようです

そのため、 キャッシュされたくない場合は、明示的に Cache-Control: max-age=0 を定義する必要があります

まとめ

  • キャッシュは、クライアント/サーバ間の通信を減らし、帯域やサーバの負荷を削減してくれる
  • 検証 とは、コンテンツの更新がなかったかサーバに問い合わせる行為
  • 検証 を行うために LastModifiedEtag がある
  • キャッシュの寿命を定義し、リクエストの回数を減らすために、Cache-Control がある ( Expires はもう古い)
  • Cache-Control: no-cache は、 cacheの禁止ではなく、検証の強制
  • Cache-Control: max-age が未定義でも勝手にキャッシュされる場合がある
  • そのため、キャッシュしたくない場合は、no-cache, must-revalidate, max-age=0 でキャッシュを明示的に禁止する

さいごに

LCLではサーバ負荷を減らし、ユーザ体験を向上させるためエンジニアを募集しています。 ご興味ありましたら以下のリンクよりお気軽にエントリください!

www.lclco.com