🧑‍🤝‍🧑

【Fastly】Segmented caching と仲良くなる記事

2024/11/30に公開

はじめに

Fastly VCL には Segmented caching という機能があります。これは、エッジサーバーにおけるキャッシュを一定の大きさごとに分割してキャッシュすることで、Range ヘッダーが付与されているようなリクエストに対してのパフォーマンスが向上する、というものです。
20MB 以上のファイルの取り扱いをするには本機能の有効化が必須になっています。

Segmented caching は、リクエストの入り口に当たる vcl_recv サブルーチン内で以下の宣言をすることで、有効にできます。

vcl_recv
set req.enable_segmented_caching = true

ただ、この機能は設定自体は簡単ですが、挙動がかなり複雑です。たとえば、 vcl_log でログを1つ出力するように VCL を記述しても、 Segmented caching を有効化していると、取得するリソースのサイズと構成次第では1リクエストに対して数十件のログが発行されることがあります。
本記事では、このようにクセのある Segmented caching の挙動について解説し、挙動の観点から Segmented caching を有効化する際の注意点を挙げます。

挙動の確認には Fastly Fiddle を利用しました。記述した VCL を手軽にテストできて便利なので、VCL の開発をする際は積極的に利用しましょう。
https://fiddle.fastly.dev/

Segmented caching の挙動詳解

この章では、Fastly Fiddle を用いて実験を行ったことで判明した、Segmented caching の挙動を詳解します。

Segmented caching のライフサイクル

RECV において Segmented caching が有効化された場合、VCL Service は HASH にてキャッシュのハッシュ値を計算した後、キャッシュヒット判定を 1MiB ごとに行います。この1MiBごとのブロックをセグメントと呼びます。(今後、便宜のために 0〜1MiB のブロックをセグメント0、...、ii+1MiB のブロックをセグメントi と呼びます)
VCL Service はキャッシュヒット判定時に、各セグメントごとに Inner request という名のサブプロセスを立ち上げ、以降は Inner request がセグメントのデータを取得してくる責務を負います。元々のリクエストは(Outer request)はデータ取得に一切関わらず、Inner request が持ってきたデータを統合するのみです。

各 Inner request においては、まず最初にセグメントキャッシュの存在有無が判定されます。キャッシュが存在すれば HIT、存在しなければ MISS -> FETCH を経てセグメントのデータを取得し、DELIVER に遷移します。
DELIVER 終了後、Inner request によって取得された内容は Outer request のレスポンスボディに書き込まれ、全部のデータがそろったタイミングでクライアントにレスポンスを返す流れになります。

各サブルーチンが Inner request・Outer request のどちらで呼ばれるかを示す表は以下のとおりです。

ライフサイクル Outer request Inner request 備考
RECV
HASH キャッシュ有無判定で使用されるハッシュ値は全 Inner request で共通
HIT HIT時は Inner request の本処理がここで終了
PASS
MISS
FETCH MISS/PASS時は Inner request の本処理がここで終了
DELIVER 実は Outer request では呼び出されない。レスポンスヘッダーやステータスは1つめの Inner request から作成※、ボディは全 Inner request の合成で作成されるようである
ERROR Inner で ERROR に入り DELIVER に遷移したら、それ以降に発行されるはずだった Inner は発行されない
LOG Inner 本処理が終了した後に、Outer は DELIVER 後に発行される

※ DELIVER・ERROR の挙動が合わさると、「2つめ以降の Inner request で 2xx 以外のエラーが発生しリソースの取得が中途半端に終わった場合でも、最終的なレスポンスは 200 になる」という現象を確認しています(Fiddle)。筆者はこの挙動が正しいように思えず、Fastly サポートに問い合わせ中です

Inner request の発行のされかた

Inner request は、byte位置が小さいセグメントから順番に逐次実行されます。すなわち、同時に2つ以上の Inner request が同時に処理されることはありません。
このような仕様となっているのは、オリジンサーバーとの通信が Inner request でしか行われず、取得しようとするリソースの大きさを事前に予測できないためだと筆者は考えています。

Inner request 発行対象となるセグメントの範囲は、「取得対象のバイト範囲を包含するような範囲のうち最小のもの」です。
たとえば、クライアントからのリクエストで Range: bytes=1548576-4604304 と指定された場合、下限の1548576バイト目はセグメント1に、上限の4604304バイト目はセグメント5に含まれているため、対象のセグメント範囲はセグメント1〜5になります。

同じ範囲のリクエストに対して、オリジンのリソースの大きさが2.5MiB程度しかなかった場合は、リソースの最大バイトまでのセグメントのみが Inner request の発行対象となります。

