CDN での HTTP フレッシュネスのテスト

CDN はすべて HTTP キャッシングを使用してパフォーマンスを最適化しています。ただし、さまざまな CDN でこれを行う方法がわずかに異なる場合があり、お客様にとって状況が複雑になります。このブログ記事では、CDN の相互運用性について説明し、今後に備えて CDN の違いを識別するために役立つ共通のテストスイートを紹介します。


フレッシュネスをテストする理由


RFC7234 によって、「フレッシュネス」は HTTP キャッシングの中心的な概念の1つと示されています。フレッシュなレスポンスを使用できる場合には、オリジンサーバーにアクセスする必要がなく効率が向上します。これは、実質的には、パブリッシャーとすべてのダウンストリーム HTTP キャッシュの間での、「この期間であればパブリッシャーに確認せずにパブリッシャーのコンテンツを提供できる」という約束です。


長年のうちに、CDN とリバースプロキシは HTTP のフレッシュネスに関するコントロールの一部 (リクエストの Cache-Control など) を無視するように進化し、Surrogate-Control などいくつか新しいコントロールを追加しました。多くの場合、これは現時点で賢明なやり方です。CDN とリバースプロキシはオリジンサーバーと特別な関係があり、それを利用してサービスを最適化しているためです (Poul-Henning Kamp の言葉では「Varnish」の追加)。


残念ながら、各 CDN がこれを行う方法が異なり、互換性がないこともあるという話を耳にします。これはお客様にとってよいことではありません。CDN とリバースプロキシが予想外に HTTP キャッシングのルールに違反すると、好ましくない問題が発生して驚かされる可能性があります。


たとえば、パーソナライズされたコンテンツがキャッシュされないようにするために、Cache-Control: private を利用しているサードパーティ製サーバーフレームワークを使用していると想像してください。これに従わない CDN を間に入れると、悲惨な結果がもたらされる可能性があります。あるユーザーのみを対象にしたコンテンツが別のユーザーに表示されます。フレームワークでは HTTP キャッシングが正常に機能していると (正しく) 想定しているためです。


同時に、HTTP は5年前に比べて、はるかに想像力豊かできめ細かい方法で使用されています。これは特に HTTP ベースの API で顕著です。その結果、プロトコルの関連機能すべてを信頼して使用できるように、CDN とリバースプロキシは仕様に合った予測可能な方法で動作することが期待されています。


__CDN が HTTP キャッシングの詳細レベルをどのように処理するかという共通プロファイル__が存在し、「通常」動作との違いに関する詳しいドキュメントもあることが理想です。これで、CDN の切り替えに伴う摩擦やリスクが軽減されるはずです。また、サードパーティ製ソフトウェアは、増え続ける CDN と統合しようとするのではなく、1つを対象にすることができます。


ただし、そのようなプロファイルの定義は簡単ではありません。たとえ、仕様や他の CDN との互換性がないとしても、現在の CDN の個々の動作が異なっているのには正当な理由があるためです。また、すべての CDN (Fastly を含む) は、既存のお客様を驚かせるような動作方法の変更を望んでいません。


私たちにできるのは、CDN (またはリバースプロキシ) がどのように動作すべきかという考えをまとめて、問題がある箇所を特定できるように、データを収集することです。つまり、__CDN やリバースプロキシのためのテストスイート__を用意して、彼らやそのお客様が動作の様子を理解できるようにする必要があるということです。これは、いずれ動作をどこに収束するのが合理的かという議論を進める助けとなるはずです。


私はしばらく前に Web ブラウザでの HTTP キャッシングのための一連のテストを作成し、これらが Web プラットフォームテスト (W3C のブラウザ向け共通テストスイート) に含まれています。これがパブリック CDN テストスイートのベースとして適切であることがわかりました。


