はじめに
皆さんはキャッシュを利用しているでしょうか?
キャッシュはコンテンツの取得を高速化するためによく使われますが、正しく理解して使わないと「なぜかコンテンツが更新されない!?」といった罠にハマることがあります。
今回は、特にCDNでのキャッシュ挙動に見事にハマってしまったので、自戒を込めて整理・勉強した内容を記事にまとめました。
キャッシュの設定について
キャッシュの挙動を理解するために、以下のような通信経路を考えます。
- ブラウザがコンテンツを要求する
- 通信がCDNを経由するが、CDNにキャッシュが存在しないためオリジンへ要求が送られる
- オリジンがCDN経由でブラウザにコンテンツを返却する。このとき、オリジンで設定された
Cache-Control
に従ってCDNにキャッシュが保存される - ブラウザにコンテンツが表示され、同じくオリジンで設定された
Cache-Control
に従ってブラウザにもキャッシュが保存される - 以降の通信では、ブラウザまたはCDNに保存されたキャッシュからコンテンツが返却される
キャッシュの挙動は、オリジンからのレスポンスヘッダ Cache-Control
で制御できます。代表的なディレクティブを以下にまとめます。
-
max-age
ブラウザ側でのキャッシュ保持期間を設定します。マネージドなサービスだと ブラウザではなくCDNも設定できることがあります。 -
s-maxage
max-age
の CDN 版です。CDN 側でのキャッシュ保持期間を設定します。 -
no-cache
キャッシュを使用する前にオリジンへアクセスし、コンテンツが更新されていないか確認します。更新されていた場合はキャッシュを更新します。 -
must-revalidate
キャッシュの有効期限が切れた際に、必ずオリジンへアクセスして更新を確認します。 -
proxy-revalidate
must-revalidate
の CDN 版です。 -
no-store
キャッシュを一切保存しない設定です。 -
stale-while-revalidate
キャッシュを再検証している間、古いキャッシュを再利用できる期間を設定します。 -
stale-if-error
オリジンがエラーを返した場合に、古いキャッシュを利用できる期間を設定します。
似ている設定の差分
no-store
vs no-cache
-
no-store
はキャッシュを一切保存しません。 -
no-cache
はキャッシュを保存しますが、使用する際に必ずオリジンへ確認(検証)を行います。
そのため通信経路上の動作には大きな違いはなく、「キャッシュをストレージに残すかどうか」が主な差分になります。
max-age=0
vs no-cache
-
max-age=0
は「キャッシュの有効期限が切れている状態」を意味します。この場合、must-revalidate
やstale-while-revalidate
などの再検証関連ディレクティブが動作します。※仕様上は、そのように区別できますが実際は、no-store
と同様に動く場合もあります。 -
no-cache
は常にオリジンへの検証が発生します。
no-cache
vs must-revalidate
-
no-cache
は常にキャッシュ使用前に検証を行います。 -
must-revalidate
はキャッシュの有効期限が切れた後にのみ検証を行います。
したがって、
no-cache
= max-age=0, must-revalidate
と考えることができます。
max-age=600
vs max-age=600, must-revalidate
どちらも有効期限(600秒)を過ぎると最新のコンテンツを取得しに行きます。
ただし挙動に以下の違いがあります。
-
max-age=600
の場合、オリジンでの検証が失敗すると古いキャッシュを返す可能性があります。 -
max-age=600, must-revalidate
の場合、オリジンでの検証が失敗すると504 Gateway Timeout
を返します。
今回のハマった件
キャッシュ設定を変更した際に、想定外の挙動が発生したため、その検証を行いました。
前提
あるコンテンツ管理システム(CMS)において、「CDNのキャッシュを無効化する」設定を行いました。
ただし、このシステムでは Web 管理画面からしかキャッシュ設定を変更できず、変更可能なディレクティブは以下の4つのみでした。
max-age
s-maxage
stale-while-revalidate
stale-if-error
設定変更内容は以下の通りです。
ヘッダー | 変更前 | 変更後 |
---|---|---|
max-age | 3600 | 0 |
s-maxage | 3600 | 0 |
stale-while-revalidate | 86400 | 86400 |
stale-if-error | 86400 | 86400 |
なお、Web管理画面上では stale-while-revalidate
と stale-if-error
を設定していませんでした。
しかし、実際にはシステム側でデフォルト値(一日)が自動的に設定されており、この仕様に気づかずハマってしまいました。
起きた事象
結論から言うと、後述する AWS CloudFront のキャッシュポリシーをいろいろ変更しても、同じ現象を再現することはできませんでした。(この事象が起きたのはCloudfrontではないCDNです。)
そのため、ここでは実際に発生した事象のみを記録します。
変更手順は以下の通りです。
- CMS 上でキャッシュ設定を無効化する
- コンテンツを更新する
- コンテンツにアクセスする
→ キャッシュ変更前に設定されていたstale-while-revalidate
の影響で、更新前のコンテンツが返却される - もう一度コンテンツにアクセスする
→ 更新後のコンテンツが返却される - しばらく時間を置いて再度アクセスする
→ なぜか更新前のコンテンツが返却される(⁉⁉)
原因考察
おそらく、CDN ごとに s-maxage=0
を指定した際の「古いキャッシュの扱い方」に差があるのだと思われます。
-
s-maxage=0
を指定した時点で、常に古いキャッシュを削除する実装 - 古いキャッシュを削除せず、ただし
s-maxage=0
のため新しいキャッシュを保持しない実装
このように CDN の実装差によって挙動が異なり、その結果として今回の事象が発生したのではないかと考えています。今回発生した事象は後者のような動きをしているのだと思っています。
検証環境
上記の減少の再現を目指して。AWSのCloudfrontとLambda関数を用いて試みました。
検証環境は以下の記事と同様のものを作成しました。またCloudfrontではキャッシュポリシーを無効化し、「Use legacy cache settings」を有効化しています。
https://zenn.dev/devcamp/articles/e9877f79230ef2
変更手順
今回はCloudfrontでの挙動を見たいので、max-age=60
の値を変更していきます。max-age
だけを指定するとCloudfrontのキャッシュの時間になります。
手順
- Lambda関数で "max-age=60, stale-while-revalidate=3600, stale-if-error=3600"にする
- cloudfrontにアクセスして、キャッシュを保存させる
-
max-age=0
にする - キャッシュ保存期間終了後に、何度かcloudfrontにアクセスして、コンテンツを確認する
仕様上だと、stale-while-revalidate
が動作してもおかしくないのですが、max-age=0
にした後はAgeがなくキャッシュが働いていないのがわかります。
結論:s-maxage=0
には注意
基本的に、普段使っている分には何も問題ないと思いますが、CDNによって挙動が違うことを実感しました。
s-maxage=0
だと、CDNによって処理が違うように感じたので、個人的にはリアルタイム性を追求しないのであればs-maxage=1
を設定しておくと、問題ないのかなと思いました。実際、s-maxage=1
だとCloudfrontでもstale-while-revalidate
が動作してくれます。
またマネージドなCDNを使う場合でも、いつでもキャッシュクリアできる方法を準備しておくのも重要です。何かあった場合にとりあえずキャッシュクリアすれば最新版を取得できれば心配を軽減できます。
参考