[アップデート] Amazon CloudFront がオリジン Cache-Control ヘッダーの stale-while-revalidate と stale-if-error をサポートしました

2023.05.18

いわさです。

CloudFront ではオリジンの Cache-Control で個々のオブジェクトに対するキャッシュの挙動を制御することが可能です。
本日のアップデートでstale-while-revalidatestale-if-errorを CloudFront でも利用出来るようになりました。

それぞれの仕様は以下で確認出来ます。

stale-while-revalidateディレクティブは、キャッシュの有効期間が切れた後も一定期間は古いキャッシュを利用することが出来ます。
その間にバックグラウンドで非同期でオリジンレスポンスを取得してキャッシュを最新化することが出来る仕組みです。
これによって、これまではキャッシュが切れたタイミングで発生するオリジンへの同期リクエストを回避することが出来ます。

stale-if-errorディレクティブは、オリジンがエラーステータス(ステータスコード 500, 502, 503, 504)を応答する場合に一定期間は古いキャッシュを使用します。
キャッシュの有効期間が切れて、オリジンへのリクエストが発生した際にエラーになっても、一定期間は古いキャッシュでカバーすることが出来ます。

Lambda + CloudFront で検証してみる

今回次のように関数 URL を有効化した Lambda をオリジンにして、CloudFront を前段に配置する構成で検証してみました。
Lambda でレスポンスヘッダーなどを生成させます。

キャッシュが使われているかわかるように、レスポンスボディはタイムスタンプを使います。
また、同期的にオリジンリクエストが発生しているかわかるように 10 秒間のスリープを設定しています。
オリジンで指定するmax-ageディレクティブは 60 秒です。CloudFront のキャッシュポリシーにも依存しますが、60 秒間 CloudFront 側で保持するように指示しています。

lambda _function.py

import json
import time

def lambda_handler(event, context):
    time.sleep(10)
    return {
        'statusCode': 200,
        'headers': {
            "Content-Type": "application-json",
            "Cache-Control": "max-age=60"
        },
        'body': json.dumps(time.time())
    }

あとは関数 URL をオリジンに CloudFront を構成するだけです。

なお、マネージドポリシーの CachingOptimized は次のような構成になっています。

cURL でアクセスすると次のようになります。
初回は応答まで 11 秒かかりましたが、2 回目以降は同じレスポンスが応答されているのでキャッシュが使われていることがわかりますね。応答時間も 0.3 秒になりました。

% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361140.8902435
11.575475
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361140.8902435
0.349105

また、max-ageが 60 なので上記から約 60 秒経過すると次のように、またオリジンへのリクエストが発生し、約 10 秒応答の生成に必要なタイミングがあります。
それ以降はまた 60 秒間キャッシュが使われる感じです。

% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361140.8902435
0.305020
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361210.9583316
10.611459
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361210.9583316
0.447639

stale-while-revalidate

まずはstale-while-revalidateを使ってみます。
Lambda でCache-Controlに次のようにstale-while-revalidateを 30 で追加します。

lambda _function.py

import json
import time

def lambda_handler(event, context):
    time.sleep(10)
    return {
        'statusCode': 200,
        'headers': {
            "Content-Type": "application-json",
            "Cache-Control": "max-age=60, stale-while-revalidate=30"
        },
        'body': json.dumps(time.time())
    }

初回はオリジンリクエストが発生しています。
数秒ごとにリクエストを送信し続けます。60 秒間はキャッシュが使われていることがわかります。

# 初回
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361313.878341
10.827932

# 60秒後
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361313.878341
0.687723
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361313.878341
0.386813
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361384.4733567
0.302307
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361384.4733567
0.402332

さらに、60 秒経過後も一定期間は古いキャッシュが使用され続けています。
この間は古いキャッシュを使用しつつ、バックグラウンドでオリジンにアクセスしています。
オリジンから新しいレスポンスが取得された後は新しいレスポンスがキャッシュされて使用されていますね。

通常発生する同期的なオリジンリクエストの待機が発生せずに、新しいキャッシュがすぐに使われているように見えます。

なお、この機能は有効期限が切れた後のstale-while-revalidateで設定した間古いキャッシュが使われるというものなので、その期間にリクエストが発生しなければ非同期でのオリジンリクエストも発生せずに、先程の Lambda でいうと 90 秒の間にリクエストが発生していない場合は通常どおりオリジンへの同期的なリクエストが発生し、10 秒間待機することになります。

stale-if-error

続いてstale-if-errorです。
こちらは先程と少し似ているのですが、有効期間が切れた後も、一定期間はオリジンでエラーが発生した場合でも古いキャッシュを使い続けてくれるというものです。

ここでは次のようにstale-if-errorへ 300 を設定しました。

lambda _function.py

import json
import time

def lambda_handler(event, context):
    time.sleep(10)
    return {
        'statusCode': 200,
        'headers': {
            "Content-Type": "application-json",
            "Cache-Control": "max-age=60, stale-if-error=300"
        },
        'body': json.dumps(time.time())
    }

まずは正常な形でキャッシュさせます。

% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361529.9832242
10.708288
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361529.9832242
0.441098

続いて Lambda のステータスコードを 500 に変更します。

lambda _function.py

import json
import time

def lambda_handler(event, context):
    time.sleep(10)
    return {
        'statusCode': 500,
        'headers': {
            "Content-Type": "application-json",
            "Cache-Control": "max-age=60, stale-if-error=300"
        },
        'body': json.dumps(time.time())
    }

1 分後キャッシュが切れたタイミングでアクセスしてみるとどうでしょうか。

% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361529.9832242
0.441098
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361529.9832242
10.881692
% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684361529.9832242
10.412313

期待どおり古いキャッシュが使用され続けていますね。

ただし、オリジンレスポンスを評価して古いキャッシュを使うかどうか判断しているので上記のように都度 10 秒の待機時間が発生しています。
こちらのディレクティブはあくまでもオリジンエラーを一定期間回避するためのもので、先程のようにオリジンの同期待ちを回避するためのものではないのでご注意ください。
2 つのディレクティブを組み合わせる必要があります。

さらに、5 分程度後にアクセスしてみると次のようにエラーレスポンスが取得されました。

% curl https://d1d708qrpnpap.cloudfront.net/ -w '\r\n%{time_total}'
1684362059.602223
10.550493

さいごに

本日は Amazon CloudFront がオリジン Cache-Control ヘッダーの stale-while-revalidate と stale-if-error をサポートしたので試してみました。

CloudFront のキャッシュは強力ですが、キャッシュ有効期間の切れ目って気になりますよね。
その切れ目の期間にアクセスが集中した場合は Request Collapsing という CloudFront の仕様でオリジンへの負荷がかからないようになっていたりします。

ただ、その場合でもオリジンがレスポンスを生成するまでの待機時間は発生するので、今回サポートされた Cache-Control ディレクティブをうまく使うことでクライアント側の操作性を向上出来る気がします。