クライアントがRange ヘッダーを指定しない場合、または Range の末尾が指定されていない場合は、HIT時はキャッシュオブジェクトの最後のバイトに到達する / MISS・PASS 時はオリジンサーバーにあるリソースの最後のバイトに到達するまで Inner Request の発行が繰り返されます。
そのため、もしキャッシュされていない 100MiB のファイルを Range ヘッダーなしで取得しようとした場合、セグメント0〜99の各々に対応する Inner request が逐次的に100回発行され、オリジンサーバーとの通信も逐次的に100回行われます

Inner request に関係する変数

挙動とは少し違いますが、Inner request に関係する情報は segmented_caching という変数が管理しています。

  • リクエストが Inner か Outer かのBOOL値(segmented_caching.is_inner_req segmented_caching.is_outer_req
  • 自身が何番目に終了したリクエストかを示す数字(segmented_caching.block_number、0から開始)
  • 各セグメントの下限・上限バイト数(segmented_caching.rounded_req.range_lowsegmented_caching.rounded_req.range_high

など、Inner request の詳細情報を知ることができ、ロギングにおいて便利です。
ただ、segmented_caching は LOG からのみ参照が可能なことに注意してください。

詳しくは以下のドキュメントを参照してください。
https://www.fastly.com/documentation/reference/vcl/variables/segmented-caching/

Segmented caching 使用時のベストプラクティス

この章では、Segmented caching の挙動に準じたベストプラクティスを紹介します。
公式ドキュメントに Segment caching を使用する際の制約と考慮事項が書かれていますので、こちらも併せてご参照ください。

適用するリソースの範囲を最小限に絞る

Segmented caching は、キャッシュミスしたリソースに対して 1MiB ごとにオリジンサーバーにリクエストを逐次的に送るため、オリジンサーバーとの通信回数が増えます。コスト増につながるため、むやみやたらと Segmented caching をオンにしない方が良いでしょう。
以下の条件のいずれか満たすリソースの範囲を特定し、Segmented caching の適用は必要最小限に抑えましょう。

  • 20MBを超える
  • Range による範囲指定をする機会が多い

たとえば、オンデマンド・ライブ問わず動画配信を行う場合は、動画のセグメントファイルが 20MB を大きく超える可能性が高く、基本的には Range ヘッダーを付与して取得します。そのため、動画のセグメントファイルにのみ Segmente caching を適用します。

vcl_recv
if (req.url.ext ~ "ts|m4s") {
    set req.enable_segmented_caching = true
}

サイズが大きいことが予想されるリソースに対しては、クライアント側で範囲指定をしながら取得する

オリジンサーバーとの通信回数が増えることのもう一つのデメリットは、キャッシュミス時にレスポンス応答までの時間が長くなることです。
Range ヘッダーなしに 100MiB のリソースを取得しようとすると100回もオリジンサーバーとの通信が逐次的に発生しうるため、リソースをオリジンサーバーから取得し切る前にタイムアウトしてしまう可能性があります。

タイムアウトを避けるため、クライアント側である程度小分けにしてリソースを取得しましょう。

Outer request・Inner request のライフサイクルを意識したログを作成する

Segmented caching では、LOG が Outer request や各 Inner request において実行されるため、1つのリクエストに対してログが 1+N 個発生し、かなりややこしくなります。
また、Outer request は RECV と HASH しか通らないため、以下のように Outer request のログのみ出力するとしても、FETCH や ERROR などで発生する情報を取得することができません。

vcl_log
if (segmented_caching.is_outer_req) {
    log "syslog " req.service_id " bigquery_endpoint :: " ...
}

そこで、ログの内容に以下のような情報を付与することを検討しましょう。ログ分析がしやすくなります。

  • RECV でリクエストIDを発行し、Outer request と Inner request で共通のリクエストIDを付与する。ログのグルーピングが可能になる
    vcl_recv
    req.http.request_id_base = uuid.version4();
    
    vcl_miss, vcl_pass
    unset bereq.http.request_id
    
    vcl_log
    # bigquery にログをエクスポートする場合
    # 別途 BigQuery エンドポイントの設定が必要
    # https://docs.fastly.com/ja/guides/log-streaming-google-bigquery
    log "syslog " req.service_id " bigquery_endpoint :: "
        {"{"}
            ...
            {""requestId": "} req.http.request_id {", "}
            ...
        {"}"};
    
    • OpenTelemetry データを出力するのも良いかも(統合の仕方を解説してくれている公式ブログ記事はこちら
  • segmented_caching.is_outer_reqsegmented_caching.block_number の情報を付与して、各 Outer/Inner request の区別がつくようにする

おわりに

本記事では、Fastly における Segmented caching の挙動について詳解し、ベストプラクティスや制限を紹介しました。
セグメントごとのリクエストが直列にしか実行されない・Segmented caching に関する変数が LOG でしか使用できないなどの縛りが多く、正直使いづらさを筆者は感じます。しかし、20MB 以上のファイルの配信、特に動画配信を行うのであれば Segmented caching が必須となるため、避けて通ることはできません。
本記事を読んで、Segmented caching と仲良くなりましょう!

Discussion