続・HTTPキャッシュを使いこなして、Webアプリを快適に(2)
2023年05月25日 木曜日
CONTENTS
「続・HTTPキャッシュを使いこなして、Webアプリを快適に(1)」の後半です。
まだ読んでない方は、そちらを先に読んでください。
「HTTPキャッシュを使いこなして、Webアプリを快適に」の続編でもあります。
まだ読んでない方は、そちらも先に読んでください。
cats_dogs開発者のヒラマツです。
今回は、Cache-Controlヘッダーの具体的な使い方の話になります。
ディレクティブを組み合わせる
前回説明で出てきたフローチャートの、A判断、B判断、C判断を考慮して選んだ後の、ディレクティブの組み合わせ方についてです。
Cache-Controlヘッダーの具体例を紹介して行くので、見比べやすいように、フローチャートの図にも再登場してもらいます。

基本的には、A判断、B判断、C判断の、3つの基準で必要なディレクティブを選んで、組み合わせれば、Cache-Controlヘッダーは作れます。
ただ、以下の点を意識すると、よりシンプルなCache-Controlヘッダーにできます。
no-storeを使うと、A判断とC判断の指定をまとめられ、さらに、B判断の指定も省略できる。- A判断のために
no-cacheを使うと、B判断の指定が省略できる。 Expiresヘッダーを使わない。(動作が複雑になり、難しくなります)
説明だけでは分かりにくいので、いくつか具体例を紹介して行きます。
例1: 認証情報を表示するページを保護したい
先のフローチャートにやりたいことを矢印で追記すると、以下のようになります。

このやりたいことについて、判断ごとの条件を書き出すと、以下のようになります。
- A判断 – 常に最新のページを表示したい。(
no-cacheまたは、no-storeが必要) - B判断 – 短時間でも検証しない状況は許さない。(指定するなら
max-age=0ですが、常に検証するので使われない) - C判断 – 認証情報が含まれるので、通信失敗時は表示させたくない。(
no-storeまたは、must-revalidateが必要)
no-storeを使うとA判断とC判断を同時に指定でき、B判断の指定が省略できるので、Cache-Controlヘッダーは、以下のような指定になります。
Cache-Control: no-store
例2: 単に情報をリアルタイムに更新したい
先のフローチャートにやりたいことを矢印で追記すると、以下のようになります。

このやりたいことについて、判断ごとの条件を書き出すと、以下のようになります。
- A判断 – プログラムがリアルタイム生成する最新のページを表示したい。(
no-cacheまたは、no-storeが必要) - B判断 – 頻繁に検証させたい。(指定するなら
max-age=0だが、A判断の結果、使われない) - C判断 – 通信失敗時は前回のデータで表示した方が良い。(
no-store、must-revalidate、proxy-revalidateの利用は避ける)
no-storeは、C判断が理由で使えないので、Cache-Controlヘッダーは、以下のようになります。
Cache-Control: no-cache
ちなみに、以下でもほぼ同じ効果です。(ブラウザ内部で、A判断とB判断のどちらで条件分岐するかの違いだけ)
Cache-Control: max-age=0
例3: 認証無しの共有ページなので、接続数を減らしつつ、変更の反映はすみやかにしたい
先のフローチャートにやりたいことを矢印で追記すると、以下のようになります。

このやりたいことについて、判断ごとの条件を書き出すと、以下のようになります。
- A判断 – キャッシュを多少は使って接続数を減らしたいほしい。(
no-cacheおよびno-storeは使えない) - B判断 – リアルタイムで反映される必要はないので、反映まで1分ぐらいは待てる。(キャッシュ期間は短めにする必要があるので、仮に30秒で
max-age=30) - C判断 – 通信失敗時は前回のデータで表示した方が良い。(
no-store、must-revalidate、proxy-revalidateはどれも使わない)
B判断の指定だけになるので、案外シンプルで、Cache-Controlヘッダーは、以下のようになります。
Cache-Control: max-age=30
例4: 顧客情報を扱うページだけど、サーバは非力なので更新頻度は低いからキャッシュしたい
先のフローチャートにやりたいことを矢印で追記すると、以下のようになります。 例なので、意図的に、ブラウザとプロキシで扱いが変わるようにしてあります。