現在、このテストでは、仕様どおりの普通の HTTP と厳密に一致しているかどうかが確認されます。前述したように、CDN が仕様から離れたのには正当な理由がある場合もあり、「正しい」動作が何かということについて共通の合意はまだありません。したがって、適合テストとして扱われるべきではありません。つまり、全問合格しても賞品はないのです。


しかしながら、このテストは優れたツールであり、CDN がどのように動作しているかを把握するだけでなく、コンテンツがどのように処理されているか、またパフォーマンスを向上させるためにコンテンツをどのように調整できるかについて洞察を得ることができます。


Fastly結果


では、現在の Fastly の状況はどうでしょうか。数字でいえば、○69、✗36です。実際のところかなりいい結果です。これは、以下の詳細を読んで、厳密な HTTP 準拠と離れた理由を見るとわかります。


どうしてすべてパス (○) ではないのでしょうか。多くの CDN と同様に、Fastly もオリジンサーバーとの「特別な」関係があります。Fastly は可能であれば常にキャッシングを促しています。また、キャッシングメタデータを上書きする外部情報 (VCL などで) が得られることがよくあります。HTTP キャッシングが設計されたのは、CDN が考案されるよりも前であり、汎用的なフォワードプロキシキャッシュでは意味をなすことが、CDN の場合には意味をなさないことがあります。


また、Fastly は Varnish に基づいているという歴史があります。既存のお客様は Varnish および/または Fastly サービスの動作に依存しているため、場合によっては動作の変更によってお客様の迷惑になることがあります。


Fastly における大きな違いは、半ば公然の秘密兵器 VCL です。必要であれば、VCL を使用することで Fastly をほぼすべてでパス (○) させることができます (105のうち103)。HTTP とさらに準拠した動作が必要な場合は、以下にリンクしたスニペットをアカウントに追加してアクティブ化するだけです。5秒後には有効になります。


そうすることがサイトにとって適切な場合もあれば適切でない場合もあります。続きを読むと理由がわかります。


明示的なフレッシュネス : Expires および Cache-Control: max-age


