【Fastly】キャッシュ TTL 書き換えの意外な落とし穴
はじめに
Fastly VCL において、キャッシュミス時にオリジンからコンテンツを取得する際、TTL を書き換えたいことがあります。
たとえば HLS 配信を行う際、マニフェストファイル(m3u8
ファイル、HLS 配信の設定や動画セグメントのファイルパスなどが記録される)が頻繁に更新されることから、このファイルのキャッシュを以下のように非常に短く設定する必要があります。
if (req.url.ext ~ "m3u8") {
set beresp.ttl = 1s;
set beresp.grace = 0s;
return (deliver);
}
逆に、静的ファイルに対して非常に長い TTL を設定したい場合も、Fastly VCL で TTL を 1 年間とすることができます。
しかし、筆者が TTL 書き換えの検証をしていたところ、TTL の書き換えがうまくいかなくなるケースに遭遇しました。
この記事では、TTL を書き換える際に注意すべきケースを2つ例示し、それぞれの対策について説明します。
Fastly のキャッシュの仕様については、以下のドキュメントとその配下にかなりのボリュームで解説が載っています。気になる方はぜひご一読ください。
TTL 書き換えがうまくいかなくなるケース
ケース1: Shielding の有効化
VCL サービスにて Shielding という機能を有効化していると、「特定の地域の組み合わせから特定の順番でリクエストが飛ぶと、その2つの地域で異なる TTL が設定される」 という現象が発生します。
その結果、片方でのみキャッシュの有効期限が切れてオリジンに新しいコンテンツを取得しにいくので、2つの地域で異なるコンテンツが取得できてしまうことがあります。
本項では、この現象の原因と対策を説明します。
Shielding とは
fastly Documentation: Shielding https://www.fastly.com/documentation/guides/concepts/shielding/ より引用
Shielding とは、オリジンと通信する VCL サービスのリージョンを1つに限定し(このリージョンを Shielding POPs と呼びます)、残りのリージョン(Edge POPs と呼びます)はその Shielding POPs の VCL サービスからコンテンツを取得することで、オリジンとの通信量を減らしネットワークの効率化を行う機能です。
オリジンにクラウドサービスを選んでいる場合は、オリジンのリージョンに対応した fastly POPs を選択することで、オリジンとの通信コストを安くすることができます(対応状況はこちら。)
オリジンの設定にて有効化するかの選択ができます。
特徴的なこととして、 VCL サービスが edge POPs にあるか shielding POPs にあるかにかかわらず、同一の VCL ファイルによって挙動が記述されることです。そのため、「はじめに」で出したように vcl_fetch
にて記述された TTL 書き換えは、条件分けをしない限りは edge POPs でも shielding POPs でも有効です。
Shielding 有効時の落とし穴
さて、shielding POPs として tyo_tokyo_jp
を選択し、/foo/bar
のフェッチが行われた場合はその TTL を 600s に書き換えるとしましょう。
// この設定はオリジンの設定で shielding を有効化すれば自動で追加される
director ssl_shield_tyo_tokyo_jp shield {
.shield = "tyo-tokyo-jp";
.is_ssl = true;
}
...
sub vcl_recv {
...
// この設定はオリジンの設定で shielding を有効化すれば自動で追加される
if (req.backend == F_sample_backend && var.fastly_req_do_shield) {
set req.backend = fastly.try_select_shield(ssl_shield_tyo_tokyo_jp, F_sample_backend);
}
...
}
sub vcl_fetch {
if (req.url ~ "/foo/var") {
set beresp.ttl = 600s;
set beresp.grace = 0s;
return (deliver);
}
}
...
まだ shielding POPs でも edge POPs でもキャッシュされていない /foo/var
に対して、以下の条件でリクエストが飛んだら何が起こるでしょうか?
リクエスト時刻 | URI | 最初にリクエストを受け取った POPs |
---|---|---|
2024/12/13 17:00:00 | /foo/var |
tyo-tokyo-jp |
2024/12/13 17:02:00 | /foo/var |
osaka-jp |
まず、2024/12/13 17:00:00 にリクエスト受け取った tyo-tokyo-jp
POPs は、自身が Shielding POPs であるため /foo/var
をオリジンに取得しにいきます。その後、vcl_fetch
で TTL が書き換わり、tyo-tokyo-jp
で TTL が 600s としてキャッシュされます。
次に、2024/12/13 17:02:00 にリクエスト受け取った osaka-jp
POPs は、Shiedling POPs である tyo-tokyo-jp
に /foo/var
を取得しに行きます。
tyo-tokyo-jp
では /foo/var
がキャッシュヒットするためそれを返し、osaka-jp
の vcl_fetch
で TTL が書き換わり、osaka-jp
で TTL が 600s としてキャッシュされます。
この時点で tyo-tokyo-jp
における TTL は 480s で、osaka-jp
と異なる TTL になっています。
そこから 480s 経過し、2024/12/13 17:10:00 になりました。すると、osaka-jp
ではまだキャッシュが生きているのに tyo-tokyo-jp
ではキャッシュが期限切れになっています!
すると、osaka-jp
へのアクセスに対しては古いキャッシュを返し、tyo-tokyo-jp
へのアクセスに対しては新しくコンテンツを取得しにいくため、オリジンのコンテンツが更新されているのであれば osaka-jp
と tyo-tokyo-jp
で異なるコンテンツを取得してしまうことになります。
解決方法
この問題は、TTL の設定を固定の 600s とするのではなく、600s から Age ヘッダーの値を引いてあげれば解決します。すなわち以下のとおりです。
...
sub vcl_fetch {
if (req.url ~ "/foo/var") {
set beresp.ttl = 600s;
+ set beresp.ttl -= std.strtol(if(beresp.http.Age, beresp.http.Age, "0"), 10);
set beresp.grace = 0s;
return (deliver);
}
}
...
2024/12/13 17:02:00 の時点で osaka-jp
が取得してきた tyo-tokyo-jp
のレスポンスには、tyo-tokyo-jp
での経過時間として Age=120
のヘッダーが記録されています。これを利用して、TTL を 600s - Age
= 480 とすれば、osaka-jp
と tyo-tokyo-jp
で TTL が同一になり、取得するコンテンツの不一致を防ぐことができます。
ケース2: キャッシュの再検証(revalidation)
VCL サービスでは、オリジンの設定によってはキャッシュの再検証というものがなされます。
このとき、「キャッシュの再検証時にオリジンでコンテンツの更新がなかった場合、オリジンで設定された TTL またはデフォルトの設定が適用されて、 TTL の書き換えができない」 という現象が発生します。
本項では、この現象の原因と対策を説明します。
ETag・Last-Modifed とキャッシュの再検証
キャッシュの再検証とは、キャッシュの期限切れで VCL サービスからオリジンへコンテンツ取得のリクエストを飛ばす際、コンテンツの取得をいきなり行うのではなく、オリジン側でコンテンツの更新があったか否かを判別してもらうための仕組みです。
この仕組みによって、オリジン側でコンテンツの更新がないのであればデータの送信をしないようにすることができるので、オリジンとの通信量を抑えることができます。
オリジンからのレスポンスに ETag
または Last-Modified
というヘッダーが付与されていた場合に有効になります。
それぞれ、以下のような仕組みでキャッシュの再検証を行います。
-
ETag
- オリジンは、コンテンツに対してハッシュ値を管理している。コンテンツが更新されると、ハッシュ値も更新される
- VCL サービスがオリジンにコンテンツ取得リクエストを飛ばすと、対象コンテンツの現在におけるハッシュ値が
ETag
ヘッダーで返される。VCL サービスはETag
の値を保存する - VCL サービスでキャッシュが期限切れした後にリクエストを受け付けると、オリジンに対するコンテンツ取得リクエストで
If-None-Match
ヘッダーを付与する。このヘッダーには、2. で保存したETag
の値を保存する - オリジンは、コンテンツの現在のハッシュ値と
If-None-Match
内の値を比較する。一致するものがあればコンテンツの更新が行われていないので、304 Not Modified を VCL サービスに返す。一致しないのであれば、通常通りコンテンツを返す
-
Last-Modified
- オリジンは、コンテンツの最終更新日時を記録している
- VCL サービスがオリジンにコンテンツ取得リクエストを飛ばすと、対象コンテンツの最終更新日時が
Last-Modified
ヘッダーで返される。VCL サービスはLast-Modified
の値を保存する - VCL サービスでキャッシュが期限切れした後にリクエストを受け付けると、オリジンに対するコンテンツ取得リクエストで
If-Modified-Since
ヘッダーを付与する。このヘッダーには、2. で保存したLast-Modified
の値を保存する - オリジンは、コンテンツの最終更新日時と
If-Modified-Since
を比較する。最終更新日時とIf-Modified-Since
が同じか、最終更新日時の方が古ければコンテンツの更新が行われていないので、304 Not Modified を VCL サービスに返す。最終更新日時の方が新しければ、通常通りコンテンツを返す
キャッシュの再検証による落とし穴
VCL サービス側で ETag
または Last-Modified
の保存がされた場合、キャッシュの再検証が有効になります。
キャッシュの期限切れ後にオリジンに対してキャッシュの再検証が走り、304 Not Modified が帰ってくると、VCL サービスは vcl_fetch
を通らずに、期限切れのキャッシュの TTL を再設定した上でキャッシュからコンテンツを返します。
この再設定される TTL は、オリジンからのレスポンスに含まれるヘッダーを参照しながら、デフォルトの TTL 設定ルール に基づいて計算されます。
たとえばバックエンドに Google Cloud を使用している場合、デフォルトでは Cache-Control: max-age=3600
というヘッダーがつくため、キャッシュの TTL は 3600s に再設定されます。
これによって、キャッシュミス時は TTL を 600s に上書きできたのに、キャッシュ期限切れ以降のアクセスで TTL が 3600s に設定されてしまい、所望の頻度でコンテンツの更新を確認しにいけないということが発生してしまいます。
解決方法
解決方法は2つあります。
-
ETag
・Last-Modified
の保存をしない or 異なるETag
を設定する
この2つのレスポンスヘッダーの保存をしなければ、キャッシュの再検証に必要なリクエストヘッダーIf-None_Match
やIf-Modified-Since
は送られず、キャッシュの再検証は行われません。具体的には、以下のとおりです。vcl_fetchunset beresp.http.etag; unset beresp.http.last-modified;
クライアントと Fastly 間だけキャッシュ再検証を有効化したいのであれば、以下のようにオリジンとは異なる Etag を作成するのも手です。
vcl_fetchset beresp.http.etag = beresp.http.etag "-fastly"; unset beresp.http.last-modified;
-
そもそも TTL の書き換えを行わず、オリジンでちゃんと TTL を指定する
TTL の書き換えの代わりに、オリジンで TTL を設定し、Cache-Control
ヘッダーなどを適切に返すようにすれば、煩雑な設定を回避しながら問題を解決できます。
ただし、オリジンへのコンテンツのアップロード時に適切に TTL を設定する必要があるのと、全ての既存ファイルに対して TTL を再設定しなければいけません。
終わりに
キャッシュ TTL 書き換えは、動的オブジェクトに対して繊細に TTL を指定し、少しでもキャッシュ効率を上げるのに有効な手です。しかし、Shielding やキャッシュ再検証といった他の機能との組み合わせでうまく機能しなくなることがあります。
この記事で説明した解決方法を参考に、自身のサービスの要件と照らし合わせながら、キャッシュ TTL 書き換えをうまく使用していただければと思います。
参考文献
- Shielding | Fastly Documentation
- HTTP caching semantics | Fastly Documentation #Overriding semantics
- Lifetime and revalidation | Fastly Documentation
- ETagとは @OmeletteCurry19 | Qiita
- ETag - HTTP - MDN Web Docs
- If-None-Match - HTTP - MDN Web Docs
- Last-Modified - HTTP - MDN Web Docs
- If-Modified-Since - HTTP - MDN Web Docs
Discussion