4月 292021
 

キャッシュなどのクソ分厚い本は書いたのですが、流石にあれだけ分厚いと読んでくれる人も少なそうな気がするので、キャッシュを使う上で知っておきたい基本的なポイントを解説したいと思います。
目的としてはとりあえずこの辺押さえておけば、MDNのキャッシュCache-Controlなどの記事が読みやすくなるんじゃないかなというのと(記事読み終わったらMDN読んでみてほしい)、興味を持ってもらえれば本を手に取ってもらえるかなぁといった感じです(割と本から内容持ってきてる)。

なお、キャッシュといっても様々なものがありますが、この記事ではHTTPにおけるキャッシュをキャッシュとしています。また、同様にProxyはReverseProxyのことを指します。

そもそもキャッシュとは何をどうしようとしているのか

そもそも一口にキャッシュといっても文脈によって異なります。
一般的にわかりやすいところだとブラウザCDNだと思いますが、この両者はキャッシュは行いますが特性はまったく違います

インターネット上の記事、本、雑誌どれでもそうなのですが、その記事が指しているのはブラウザなのか、CDNなのか、はたまた両者なのかを理解したうえで読まないと混乱しますし、キャッシュってクソ難しいよねと感じてしまいます。

ひとつずつ整理していきましょう。

キャッシュの性質と置き場所

キャッシュの性質

キャッシュはいくつかの軸で考える必要があり、その一つに再利用を誰がするのかという性質があります。

  • 特定のクライアントのみが参照可能なキャッシュ(private cache)
  • 複数のクライアントから参照可能なキャッシュ(shared cache)

この2つの性質はきちんと区別して考える必要があり、ごっちゃにすると事故の元となります。
(ちなみにcache-controlのpublic/privateとは全く別物です。それとごっちゃにしないようにしましょう)

例えばAPIのレスポンスをキャッシュするとします。
都道府県のリストを返すようなAPIであればsharedとして問題ないでしょう。
ところが、ログイン後のユーザの情報が含まれるものは複数のクライアントで共有してはいけないのでprivateです。

sharedのものをprivateとして扱うのは範囲が狭まるわけですから問題ないのですが、逆を行うと事故るのが目に見えるでしょう。
多くのキャッシュ事故はここを意図せず・適切なケアをせずに跨いでしまって起きています。

また、private/sharedですが、厳密にこの2種に収束するかというとそうではありません。

若干応用的な考え方になるので簡単にですが、例えばユーザの住んでる地域(都道府県)という属性によってAPIの結果が変わる場合について考えてみましょう。
これは複数のクライアント向けではあるものの、すべてのクライアントが対象ではないということが言えます。
特定の属性(追加のキー)の集合体(クラスタ)向けのキャッシュでprivateとsharedの中間といえるでしょう。
これはキャッシュキーがとかそういう話にはなり、キャッシュを高度に使うための話になるのですが、これについてはCDNとの付き合い方を読んでみてください。
あくまで、ここではprivate/sharedで分かれて、場合によってはその垣根を意図して跨ぐことで高度に使うことができるといったことを知っておけばよいでしょう。

キャッシュの置き場所

先ほどブラウザとCDNを例にあげましたが、Webサイトを見る上でキャッシュはどこにあるのだろうかというと、ローカル・ゲートウェイ・経路上の3か所にあります。

キャッシュの置き場所(ローカル[ブラウザ]・ゲートウェイ[Proxy]・経路上[CDN])

ゲートウェイと経路上というのはあまりキャッシュでは聞きなれないとは思います。

ゲートウェイキャッシュはオリジンとインターネットをつなぐ部分においてあるProxyで行うキャッシュで、
経路上のキャッシュはインターネットの経路上にあるCDNのエッジサーバーで行うキャッシュです。