ほとんどの人は ExpiresCache-Control: max-age をよく知っています。これらは HTTP レスポンスでフレッシュネス有効期間を宣言するための基本的な方法です。Expires、レスポンス Cache-Control: max-age (および 's-maxage)、それぞれの相対的な優先度、経過時間の計算などについて、Fastly は関連するテストほぼすべてにパスしています。これらのヘッダーを無視するのは、Surrogate-Control (以下参照) を設定するか、VCL で明示的に上書きする場合のみです。


唯一の例外は、レスポンスが Expires のみを使用してフレッシュネス有効期間を設定したときに、クロック同期の重大な問題があるアップストリームキャッシュが存在している場合です (たとえば、Fastly とオリジンの間にリバースプロキシがあるとき)。この問題の詳細については、以下の「Date、Age および Expires」を参照してください。


ヒューリスティックフレッシュネス


レスポンスで明示的なフレッシュネスが割り当てられなくても、特定の状況では HTTP キャッシュが独自のヒューリスティックフレッシュネス有効期間を計算できます。多くのコンテンツではフレッシュネスが明示的に設定されないためです。ただし、問題が発生しないようにするため、ヒューリスティックフレッシュネスが許可されるのは、特定のステータスコードの場合のみ、かつ明示的な情報が__まったく__ない場合のみです。


Fastly では、VCL やコントロールパネルで ttl パラメーターによって、使用するヒューリスティックをコントロールすることができます。また、テストによって、__許可されていないすべてのレスポンスステータスコードでのヒューリスティックフレッシュネスの使用を適切に回避している__ことが示されます。


しかし、多くのブラウザや他の中間キャッシュと同様に、Fastly はヒューリスティックフレッシュネスについて非常に保守的です。テストによって、__HTTP で許可されるすべてのステータスコードで使用しているわけではない__ことがわかります。これには、204 (No Content)405 (Method Not Allowed)414 (URI Too Long)501 (Not Implemented)、および Cache-Control: public を含むすべてのレスポンスが含まれます。明示的なフレッシュネス情報がない場合、これらのレスポンスはキャッシュに格納されません。


それらのレスポンスもキャッシュしたい場合には、次の VCL を vcl_fetch で使用してください。


# This sets a heuristic TTL of 1 hour if Last-Modified is present.
if (
( ! beresp.http.Cache-Control:max-age
&& !beresp.http.Cache-Control:s-maxage
&& !beresp.http.Expires
&& time.is_after(now,std.time(beresp.http.Last-Modified, now))
) && (
http_status_matches(beresp.status, "200,203,204,404,405,410,414,501")
|| beresp.http.Cache-Control:public
)
)
{
set beresp.ttl = 3600s;
set beresp.cacheable = true;
}

privateno-cache および no-store


他の Cache-Control レスポンスディレクティブを使用して、さまざまな方法でキャッシュを制限できます。


private ディレクティブでは Fastly のような「共有」キャッシュへのキャッシングが禁止されます。また、Fastly が Cache-Control: private のレスポンスのキャッシングを適切に回避していることがテストでわかります (ドキュメントを参照)。


no-cache レスポンスディレクティブと no-store レスポンスディレクティブはよく混同されます。no-cache では、キャッシングで格納することは許可されますが、オリジンサーバーに確認 (If-None-Match 検証の使用など) せずに再利用することができません。no-store ではキャッシング自体が禁止されます。事実上、レスポンスはキャッシングシステムを迂回する必要があります。


Varnish に基づいているため Fastly は 'no-cache' と 'no-store' の両方をデフォルトで無視し、レスポンスを格納して、オリジンサーバーに確認せずに提供します。このため、__該当するテストに失敗__します。


ただし、これらを vcl_fetch でサポートすることはできます。


if (beresp.http.Cache-Control:no-store) { return (pass); }
if (beresp.http.Cache-Control:no-cache) {
set beresp.ttl = -1s;
set beresp.grace = 0s;
return (deliver);
}

警告 : ここでの no-cache サポートは仕様に正しく従っていますが、これらのリクエストを実質的にシリアライズしているため、効率が大幅に下がることを認識してください。本番環境にデプロイする際には注意が必要です。no-cache の厳密なセマンティクスが必要ない場合には no-store の使用をお勧めします。


Surrogate-Control


Surrogate-Control ヘッダーは、かなり曖昧なエッジアーキテクチャ仕様に隠されています。これを使用すると、コンテンツプロバイダーがフレッシュネス情報を CDN に明示的に伝え、レスポンスの明示的なフレッシュネス情報コントロールをオーバーライドすることができます。


Surrogate-Control は、Fastly を含め多くの CDN で長年にわたって採用されてきました (ドキュメントを参照)。しかし、仕様の簡潔さ (エディターである私のせい) と、このヘッダーが Cache-Control に似ていることから、機能、シンタックスおよびセマンティクスのさまざまな解釈が多数生まれました。


このため、テストの目的は、max-age および no-store ディレクティブから始めて、Surrogate-Control のために合理的で相互運用可能な基盤を確立することです。想定されるのは、キャッシュが Expires または Cache-Control の前に Surrogate-Control に従い、キャッシュが超過しないように Age ヘッダーのような要素も考慮することです。


テストからは、__Fastly が Surrogate-Control: max-age を適切に処理しているが、まだ Surrogate-Control: no-store に従っていない__ことがわかります。


上記の Cache-Control: no-store と同様に、これも vcl_fetch に追加できます。


if (beresp.http.Surrogate-Control:no-store) { return (pass); }

リクエストの Cache-Control


多くの CDN と同様に Fastly はリクエストの Cache-Control ディレクティブを無視します。このため、リクエストの Cache-Control に関するテストにはほとんど失敗します


CDN がこのようにするには正当な理由があります。クライアントによるキャッシュのコントロールを拡張することは、Web サイト運営者のコントロールが縮小することを意味するからです。パフォーマンスと可用性を1つの CDN に依存している場合、これは攻撃ベクトルになる可能性があるため、ほとんどの顧客はリクエストの Cache-Control をデフォルトで無効にします。


ただし、サイトがリクエストの Cache-Control に従う必要がある場合 (たとえば、HTTP API を提供しており、クライアントが認証されている場合)、VCL では簡単にこれを行うことができます。


vcl_recv では :


# Cache-Control is removed from requests when we try to cache
# so we make a copy of the fields we need forwarded
if (req.http.Cache-Control) {
set req.http.Forward-Cache-Control = req.http.Cache-Control;
}

# handle no-store
if (req.http.Cache-Control:no-store) {
# For the edge node
set req.http.Forward-Cache-Control:no-store = "1";
return(pass);
}
if (req.http.Forward-Cache-Control:no-store) {
# For the shield node
unset req.http.Forward-Cache-Control;
return(pass);
}

if (req.http.Forward-Cache-Control:max-stale) {
declare local var.maxstale RTIME;
set var.maxstale = std.atoi(req.http.Forward-Cache-Control:max-stale);
set req.max_stale_while_revalidate = var.maxstale;
} else {
set req.max_stale_while_revalidate = 0s;
}

vcl_hit では :


declare local var.ttl INTEGER;

if (req.http.Forward-Cache-Control:no-cache) {
set obj.ttl = 0s;
restart;
}

if (req.http.Forward-Cache-Control:max-age) {
declare local var.maxage INTEGER;
set var.maxage = std.atoi(req.http.Forward-Cache-Control:max-age);
set var.ttl = var.maxage;
set var.ttl -= obj.ttl;
if (var.ttl < 1) {

return(pass);
}
}


if (req.http.Forward-Cache-Control:min-fresh) {
declare local var.minfresh INTEGER;
set var.minfresh = std.atoi(req.http.Forward-Cache-Control:min-fresh);
set var.ttl = obj.ttl;
set var.ttl -= var.minfresh;
if (var.ttl < 1) {

return(pass);
}
}


vcl_miss では :


if (req.http.Forward-Cache-Control:only-if-cached) {
error 504 "Gateway Error";
}

if (req.http.Forward-Cache-Control) {
set bereq.http.Cache-Control = req.http.Forward-Cache-Control;
}

unset bereq.http.Forward-Cache-Control;

vcl_fetch では :


# Save responses for request Cache-Control: max-stale
set beresp.stale_while_revalidate = 1h;

リクエストの Cache-Control: max-stale を有効にするために、vcl_fetch では beresp.stale_while_revalidate が設定されていることに注意してください。これは、stale-while-revalidate および max-fresh の処理で最大値として動作するため、それに応じて値を調整します。stale-while-revalidate を使用する場合には、vcl_recvreq.max_stale_while_revalidate を希望の値に変更してください。


ステータスコードおよびキャッシング


HTTP では、レスポンスに明示的なフレッシュネス情報 (または Cache-Control: public) がある限り、ほぼすべてのステータスコード (不明なものでさえ) をキャッシュできます。多くの HTTP キャッシュと同様に、Fastly はさらに保守的で、認識しているステータスコードしかキャッシングしません。ただし、これは (推測どおり) vcl_fetch で変更できます。


if (
beresp.http.Cache-Control:max-age
|| beresp.http.Cache-Control:s-maxage
|| beresp.http.Expires )
{
set beresp.cacheable = true;
}

500 (Internal Server Error)503 (Service Unavailable) については、ここで特に説明する必要があります。これらはサーバーで問題が発生していることを示すため、Fastly はデフォルトで、できるだけ長い間オリジンに対してリクエストを再試行します。クライアントに対してこれらを記述する場合は、vcl_fetch で次のようにします。


if (beresp.status == 500 || beresp.status == 503) {
set req.http.Fastly-Cachetype = "ERROR";
return (deliver);
}

DateAge および Expires


Fastly はフレッシュネスが Cache-Control に基づいたレスポンスを正しく処理していますが、フレッシュネスが Expires のみに基づいたレスポンスについてはいくつかの問題が見つかりました。


Age レスポンスヘッダーは、レスポンスがアップストリームキャッシュで費やした時間をキャッシュが把握するために役立ちます。これは、Expires に基づく場合でさえ、レスポンスのフレッシュネス有効期間の計算に使用されます。


Expires のフレッシュネスを計算する HTTP のアルゴリズムは、実際は以下のようになります。


freshness_lifetime = Date - Expires - Age

DateExpires の両方はオリジンサーバーに由来するため、キャッシュのクロックがオリジンサーバーと適切に同期しているかどうかは関係ありません。ただし、Fastly はこの計算を行いません。代わりに、Expires ヘッダーとローカルクロックを比較して、レスポンスがあとどれだけの期間フレッシュかを判別します。Age は無視します。


コードのコメントから判断すると、このアプローチは Varnish から継承されたようです。オリジンサーバーのクロックが Fastly のクロックと適切に同期している場合、この方法はうまくいきます。ただし、ずれが大きい場合には、レスポンスのキャッシュが意図したよりも長くなったり短くなったりすることになります。


これはテストによって識別できたバグです。現在、既存のお客様の VCL に影響せずにこの問題を修正する方法を調べているところです。今のところ、オリジンと Fastly の間でキャッシュを使用し、Expires を使用している場合は、NTP を使用してオリジンのクロックが適切に同期していることを確認してください。


これらのヘッダーは、ダウンストリームキャッシュ (たとえばブラウザのキャッシュ) を正しく扱うためにも重要です。テストによって、Fastly が Age ヘッダーをクライアントに送信していることが示されました。ただし、フレッシュネスが Expires に基づいているとき、レスポンスが Fastly のキャッシュに格納されていた期間のみに基づいて行われます。レスポンスがアップストリームでキャッシングされていても、Fastly から送られる Age には反映されていません。Fastly よりもダウンストリームのキャッシュがレスポンスを格納していた期間が長すぎる可能性があります。


これは、テストスイートを使用して見つけることができたもう1つのバグです。影響するのは、フレッシュネスのために Expires のみを使用しているレスポンスだけです。現在、既存のお客様が影響を受けないようにしながら、このバグを修正する方法を探しているところです。それまでの間、Fastly とオリジンの間にキャッシュ (Varnish または他のリバースプロキシなど) があるとき、そのキャッシュ内と Fastly およびダウンストリームのキャッシュでの 'Expires' を使用するオブジェクトのフレッシュネス有効期間が指定の期間を超えないようにするには、以下を vcl_fetch に追加します。


if (beresp.http.Age) {
set beresp.http.Before-Fastly-Age = beresp.http.Age;
}

vcl_deliver では :


if (resp.http.Before-Fastly-Age) {
declare local var.age INTEGER;
set var.age = std.atoi(resp.http.Age);
set var.age += std.atoi(resp.http.Before-Fastly-Age);
set resp.http.Age = var.age;
unset resp.http.Before-Fastly-Age;
}

同様に、Date ヘッダーを使用して、Expires ヘッダーに正しく従うようにできます。Fastly はすべてのレスポンスで Date を現在時刻に更新しますが、これは Age に対して潜在的に悪い相互作用があります。ダウンストリーム HTTP キャッシュでは、前述の Expires フレッシュネスのアルゴリズムが使用されるためです。


ただし、Fastly は Date を更新してから Age も送信します。つまり、ダウンストリームキャッシュ (ブラウザのキャッシュなど) によって、レスポンスのフレッシュネス期間が Expires によって許可されるよりも短いと見なすことになります。


繰り返しますが、これはテストスイートを使用して見つけることができるバグです。影響するのは、フレッシュネスのために Expires のみを使用しているレスポンスだけです。Fastly は、現在のお客様に影響せずに修正する方法を調べているところです。ただし、フレッシュネスのために Expires のみを使用するレスポンスが、ダウンストリームキャッシュでレスポンスの有効期間全体を示すことが重要な場合は、vcl_fetch でこれを対処できます。


if (beresp.http.Date) {
set beresp.http.Before-Fastly-Date = beresp.http.Date;
}

vcl_deliver では :


if (resp.http.Before-Fastly-Date) {
set resp.http.Date = resp.http.Before-Fastly-Date;
unset resp.http.Before-Fastly-Date;
}

今後の展望


長期的には、すべての CDN とリバースプロキシがこれらのテストすべて (およびその他のテスト) にパスするのを見るほど嬉しいことはありません (フレッシュネスはまだ始まったばかりです)。そこにたどり着くには、いくつものテストの変更、CDN およびリバースプロキシの調整、多くの議論が必要になります。また、かなりの時間もかかるでしょう。


それで構いません。オープンテストを実施すると、議論が広がるだけでなく実装の決定にも影響することになります。また、お客様にとって Fastly がどのように HTTP を扱っているかに関する透明性が高まります。


CDN の相互運用性が向上することは、すべての人に利益をもたらします。Cache-Control ヘッダーの処理方法に基づいて製品を区別する CDN はありません (私が知る限りでは)。しかし、そのような違いは機能と効率性の両方でお客様のデメリットになる場合があります。このような細かい部分の処理方法の一貫性が向上するほど、多くのお客様およびサードパーティのツールとフレームワークが、1回限りで設定するプラットフォームではなく、構築の基盤として使用できる堅固なプラットフォームとして CDN を見なすようになります。


ご協力いただける場合は、このテストスイートを使用して、(バグまたは新しいテストについて) 問題を報告し、プルリクエストを行ってください。CDN をお使いの場合は、相互運用性が重要であることを CDN プロバイダーに伝えてください。また、CDN の立場の方であれば、どのテストに合格すべきか、また最高のものにするために他に変更すべき箇所は何かについてぜひ話し合いたいと思います。前述したように、場合によって HTTP 仕様に従わないことには正当な理由がありますが、CDN によるプロトコル処理の気まぐれな変更は誰の役にも立ちません。


VCL に関してご協力してくれた、Rogier Mulhuijzen と Andrew Betts に感謝します。

Mark Nottingham
Senior Principal Engineer
投稿日

この記事は0分で読めます

興味がおありですか?
エキスパートへのお問い合わせ
この投稿を共有する
Mark Nottingham
Senior Principal Engineer

Mark Nottingham は90年代後半から、HTTP、キャッシング、リンク、Web アーキテクチャ、プライバシー、セキュリティなどに関する 30 件を超える IETF RFC や W3C 勧告の作成や編集に参加し、Web やインターネットを定義し発展させる活動に貢献してきました。


2007年からは HTTP ワーキンググループの議長を務め、特に HTTP/2 をはじめとする Web の基盤となるプロトコルの拡張を統括してきました。また、QUIC ワーキンググループの議長として、HTTP/3 の策定やインターネットトランスポートプロトコルの拡張においても指揮をとりました。彼は、Internet Architecture Board や W3C Technical Architecture Group などのインターネットガバナンス機関でも活躍しています。


現在は、Fastly で CTO のオフィスに所属しており、メルボルン法科大学院で通信法を学んでいます。Mark は Anitra と結婚し、Charlie と Bennet という2人の息子と共に、家族でオーストラリアのメルボルンに住んでいます。

Fastly試してみませんか ?

アカウントを作成してすぐにご利用いただけます。また、いつでもお気軽にお問い合わせください。