Vary ヘッダーの使用に関するベストプラクティス
Vary とは
Vary は、最もパワフルな HTTP レスポンスヘッダーのひとつです。正しく使用すれば、優れた性能を発揮します。しかし残念なことに、このヘッダーは誤って使用されることが多く、そうなるとヒット率はきわめて低くなります。さらには、タイミングを間違えると、誤ったコンテンツが配信されかねません。
この記事では、単に Vary の仕様を紹介するのではなく、最も一般的なユースケースである圧縮を用いて Vary ヘッダーについてご説明します。
Apache の mod_deflate を使用している場合、正しい Vary ヘッダーは自動的にレスポンスに追加されます。Fastly の gzip 機能でも同様です。ただし、これは非常にシンプルかつ実際的な Vary の使用方法なので、これを使って Vary の仕組みをご説明します。
undefined
HTTP リクエストの構造
通常、Fastly のキャッシュのひとつにリクエストが届くと、キャッシュ内でオブジェクトを探すために、リクエストの2つの部分、すなわちパス (もしあればクエリ文字列も) とホストヘッダーのみが使用されます。
以下は、http://example.com/somepage.php に対するごく一般的なリクエストです。
GET **/somepage.php** HTTP/1.1
Host: **example.com**
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36
Accept-Language: en-US,en;q=0.8
Accept-Encoding: gzip,deflate,sdch
上記の通り、ブラウザは URL と一緒に多くの情報を送信します。Accept ヘッダーはブラウザが好むコンテンツの種類を示し、User-Agent はブラウザの種類とバージョンを指定します。Accept-Language にはユーザーが設定している言語 (および方言) のリストが含まれます。Accept-Encoding は、ブラウザでサポートされる圧縮方法を示します。
実用上の理由から、私たちは gzip
のみを扱います。deflate
は古く、sdch
は Google 以外ではサポートされていないためです。
圧縮の仕組み
以下のように仮定します。mod_deflate
のない Web サーバーを使用していますが、PHP で gzip 圧縮を行う方法を見つけました。そのため、Accept-Encoding
ヘッダーに gzip
がある場合には、Content-Encoding: gzip
ヘッダーを設定してブラウザに実行内容を伝え、レスポンス本文を圧縮します。
このサーバーが Fastly サービスのオリジンであり、このページがキャッシュ可能であると想定してください。圧縮を理解しないブラウザが最初にこのページを要求した場合、何が起こるでしょうか。キャッシュには、圧縮されていないページが格納されることになります。
これ自体は大きな問題ではありません。圧縮を理解するブラウザがこのページをリクエストした場合、非圧縮バージョンがキャッシュから取得され、正常にレンダリングが行われます。ただし、非圧縮バージョンはサイズが大きいため、送信に使用される帯域が増加する結果、エンドユーザーへの配信が遅くなり、コストが上昇します。
より深刻な問題が起きるのは、オブジェクトに対する最初のリクエストが圧縮を行うブラウザから届いた場合です。この場合、圧縮バージョンがキャッシュされることになります。そうなると、圧縮を理解しないブラウザがリクエストしたときに、そのブラウザは圧縮バージョンを取得することになり、仕方なく文字化けを表示するほかありません。
Vary を使用した問題の解決方法
この問題を解決する方法は2つあります。ひとつ目は、Fastly の設定でキャッシュキーを変更することです。ただし、これはさらなる問題の原因となります。すなわち、
圧縮バージョンと非圧縮バージョンの両方を別々にパージする必要がある。
変更を適切に行わないと、すべての URL が同じ単一のオブジェクトを返すようになる可能性がある。
これを行う代わりに、Vary ヘッダーを使用することで、これら両方の問題を回避できます。
Vary ヘッダーは、HTTP キャッシュが正しいオブジェクトを見つけようとする際に、リクエストヘッダーのパスと Host ヘッダー以外のどの部分を考慮すべきかを指示します。これは関連のあるヘッダー名を列挙することで行われ、今回のケースでは Accept-Encoding
となります。もしレスポンスに影響するヘッダーが複数ある場合は、ひとつのヘッダーにすべてがコンマ区切りで列挙されます。
圧縮レスポンスのレスポンスヘッダーは以下のようになります。
HTTP/1.1 200 OK
Content-Length: 3458
Cache-Control: max-age=86400
Content-Encoding: gzip
**Vary: Accept-Encoding**
非圧縮レスポンスのレスポンスヘッダーは以下のようになります。
HTTP/1.1 200 OK
Content-Length: 8365
Cache-Control: max-age=86400
**Vary: Accept-Encoding**
ここで注目すべきは、圧縮が使用されるかどうかにかかわらず、レスポンスヘッダーに Vary ヘッダーが存在することです。
これはなぜでしょうか。実際に Vary ヘッダーがあると何が起こるかを見てみましょう。
まず、あるオブジェクトに対するリクエストが届きますが、これに Accept-Encoding ヘッダーはありません。このオブジェクトがキャッシュにないため、オリジンにリクエストが行き、オリジンは Vary ヘッダーを付けてこれを返します。Fastly がこのオブジェクトをキャッシュに保存する際、Vary ヘッダーが付記され、リクエストに含まれている関連ヘッダーの値も格納されます。
これで、「Accept-Encoding
がリクエストヘッダーに含まれていないリクエストに対してのみ使用する」という指示のフラグが付けられたオブジェクトが、キャッシュに存在することになります。
ここで、圧縮を理解できるブラウザから上述のようなリクエストを受信したと想定してください。まず、Host ヘッダーとパスを使用して参照が行われます。これでオブジェクトが見つかりますが、このリクエストには gzip,deflate,sdch
が指定された Accept-Encoding ヘッダーがあり、このオブジェクトに付けられたフラグとは一致しません。
そのため Fastly は再びオリジンにアクセスするものの、今度はオブジェクトの圧縮バージョンを受け取ります。このレスポンスは、その後「このバージョンは Accept-Encoding: gzip,deflate,sdch
を含むリクエストに対してのみ使用される」というフラグが付けられてキャッシュに格納されます。
Vary ヘッダーが最初のレスポンスになければ、キャッシュされたオブジェクトを2番目のリクエストに使用できないことを認識できなかったでしょう。
正規化
現在のすべてのブラウザが同じ Accept-Encoding ヘッダーを送信するのかどうか、疑問に思われるかもしれません。
残念ながら、答えは「No」です。
Fastly のキャッシュのひとつで、10万件のリクエストをサンプルとして抽出したところ、44の異なる Accept-Encoding ヘッダーが見つかりました (この調査方法や数値にご興味がある方は、こちらをご参照ください)。
それらのリクエストすべてが同一の URL に対するものだったとすれば、キャッシュには44の「異なる」バージョンが存在することになります。しかし、オリジンが生成できるのは、Accept-Encoding
に gzip
が 含まれる場合と含まれない場合の2つのバージョンのみのため、42件のオリジンへのリクエストは回避した方がよいことになります。
オリジンが確認するのは gzip の有無のみであるため、 gzip
を含むまたは含まない Accept-Encoding ヘッダーを正規化すればよいのです。
リクエストの Accept-Encoding ヘッダーには2種類しかなく、ゆえにキャッシュに格納されるオブジェクトも2種類のみになります。
これは数行の VCL で簡単に行うことができます。
# do this only once per request
if (req.restarts == 0) {
# normalize Accept-Encoding to reduce vary
if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
unset req.http.Accept-Encoding;
}
}
旧来の HTTP クライアントをまだサポートする必要があるかもしれないので、deflate
のサポートも追加し、(圧縮ファイルの処理が苦手で有名な) Internet Explorer 6 が圧縮ファイルを扱わずに済むようにします。
# do this only once per request
if (req.restarts == 0) {
# normalize Accept-Encoding to reduce vary
if (req.http.Accept-Encoding) {
if (req.http.User-Agent ~ "MSIE 6") {
unset req.http.Accept-Encoding;
} elsif (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} elsif (req.http.Accept-Encoding ~ "deflate") {
set req.http.Accept-Encoding = "deflate";
} else {
unset req.http.Accept-Encoding;
}
}
}
これに不思議なほどそっくりな VCL が、会社の設立当初から Fastly Master VCL の一部となっています。
Vary の使用に関するヒント
ご覧のように、リクエストヘッダーの正規化を行わずに単純な Vary ヘッダーをレスポンスに追加するだけで、オリジンに送信されるリクエストの量が大幅に増加し、悲惨な事態につながる可能性があります。これを防ぐ唯一の方法は、Fastly が行う標準の正規化です。
まず第一に、多くのバリエーションがあるヘッダーに対し、正規化を行わずに Vary を使用しないでください。
第二に、正規化を行う場合は、ヘッダーのバリエーションを多くても5種類ほどに抑えるようにします。これは、regsub()
を利用せずに値を VCL でハードコーディングすることで可能となります。経験からいうと、有効期限が長い人気のコンテンツの場合、オリジンへのトラフィック量は可能値の数に比例して増大します。
以下に、長年目にしてきたいくつかの Vary ヘッダーと、それらが不適切な理由、さらにそれらを正規化する方法をご紹介します。
Vary : User-Agent
文字通り、数千の異なる User-Agent 文字列があります。10万件のリクエストのサンプルでは8,000弱が見つかりました。
あるシナリオで、モバイルユーザー向けに異なるフォーマットを表示したいとします。この例では、User-Agent ヘッダーが通常の値とはまったく異なるシンプルな文字列に置き換えられます。このヘッダーを使用する場合は、オリジンがその処理方法を確実に認識できるようにする必要があります。
以下のような VCL を使用できます。
if (req.http.User-Agent ~ "(Mobile|Android|iPhone|iPad)") {
set req.http.User-Agent = "mobile";
} else {
set req.http.User-Agent = "desktop";
}
Vary : Referer
コンテンツの人気がとても高いと、多くの他サイトからリンクされ、また Google での検索クエリごとに固有の URL が生成されます。これらはいずれも固有の Referer 値の増加につながります。
例えば、他サイトのページから自社ページのいずれかに訪問したユーザーにはウェルカムポップアップを表示し、自社サイト内で移動するユーザーに対しては表示しないように設定したい場合、
以下のような VCL が必要になります。
if (req.http.Referer ~ "^https?://www.example.com/") {
set req.http.Referer = "http://www.example.com/";
} else {
unset req.http.Referer;
}
Vary : Cookie
Cookie
は、おそらく最も一意性が高いリクエストヘッダーのひとつであるため、Vary ヘッダーとしてはきわめて不適切です。Cookie によって認証の詳細が伝えられることが多いので、その場合はページをキャッシュしようとせず、通過させるだけにします。Cookie の追跡を使用したキャッシングに関心がある場合は、こちらをご参照ください。
ただし、Cookie を A/B テストのために使用する場合は、カスタムヘッダーに Vary を使用して、Cookie ヘッダーをそのまま残すことをお勧めします。これにより、Cookie を必要とする URL (おそらくキャッシュ不可能) 向けの Cookie ヘッダーを残すために多くのロジックを追加せずに済みます。
sub vcl_recv {
# set the custom header
if (req.http.Cookie ~ "ABtesting=B") {
set req.http.X-ABtesting = "B";
} else {
set req.http.X-ABtesting = "A";
}
...
}
...
sub vcl_fetch {
# vary on the custom header
if (beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary ", X-ABtesting";
} else {
set beresp.http.Vary = "X-ABtesting";
}
...
}
...
sub vcl_deliver {
# Hide the existence of the header from downstream
if (resp.http.Vary) {
set resp.http.Vary = regsub(resp.http.Vary, "X-ABtesting", "Cookie");
}
# Set the ABtesting cookie if not present in the request
if (req.http.Cookie !~ "ABtesting=") {
# 75% A, 25% B
if (randombool(3, 4)) {
add resp.http.Set-Cookie = "ABtesting=A; expires=" now + 180d "; path=/;";
} else {
add resp.http.Set-Cookie = "ABtesting=B; expires=" now + 180d "; path=/;";
}
}
...
}
キャッシュのルックアップの前にある vcl_recv
では、Cookie に基づき VCL を使用してカスタムヘッダーを追加できます。この場合、Cookie の値は B
または A
であると仮定され、Cookie がない場合にはデフォルトで A
になります。
次に、vcl_fetch
で Vary にカスタムヘッダーを追加します。最後に、vcl_deliver
で Vary のカスタムヘッダーを Cookie
と置き換えます。これにより、カスタムヘッダーの存在を外部から隠すだけでなく、Fastly とエンドユーザーが共有するキャッシュがある場合にも適切なバージョンのオブジェクトを取得できます。
ここで A/B テストが行われますが、すべてのブラウザで見えるのは単に別の Cookie で、オリジンはごくシンプルなヘッダー (X-ABtesting
) にもとづいて異なる処理を行えばよいだけです。
Vary : *
これは使用不可です。
HTTP RFC では、Vary ヘッダーに特殊なヘッダー名*
が含まれる場合、当該 URL に対する各リクエストは一意の (キャッシュ不可能な) リクエストとして処理されるとしています。
これを行う場合は、Cache-Control: private
で示した方がレスポンスヘッダーを読む誰から見てもわかりやすく、またオブジェクトを格納すべきでないことも示されるため、こちらのヘッダーの方がはるかに安全です。
Vary : Accept-Encoding、User-Agent、Cookie、Referer
実際にこれに遭遇したことがあります。まったく正規化されていませんでした。ご想像どおり、キャッシュヒットがある可能性は、万に1つならぬ1グーゴルプレックス (10の10の100乗乗) に1つでしょう。
さらに高度な活用
今回は、Vary での圧縮についてお話をして、デバイス検出のシンプルなロジックを紹介して、エッジでの A/B テストのロジックを設定しました。次回のブログ記事では、GeoIP や Accept-Language など、Vary のさらに高度な活用方法について説明します。