🪤

【Fastly】キャッシュ TTL 書き換えの意外な落とし穴

2024/12/13に公開

はじめに

Fastly VCL において、キャッシュミス時にオリジンからコンテンツを取得する際、TTL を書き換えたいことがあります。
たとえば HLS 配信を行う際、マニフェストファイル(m3u8 ファイル、HLS 配信の設定や動画セグメントのファイルパスなどが記録される)が頻繁に更新されることから、このファイルのキャッシュを以下のように非常に短く設定する必要があります。

vcl_fetch
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 のキャッシュの仕様については、以下のドキュメントとその配下にかなりのボリュームで解説が載っています。気になる方はぜひご一読ください。
https://www.fastly.com/documentation/guides/concepts/edge-state/cache/

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 に書き換えるとしましょう。

main.vcl
// この設定はオリジンの設定で 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-jpvcl_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-jptyo-tokyo-jp で異なるコンテンツを取得してしまうことになります。

解決方法

この問題は、TTL の設定を固定の 600s とするのではなく、600s から Age ヘッダーの値を引いてあげれば解決します。すなわち以下のとおりです。

main.vcl
...
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-jptyo-tokyo-jp で TTL が同一になり、取得するコンテンツの不一致を防ぐことができます。

ケース2: キャッシュの再検証(revalidation)

VCL サービスでは、オリジンの設定によってはキャッシュの再検証というものがなされます。
このとき、「キャッシュの再検証時にオリジンでコンテンツの更新がなかった場合、オリジンで設定された TTL またはデフォルトの設定が適用されて、 TTL の書き換えができない」 という現象が発生します。
本項では、この現象の原因と対策を説明します。

ETag・Last-Modifed とキャッシュの再検証

キャッシュの再検証とは、キャッシュの期限切れで VCL サービスからオリジンへコンテンツ取得のリクエストを飛ばす際、コンテンツの取得をいきなり行うのではなく、オリジン側でコンテンツの更新があったか否かを判別してもらうための仕組みです。
この仕組みによって、オリジン側でコンテンツの更新がないのであればデータの送信をしないようにすることができるので、オリジンとの通信量を抑えることができます。

オリジンからのレスポンスに ETag または Last-Modified というヘッダーが付与されていた場合に有効になります。
それぞれ、以下のような仕組みでキャッシュの再検証を行います。

  • ETag
    1. オリジンは、コンテンツに対してハッシュ値を管理している。コンテンツが更新されると、ハッシュ値も更新される
    2. VCL サービスがオリジンにコンテンツ取得リクエストを飛ばすと、対象コンテンツの現在におけるハッシュ値が ETag ヘッダーで返される。VCL サービスは ETag の値を保存する
    3. VCL サービスでキャッシュが期限切れした後にリクエストを受け付けると、オリジンに対するコンテンツ取得リクエストで If-None-Match ヘッダーを付与する。このヘッダーには、2. で保存した ETag の値を保存する
    4. オリジンは、コンテンツの現在のハッシュ値と If-None-Match 内の値を比較する。一致するものがあればコンテンツの更新が行われていないので、304 Not Modified を VCL サービスに返す。一致しないのであれば、通常通りコンテンツを返す
  • Last-Modified
    1. オリジンは、コンテンツの最終更新日時を記録している
    2. VCL サービスがオリジンにコンテンツ取得リクエストを飛ばすと、対象コンテンツの最終更新日時が Last-Modified ヘッダーで返される。VCL サービスは Last-Modified の値を保存する
    3. VCL サービスでキャッシュが期限切れした後にリクエストを受け付けると、オリジンに対するコンテンツ取得リクエストで If-Modified-Since ヘッダーを付与する。このヘッダーには、2. で保存した Last-Modified の値を保存する
    4. オリジンは、コンテンツの最終更新日時と 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つあります。

  • ETagLast-Modified の保存をしない or 異なる ETag を設定する
    この2つのレスポンスヘッダーの保存をしなければ、キャッシュの再検証に必要なリクエストヘッダー If-None_MatchIf-Modified-Since は送られず、キャッシュの再検証は行われません。具体的には、以下のとおりです。

    vcl_fetch
    unset beresp.http.etag;
    unset beresp.http.last-modified;
    

    クライアントと Fastly 間だけキャッシュ再検証を有効化したいのであれば、以下のようにオリジンとは異なる Etag を作成するのも手です。

    vcl_fetch
    set beresp.http.etag = beresp.http.etag "-fastly";
    unset beresp.http.last-modified;
    
  • そもそも TTL の書き換えを行わず、オリジンでちゃんと TTL を指定する
    TTL の書き換えの代わりに、オリジンで TTL を設定し、 Cache-Control ヘッダーなどを適切に返すようにすれば、煩雑な設定を回避しながら問題を解決できます。
    ただし、オリジンへのコンテンツのアップロード時に適切に TTL を設定する必要があるのと、全ての既存ファイルに対して TTL を再設定しなければいけません。

終わりに

キャッシュ TTL 書き換えは、動的オブジェクトに対して繊細に TTL を指定し、少しでもキャッシュ効率を上げるのに有効な手です。しかし、Shielding やキャッシュ再検証といった他の機能との組み合わせでうまく機能しなくなることがあります。
この記事で説明した解決方法を参考に、自身のサービスの要件と照らし合わせながら、キャッシュ TTL 書き換えをうまく使用していただければと思います。

参考文献

Discussion