なお、これらの区分け(ローカル・ゲートウェイ・経路上)はあまり一般的ではなく、定義でも揺れてる感があるんで整理したものです。(参照

また、これらのキャッシュはすべてのサイトで使われているわけではありません。
CDNを未導入のサイトもありますし、ブラウザキャッシュをうまく使えていないサイトも多数あります。
とりあえず、3か所あるということは知っておきましょう。

ローカルキャッシュ(ブラウザキャッシュ)

ローカルキャッシュはいわゆるブラウザキャッシュで非常にわかりやすいものです。

そのクライアントのローカルにあるので、当然他のクライアントから参照することができません。
そのため特定のクライアント向けのprivate cacheと複数のクライアント向けのshared cacheのどちらも格納することが可能です。
複数のクライアント向けだからといって必ず他クライアントに共有しなくてはいけないというものではなく、あくまで性質であるということを覚えておきましょう。

ゲートウェイキャッシュ(Proxy)・経路上のキャッシュ(Proxy/CDN)

ProxyとCDNですが、どちらも複数のクライアントからリクエストを受けます。

そのため、格納できるのはshared cacheのみです。

Proxyもインターネットの経路上にあるわけなので経路上のキャッシュではと感じたかは鋭いです。
そのとおりでProxyも経路上のキャッシュではあるのですが、CDNと一緒にするには性質が異なるため分けています。
ここでは詳しく触れませんが、いったんはProxyとCDNの違いはいくつかあり、両者を併用することでメリットもあるのですが、いったんはsharedを格納する場所と覚えておきましょう。

まとめるとこんな感じです

種類クライアントとの対応格納できる場所
private1:1ローカル
shared1:nローカル・経路上・ゲートウェイ

ローカル・経路上のキャッシュを併用しよう

キャッシュは再利用されるほどいいものです。

サイトの規模にもよるのですが、ローカルと経路上のキャッシュはそれぞれ性質が異なるため、ブラウザキャッシュだけ適切に設定しておけば経路上では不要というわけではありません。


ローカルキャッシュはキャッシュを持つクライアント自身がサイトを再訪する場合は有効ですが、キャッシュを持っていない新規クライアントには無効です。

経路上のキャッシュは新規クライアントに対してもキャッシュを返すことができるため、例えばサイトへの流入が突然増えるといった事態でも対処がしやすいです。

そのためコンテンツ次第ではありますが、ブラウザキャッシュのように特定のクライアントでしか使えないprivate cacheにするよりも、 効率を考えてローカル・経路上のどちらでもキャッシュができ、多数のクライアントで共有できるshared cacheに積極的にすべきでしょう。

ここまでのまとめ

  • 一口にキャッシュといってもその性質でprivateとsharedの2種類がある
  • キャッシュの格納する場所としてローカルと経路上(+ゲートウェイ)がある
    • ローカルにはprivateとsharedが格納できる
    • 経路上にはsharedが格納できる

とりあえずこの辺りを抑えたうえでキャッシュの各記事を読んでみると、CDN(経路上)の話をしているのか、ブラウザ(ローカル)の話をしているのかとわかりやすくなると思います。

Cache-Controlとはなにを行うものか

そろっと新しいRFCが出そうですが、現状のキャッシュの仕様はRFC7234で、そのセクション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.

キャッシュはHTTPのオプション機能ではあるが、キャッシュの再利用は望ましいものであり、それを防止するような要件・設定がない場合は「デフォルトの動作」である。

RFC7234#2

と触れられています。

まず最初に知っておきたいのは条件さえそろえばCache-Controlを指定しなくてもキャッシュされるということです。
条件は様々なものがありますが、重要な条件にTTLが計算できるということがあります。
TTLはCache-Controlのmax-ageで指定するものでは?と思われるかもしれませんが、ヒューリスティックなTTL計算(RFC7234#4.2.2)というのがあります。

Last-Modifiedというコンテンツの最終更新日と現在時刻を表すDateヘッダがありますが、この2つのヘッダを使うことで、ヒューリスティックなTTLの計算を行います。

すごい乱暴に言えば最終更新日から1日経過してるんでまぁ2時間ぐらい(10%程度)キャッシュしてもいいでしょといった感じです。

ヒューリスティックなTTL = (Date – Last-Modified) / 10

つまり、キャッシュされたくないコンテンツはCache-Controlを正しく設定する必要がありますし、キャッシュをするにしても意図した動作を行うように正しく設定する必要があります。

キャッシュを格納する、そして使う

ローカル・経路上のどちらの場合でもキャッシュというものは格納する使うという2つのステージがあります。

例えばTTLの範囲内かといった計算は利用するタイミングで行うわけですから、当たり前といえば当たり前なのですが、案外この辺りの意識が抜けているとことが多いです。

Cache-Controlには様々なディレクティブがありますが、それがどのステージに効くのかというのを考えるとわかりやすいことがあります。

勘違いされやすさで有名なno-cacheとno-storeですが、no-cacheはキャッシュを利用するときのディレクティブでno-storeはキャッシュを格納するときのディレクティブとなります。

no-cacheは利用するときのディレクティブなので、キャッシュは格納されます。で、利用時に再検証を行い最新であることを確認して利用します。

no-storeは格納するときのディレクティブなので、そもそもキャッシュに格納させないわけです。

レスポンスだけではないCache-Control

これはexample.netに対してF5でリロードをしたときのリクエストおよびレスポンスヘッダとなります。

レスポンスヘッダにCache-Control: max-age=604800とある以外にリクエストヘッダにCache-Control: max-age=0とあることに気づきます。(ちなみにctrl+F5だとリクエスト側がno-cacheに変わります)

リクエストを行う際にもCache-Controlがつきますがこれは何を意味しているのでしょうか?

レスポンスヘッダのmax-ageはそのコンテンツのTTLを指定しますが、リクエストヘッダは指定したTTL以上のコンテンツを受け入れる気がない、つまりmax-age=0なので0秒経過したコンテンツは欲しくないので新しいのくださいってことですね。

これだとちょっとわかりづらいと思うので雑に言うとキャッシュに格納されているレスポンス由来のmax-ageをリクエスト由来のmax-ageに一時的に書き換えて評価するといった感じでしょう。

さて、これを踏まえて先ほどのno-cache/no-storeをもう一度考えてみましょう。

no-storeは格納するときの指定なわけですから、リクエスト中に含まれていた場合はキャッシュに格納しないでねといった動きになります。
あまりレスポンスの時の動きと変わらないような気がしますが、既にキャッシュがある状態を考えてみましょう。
no-storeは利用時には評価されないため、キャッシュがレスポンスされます。

とはいえ、CDNやProxyの経路上のキャッシュに対してF5でリロードしてもオリジンに毎回検証してくれるわけではありません。
当然といえば当然なのですが、律儀に全部通していたらあまりCDNの意味がありません。

じゃあどこに効くのかというとブラウザなどのローカルキャッシュです。

ローカルキャッシュ自体をサーバーと考えてそこに対してCache-Controlを通じて問い合わせを行っていると考えるとしっくりくるかもしれません。

なのでxhrでの問い合わせの時にキャッシュされるとか困ってる場合はこの辺りを思い出してみるとよいかと思います。

Cache-Controlの解釈は実装によって異なる

リクエスト・レスポンスに含まれるCache-Controlはあくまで指定でしかなく、その解釈は受け取った側に委ねられています。

例えばno-storeを指定したとしても本当にキャッシュを格納しないのかというと受け取った実装依存となるわけなので、CDNを使う場合はこの辺りもきちんと調べておくとよいでしょう。

ちなみに今まで見た中で一番思い切りがいいなとおもったのはステータス200ならCache-Control一切見ないでX時間キャッシュというものです。こういうのもあります

TTLは経路上とローカルで共通

たとえばsharedキャッシュでmax-age=3600と指定した場合、経路上・ローカルで1時間キャッシュできます。
この1時間はそれぞれではなく合算となります。

たとえば経路上で30分キャッシュ(Age: 1800)したものはローカルでは30分しかキャッシュできません。

案外この辺りを失念しやすいのか、サイトを眺めているとAgeヘッダ > max-ageとなっていて毎回検証リクエストが飛んでるといったことがあります。

先ほどローカル・経路上のキャッシュを併用しと触れましたが、併用すれば当然TTLの割り振りも考える必要があります。

ここまでのまとめ

  • キャッシュは条件を満たしていればCache-Controlを指定しなくてもされる
  • Cache-Controlの解釈は実装によって異なる可能性があるのでCDNを使う場合はきちんと調べよう
  • キャッシュは格納と利用の2ステージがある

最後に

この辺りの話は重要なのですが、案外触れている記事がない(もしくは知っているものとして書かれている)ので、
押さえておくと、様々な記事についても読みやすくなると思います。

また、この辺りの話も含めてめっちゃ詳しく書いてますので、よかったら読んでもらえるとうれしいです。


 Posted by at 9:32 PM

 Leave a Reply

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

(required)

(required)

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください