【Fastly】Segmented caching と仲良くなる記事
はじめに
Fastly VCL には Segmented caching という機能があります。これは、エッジサーバーにおけるキャッシュを一定の大きさごとに分割してキャッシュすることで、Range ヘッダーが付与されているようなリクエストに対してのパフォーマンスが向上する、というものです。
20MB 以上のファイルの取り扱いをするには本機能の有効化が必須になっています。
Segmented caching は、リクエストの入り口に当たる vcl_recv
サブルーチン内で以下の宣言をすることで、有効にできます。
set req.enable_segmented_caching = true
ただ、この機能は設定自体は簡単ですが、挙動がかなり複雑です。たとえば、 vcl_log
でログを1つ出力するように VCL を記述しても、 Segmented caching を有効化していると、取得するリソースのサイズと構成次第では1リクエストに対して数十件のログが発行されることがあります。
本記事では、このようにクセのある Segmented caching の挙動について解説し、挙動の観点から Segmented caching を有効化する際の注意点を挙げます。
挙動の確認には Fastly Fiddle を利用しました。記述した VCL を手軽にテストできて便利なので、VCL の開発をする際は積極的に利用しましょう。
Segmented caching の挙動詳解
この章では、Fastly Fiddle を用いて実験を行ったことで判明した、Segmented caching の挙動を詳解します。
Segmented caching のライフサイクル
RECV において Segmented caching が有効化された場合、VCL Service は HASH にてキャッシュのハッシュ値を計算した後、キャッシュヒット判定を 1MiB ごとに行います。この1MiBごとのブロックをセグメントと呼びます。(今後、便宜のために 0〜1MiB のブロックをセグメント0、...、
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_low
・segmented_caching.rounded_req.range_high
)
など、Inner request の詳細情報を知ることができ、ロギングにおいて便利です。
ただ、segmented_caching
は LOG からのみ参照が可能なことに注意してください。
詳しくは以下のドキュメントを参照してください。
Segmented caching 使用時のベストプラクティス
この章では、Segmented caching の挙動に準じたベストプラクティスを紹介します。
公式ドキュメントに Segment caching を使用する際の制約と考慮事項が書かれていますので、こちらも併せてご参照ください。
適用するリソースの範囲を最小限に絞る
Segmented caching は、キャッシュミスしたリソースに対して 1MiB ごとにオリジンサーバーにリクエストを逐次的に送るため、オリジンサーバーとの通信回数が増えます。コスト増につながるため、むやみやたらと Segmented caching をオンにしない方が良いでしょう。
以下の条件のいずれか満たすリソースの範囲を特定し、Segmented caching の適用は必要最小限に抑えましょう。
- 20MBを超える
- 20MB 超のリソースは、Segmented caching なしで配信しようとするとエラーを吐きます(詳しくは、Fastly ドキュメント:大容量ファイル配信でのフェイルモードを参照してください)
- RECV で設定を行う都合上、リソースの大きさで Segmented caching の適用有無を制御することはできません
- Range による範囲指定をする機会が多い
たとえば、オンデマンド・ライブ問わず動画配信を行う場合は、動画のセグメントファイルが 20MB を大きく超える可能性が高く、基本的には Range ヘッダーを付与して取得します。そのため、動画のセグメントファイルにのみ Segmente caching を適用します。
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 などで発生する情報を取得することができません。
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_passunset 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_req
やsegmented_caching.block_number
の情報を付与して、各 Outer/Inner request の区別がつくようにする
おわりに
本記事では、Fastly における Segmented caching の挙動について詳解し、ベストプラクティスや制限を紹介しました。
セグメントごとのリクエストが直列にしか実行されない・Segmented caching に関する変数が LOG でしか使用できないなどの縛りが多く、正直使いづらさを筆者は感じます。しかし、20MB 以上のファイルの配信、特に動画配信を行うのであれば Segmented caching が必須となるため、避けて通ることはできません。
本記事を読んで、Segmented caching と仲良くなりましょう!
Discussion