バカンス駆動開発

この前バカンスって言ったら「古っ」って言われました

静的ファイルのキャッシュコントロールについて ISUCON7

※以前掲載していたブログが消えたので再掲です。

今回初めてISUCON7に参加させていただきました。(チーム名:元pyns)

今回のお題の一つ目の壁は、いかに画像ファイル(アバターアイコン)をキャッシュさせてサーバーからデータを返さないようにするかでした。
8時間の大部分をこの対応に費やしましたが解決は出来ませんでした。
原因はきっちり304を返すための基礎知識が足りていなかったことです。
ですのでこれを機に勉強しなおしてみました。

304 (Not Modified)

大前提ですが、304ステータスコードは キャッシュの有効無効の確認付きリクエストに対して、有効である場合に返すステータスコード です。
この場合サーバーはリソースデータ(ペイロード)を送信しません。

すなわち,サーバは、[ クライアントに格納済みの その表現を 200 (OK) 応答のペイロードであったかのように,用立ててもらう ]べく,クライアントをリダイレクトしている。

RFC 7232 — HTTP/1.1: Conditional Requests(日本語訳)

訳が険しいですが意味は十分わかると思います。
静的ファイルをキャッシュさせようとする場合、適切に304を返すことが重要になります。(isucon開始前の自分に言ってます)

HTTPのキャッシュに関係するヘッダ

キャッシュの振る舞いを制御するためのレスポンスヘッダにはこれらがあります。

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

Cache-Control

Cache-Controlはレスポンス/リクエストに対してキャッシュのふるまいを制御するための指示を指定するために利用されます。

レスポンスのCache-Controlに指定できるディレクティブはこれらです。

ディレクティブ 意味
must-revalidate 期限切れのキャッシュに対してオリジンサーバーで現在でも有効かどうか確認しなければキャッシュとして使用できないことを指示する
no-cache キャッシュはさせるが、オリジンサーバーに現在でも有効かどうか確認しなければキャッシュとして使用できないことを指示する。 確認方法にはIf-None-MatchまたはIf-Modified-Sinceを用います。いかにも「キャッシュするな」ぽいのがややこしい
no-store リクエストもレスポンスも一切キャッシュしてはいけないことを指示する
no-transform 中継者(CDN,プロキシなど)に対してメディアの形式変換を禁止する
public 共有キャッシュに保持して良いことを指示する
private 単独の利用者の使用が意図されているものであり、共有キャッシュに格納されてはならいことを指示する
proxy-revalidate must-revalidateの中間キャッシュに対するもの
max-age 指定した時間を過ぎるとfreshではないとみなすことを指示する1分キャッシュの例:Cache-Control: max-age=60
s-maxage max-ageがクライアントに対しての有効期限であるのに対して、こちらは共有キャッシュに対しての有効期限。max-age,s-maxage両方セットされている場合は、共有キャッシュに関してはs-maxageを上書きする

静的コンテンツをキャッシュさせる場合はmax-ageとpublicに着目

max-age

max-ageが1以上の場合、その秒数クライアントはリクエストを行いません。

public

共有キャッシュに保持しても良いということですが、共有キャッシュとは複数のユーザーが通る中間点のことで、例えばCDNやプロキシなどです。

publicを指定するとこの図の左下の状態になります。

HTTP キャッシュ - HTTP | MDN から

今回のisucon7では、publicを指定すると点数が跳ね上がったみたいですのでチェッカークライアントとサーバーの間に何かしらキャッシュ機構があったということでしょうか。
でもこれはpublicにして点数が上がること以外にどうやって察知したら良かったのでしょうか・・💧
チェッカーから直接飛んで来ていない可能性については微塵も思いつきませんでした・・w

Expires

意味的にはCache-Control max-ageと同じです。 ヘッダの記載の例はこうなります。

Expires: Thu, 01 Dec 1994 16:00:00 GMT

時刻が無効なフォーマットの場合はゼロ、つまりキャッシュの有効期間切れと判断します。 Cache-Control max-ageと同時に指定されている場合、クライアントはExpiresヘッダを無視します。 HTTP1.0時代の名残のようです。

Etag

めずらしくwikiの説明がわかりやすいのでwikiを見ていただければ良いのですが
HTTP ETag – Wikipedia
かいつまむと、リソースに対してフィンガープリントのような識別子を生成し、それをサーバー・クライアント間で連絡し合うことでリソースが変更されたかどうか検知させます。

レスポンスヘッダにEtagが含まれているとクライアントは次回のリクエストからヘッダにIf-None-Matchをつけます。 If-None-Matchを受けたサーバーは識別子が同一であれば304を返します。 異なれば新しいリソースとともに新しいEtagヘッダを返します。 レスポンスヘッダの例

ETag: "686897696a7c876b7e"←識別子

リクエストヘッダの例

If-None-Match: "686897696a7c876b7e"←識別子

識別子の生成ルール

またHTTPにおいて、Etagの識別子の生成に決まりはありません。 nginxでは[mtime size]を使って生成します。 etag生成箇所のソースを貼っつけてくれている方がいました。
Algorithm behind nginx etag generation - Server Fault

apacheではデフォルトで[inode mtime size]を使って生成します。
#FileETag Apache HTTP サーバ バージョン 2.4
httpd.confで以下のように指定することで変更できます。

//最終更新時刻とファイルサイズでEtagを生成
FileETag MTime Size

// 生成しない
FileEtag None

配信サーバーが2台以上ある場合

同一のコンテンツを配信するサーバーが2台以上ある場合は、気をつけないとEtagによるキャッシュ制御が意図したものになりません。 apacheのデフォルトではinodeを用いているので仮にファイルのmtimeとsizeが同じでもサーバーが違えばinodeが異なります。 またinodeを無視しても、mtimeがずれていると異なるEtagになるのでそれぞれ別でキャッシュしてしまいます。

Last-Modified

このヘッダはリソースの更新時刻をクライアントに返し、次回アクセス時にクライアントからその更新時刻を送信させます。 一致していれば304を返し、一致していなければ新しい(バージョンの)リソースと更新時刻を返します。

レスポンスヘッダの例

Last-Modified: Tue, 15 Nov 1994 12:45:26 GMT

リクエストヘッダの例

If-Modified-Since: Tue, 15 Nov 1994 12:45:26 GMT

配信サーバーが2台以上ある場合 こちらもEtag同様で同じファイルでもサーバー間で更新時刻がずれていると個別でキャッシュされて非効率になります。

まとめ

ISUCON最高でした! 運営の皆さんにはこんな貴重な機会を提供していただき感謝しております。

参考