このやりたいことについて、判断ごとの条件を書き出すと、以下のようになります。
- A判断 – ブラウザのキャッシュを再利用してほしい。プロキシでは、キャッシュを再利用してほしくない。(
private一択。no-cacheおよびno-storeは使えない。) - B判断 – 反映の遅れが許されるので、キャッシュ期間を長くできる。(仮に300秒で、
max-age=300) - C判断 – 認証失敗で表示されたくない。(
no-store、must-revalidateのどちらかが必要)
A判断の条件が理由で、C判断にもno-storeは使えないので、must-revalidate一択になりますから、Cache-Controlヘッダーは、以下のようになります。
Cache-Control: max-age=300, private, must-revalidate
4つの例だけですが、3つの判断でわけて考えるCache-Controlヘッダーの作り方が、イメージ出来たのではないでしょうか。
WebサーバとWebアプリ
Webサーバのプロキシ機能でWebアプリを動かすと、プロトコルの処理やキャッシュ機能をWebサーバが代行してくれるので、楽ができます。

Webサーバのキャッシュ機能まで使えば、以下のようなメリットがあるわけです。
- HTTPの各種プロトコルの違いは、Webサーバが代行して何とかしてくれる。
- TLSの暗号化とかも、Webサーバが代行してくれる。
- Webサーバのキャッシュ機能が使えれば、キャッシュ処理も任せられる。
- Webサーバのキャッシュが再利用されると、Webサーバだけで処理が完結し、かなり高速化する。
雑に言うと、Webサーバのプロキシ機能のキャッシュまで使いこなせば、ローカルなCDN(コンテンツデリバリネットワーク)的に機能するわけです。
そして、Webサーバがプロキシとして動いて、キャッシュを作っているわけですから、もちろん、キャッシュ制御が関係してきます。
というわけで、WebアプリでWebサーバのキャッシュ制御をする時の話です。
Cache-Controlが共有されてしまう
Webサーバのプロキシ機能を利用すると、Webアプリは楽できるのですが、ハマりどころもそこそこあります。
その1つが、ブラウザだけでなく、Webサーバ、プロキシを含めた全てのシステムで、Cache-Controlヘッダーを共有してしまうことです。
この問題が起きるのは、以下の図のように、プロキシの処理がWebサーバとブラウザの両方の機能を持っている為です。

図にも書いてありますが、ブラウザ的な動作をするプログラムが、Webサーバ、プロキシ、ブラウザの3種類もあります。
そのため、以下の図のように、同じCache-Controlヘッダーが複数のシステムに使い回されてしまうのです。

ということは、例えば、ブラウザだけにCache-Controlヘッダーを渡したつもりでも、Webサーバやプロキシにも、影響を与えてしまいます。
この問題の解決策の1つは、どこでも、だれでも、正しく動作するCache-ControlヘッダーをWebアプリが設定することです。 でも、この方法は、以下のようになかなか難しいのです。
- 1つの
Cache-Controlヘッダーで、全ての関係システムを考慮するのは難しい。 - 難しい
Cache-Controlヘッダーが書けたとしても、他人には読み解くのが難しく、メンテナンスも難しい。
その上、プロキシは多段にできます。 例えば、CDNは高機能なプロキシと見ることが出来るので、プロキシが多段になっていることは普通にあります。

この様な多段な状況でも、問題を起こさないようにCache-Controlヘッダーを設定するのは大変そうです。 ですので、せめて、自分の管理の手が届きそうなWebサーバはこの影響から外したいところです。
それが、もう一つの解決解決策の、WebアプリとWebサーバで別のCache-Controlヘッダーを指定する方法です。具体的な方法は以下の通りです。
- ブラウザ向けの
Cache-Controlヘッダーは、Webサーバで設定する。- nginxの設定例:
add_header Cache-Control "お好きなディレクティブ";
- nginxの設定例:
- Webサーバ用の
Cache-Controlヘッダーは、Webアプリに出力させる。 - Webアプリの出力した
Cache-Controlヘッダーは、Webサーバで消す。(Webサーバしか見ない。)- nginxの設定例:
proxy_hide_header Cache-Control;
- nginxの設定例:
要は、WebサーバでCache-Controlヘッダーを付け直すだけです。

この方法なら、個々のCache-Controlヘッダーの記述がシンプルになり、Webサーバやブラウザが誤動作する心配も無くなります。(管理外のプロキシやCDNについては、その管理者にご相談ください…)
nginxでキャッシュ使うときのTIPS
nginxは、Webアプリの機能の一部を担うことが考慮されているので、cats_dogsでも便利に使っています。
そのcats_dogsを作っているときに気がついた、nginx経由でWebアプリを使うときの、キャッシュ関連のTIPSをざっと紹介しておきます。
- 何も設定しないと(デフォルトでは)、キャッシュを再利用しません。
- proxy_cache_pathとproxy_cacheの設定が必要です。
- 勝手にキャッシュが再利用されると危険なので、デフォルトで無効なのは妥当ではある。
- HEAD対応しなくていい。
- HEADメソッドをGETメソッドに変換してくれます。
- proxy_cache_convert_headで変換を無効にも出来ます。
- Webアプリ側のHEADメソッドの実装を省略できる。
- 設定しないと、キャッシュ検証の動作をしません。
- proxy_cache_revalidateの設定が必要です。
- Webアプリ側も検証に対応してないと、効果はありません。
- 検証をうまく実装できれば、Webアプリの処理をかなりオフロードしてくれます。
- キャッシュをユーザ間で共有します。(情報漏えいに繋がる可能性があります。)
- 対策0: キャッシュを無効にする(有効にしない)。
- ページのURLが規則的であれば、nginxの設定で部分的に無効にすることも出来ます。
- ブラウザのキャッシュは活用できるので、性能があまり必要ないなら設定しないのも有り。
- 対策1: ユーザごとにキャッシュを分離する。
- proxy_cache_keyでアカウント間で共有されないキャッシュのキーを設定する。
- 例:
proxy_cache_key "$scheme$proxy_host$request_uri $remote_user"; - もちろん、キャッシュの効率は下がります。
- 際どい設定なので、キャッシュがユーザごとに分かれるか十分にテストしてください。
- 対策2: Webアプリが内容に合わせて
Cache-Controlヘッダーを動的に変える。- もちろん、Webアプリの実装が複雑になります。
- キャッシュされたくないページだけ、
no-storeに変えたり、privateディレクティブを追加するぐらいは、可能そうです。
- 対策0: キャッシュを無効にする(有効にしない)。
no-cacheの答え合わせ
前編「HTTPキャッシュを使いこなして、Webアプリを快適に」の始めに書いた、
Cache-Control: no-cacheを設定しても、キャッシュは禁止されない。
という話の、答え合わせみたいなことをしておきます。(クイズではないので、明確な正解とかはありませんが…)
すでに説明した通り、no-cacheディレクティブのみの指定では、通信失敗時に、キャッシュのデータを利用されてしまいます。
これは、通信失敗時だけなのであまり問題無いように思うかもしれませんが、以下のように、簡単に意図的に起こせます。
- 認証を失敗させる。(パスワードを知らない人でも実現できる。)
- ブラウザの通信をタイムアウトさせる。(サーバが重いだけで、普通に起こります。ネットワークケーブルを抜いても、出来ますね。)
「第三者が意図的に起こせる」点が問題で、no-cacheの指定では、悪意がある第三者が、狙ってキャッシュのデータを表示できるわけです。
要は、他人からブラウザを触れる状況ができれば、情報漏えいが可能になります。

そして、この設定の悲しいところが、意図的を通信失敗させないと、テストではうまく動いているように見えるところです。
と言うわけで、「キャッシュを禁止したい」ぐらいに、ブラウザに勝手にキャッシュを再利用して表示してほしくない用途では、以下のように、no-storeを指定するのが、現実解です。
Cache-Control: no-store
「ヤバいページはno-store!」、そもそも他人にブラウザは触らせない!、ということで、Cache-Controlヘッダーは、注意して使ってください。
おわり
HTTPキャッシュについて、基本的な用語の説明からnginxでのTIPSまで、長々と解説しました。
最後まで、読んでいただきありがとうございます。
HTTPキャッシュをうまく活用すると、Webアプリは劇的に速くなります。 注意して設定すれば、より安全になります。
ぜひうまく活用してください。お疲れさまでした。