一覧WB-04-2 · HTTPキャッシュ

キャッシュキーとVary

言語とクエリを変え、保存された応答がどの要求へ再利用されるかを確認します。続いて利用者名を含む応答を共有・個人用キャッシュで取得し、privateとs-maxageによる保存範囲・期限の違いを学びます。

説明

同じURLでも、言語や利用者によって本文が変わる場合があります。キャッシュがそれらの違いを考慮せず保存済み本文を返すと、別の言語や別の利用者の応答が混ざります。URLだけで十分か、Varyで何を指定するか、共有してよいかを設計する必要があります。

言語とクエリを変え、保存された応答がどの要求へ再利用されるかを確認します。続いて利用者名を含む応答を共有・個人用キャッシュで取得し、privateとs-maxageによる保存範囲・期限の違いを学びます。

1日本語の応答を保存

Accept-Language: jaで挨拶を取得します。cache.pyの--langが要求ヘッダーの値になります。

python3 cache.py get http://127.0.0.1:18080/greeting --lang jaAccept-Languageにjaを指定 --at 0

本文は「こんにちは」です。この配信元はVary: Accept-Languageを返すため、保存時に要求の言語も記録します。

2同じURLへ英語を要求

1秒後、URLを変えずに英語を要求します。

python3 cache.py get http://127.0.0.1:18080/greeting --lang en同じURLで言語だけ変更 --at 1

日本語の保存はありますが、Varyで指定された値が一致しないため配信元へ要求し、Helloを保存します。Varyは言語を変換する機能ではなく、再利用時に照合する要求ヘッダーを指定します。

3日本語へ戻してキーを確認

再び日本語を取得して保存一覧を開きます。

python3 cache.py get http://127.0.0.1:18080/greeting --lang ja日本語の保存応答を再利用 --at 2 python3 cache.py show言語ごとの保存条件

日本語の応答にHITします。同じURLに対して、valuesのaccept-languageがjaとenの二件が保存されています。キャッシュキーには少なくともメソッドと対象URIを使い、この例ではVaryが追加の照合条件になります。

4Varyが欠けた応答

言語で本文が変わるのにVaryを返さない/broken-greetingを、ja・enの順で取得します。

python3 cache.py get http://127.0.0.1:18080/broken-greetingVaryがない配信 --lang ja --at 3 python3 cache.py get http://127.0.0.1:18080/broken-greeting --lang en --at 4別言語で保存済み応答に一致

英語を要求しても保存済みの「こんにちは」が返ります。このキャッシュには言語を照合する指定がなく、同じURLとして再利用するためです。応答が何によって変化するかと、キー・Varyの設定を一致させる必要があります。

5クエリの違うURL

page=1とpage=2を取得し、もう一度page=1へ戻ります。

python3 cache.py get "http://127.0.0.1:18080/catalog?page=11ページ目のURI" --at 5 python3 cache.py get "http://127.0.0.1:18080/catalog?page=22ページ目のURI" --at 6 python3 cache.py get "http://127.0.0.1:18080/catalog?page=1" --at 7

ページごとに保存され、三回目だけHITします。クエリは対象URIの一部です。CDNなどでキーからクエリを除外する設定を使う場合、本文の選択に使う値まで除外すると混同が起きます。

6個人用本文を共有してしまう設定

X-Userで利用者を指定する演習用の/profileを、publicとしてAlice、Bobの順に取得します。

python3 cache.py get "http://127.0.0.1:18080/profile?policy=public共有保存を許す設定" --user Alice最初の利用者 --at 8 python3 cache.py get "http://127.0.0.1:18080/profile?policy=public" --user Bob次の利用者 --at 9

Bobの要求にuser=Aliceが返ります。本文はX-Userによって変わるのに、その値はキーに入っておらず、共有保存を許しているためです。X-Userはこの演習の本文選択用で、認証方式ではありません。利用者が変わればキャッシュも自動的に分かれるとは限りません。

7privateで共有保存を防ぐ

同じ形式の個人用本文にprivateを指定し、共有キャッシュ経由で二人分を取得します。

python3 cache.py get "http://127.0.0.1:18080/profile?policy=private共有キャッシュには保存させない" --user Alice --at 10 python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Bob別利用者の本文を取得 --at 11

どちらもstore=noで配信元へ要求し、それぞれの利用者名が返ります。privateは共有キャッシュへの保存を禁じます。通信の暗号化や利用者認証を行う指示ではなく、保存範囲の指定です。

8個人用キャッシュでは保存できる

Alice専用の保存ファイルを使い、private応答を二回取得します。

python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Alice --mode private --cache alice.jsonAlice専用の個人用保存先 --at 12 python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Alice --mode private --cache alice.json --at 13同じ利用者で再利用

個人用キャッシュでは保存が許され、二回目はHITします。ここではAlice専用のファイルを用意しています。privateが同じブラウザ内のアカウント切替まで自動的に識別するわけではなく、利用者の切替時に古い個人用応答をどう扱うかは別途設計が必要です。

9共有用と個人用の鮮度寿命

max-age=60とs-maxage=5を返す価格を、共有・個人用の別キャッシュへ保存します。

python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode shared --cache shared.json共有キャッシュ --at 20 python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode private --cache private.json個人用キャッシュ --at 20

同じ本文とヘッダーを保存します。s-maxageは共有キャッシュに対する寿命で、共有側ではmax-ageより優先されます。個人用側ではmax-ageの60秒を使います。

105秒後の再検証の差

両方の保存先から25秒時点で取得します。

python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode shared --cache shared.json共有側の5秒期限 --at 25 python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode private --cache private.json個人用側の60秒期限 --at 25

共有側はage=5、lifetime=5で再検証し、304を受け取ります。個人用側はage=5、lifetime=60でHITします。同じ応答でも、保存するキャッシュの種類によって確認が必要になる時刻が異なります。

まとめ

URLとVaryの指定は、本文を選ぶ条件と対応させます。クエリや言語の違いを落とすと別の応答が混ざります。

privateは共有キャッシュへの保存を禁止し、個人用キャッシュへの保存は許します。認証や暗号化とは別の役割です。

s-maxageは共有側の寿命を指定します。同じ応答でも共有・個人用で再検証の時刻が変わります。

操作履歴と解説を振り返れます。

シェル / curl・HTTP/1.1

1
python3 cache.py get http://127.0.0.1:18080/greeting --lang ja --at 0
origin=200 transferred-body=16 bytes
store=yes Cache-Control: public, max-age=60
こんにちは
2
python3 cache.py get http://127.0.0.1:18080/greeting --lang en --at 1
origin=200 transferred-body=6 bytes
store=yes Cache-Control: public, max-age=60
Hello
3
python3 cache.py get http://127.0.0.1:18080/greeting --lang ja --at 2 python3 cache.py show
cache age=2 lifetime=60 fresh=True
HIT origin-request=no
こんにちは
{"target": "/greeting", "vary": ["accept-language"], "values": {"accept-language": "ja"}, "saved": 0, "control": "public, max-age=60", "etag": "\"24d22f3d\"", "body": "こんにちは\n"}
{"target": "/greeting", "vary": ["accept-language"], "values": {"accept-language": "en"}, "saved": 1, "control": "public, max-age=60", "etag": "\"66a045b4\"", "body": "Hello\n"}
entries= 2
4
python3 cache.py get http://127.0.0.1:18080/broken-greeting --lang ja --at 3 python3 cache.py get http://127.0.0.1:18080/broken-greeting --lang en --at 4
origin=200 transferred-body=16 bytes
store=yes Cache-Control: public, max-age=60
こんにちは
cache age=1 lifetime=60 fresh=True
HIT origin-request=no
こんにちは
5
python3 cache.py get "http://127.0.0.1:18080/catalog?page=1" --at 5 python3 cache.py get "http://127.0.0.1:18080/catalog?page=2" --at 6 python3 cache.py get "http://127.0.0.1:18080/catalog?page=1" --at 7
origin=200 transferred-body=7 bytes
store=yes Cache-Control: public, max-age=60
page=1
origin=200 transferred-body=7 bytes
store=yes Cache-Control: public, max-age=60
page=2
cache age=2 lifetime=60 fresh=True
HIT origin-request=no
page=1
6
python3 cache.py get "http://127.0.0.1:18080/profile?policy=public" --user Alice --at 8 python3 cache.py get "http://127.0.0.1:18080/profile?policy=public" --user Bob --at 9
origin=200 transferred-body=11 bytes
store=yes Cache-Control: public, max-age=60
user=Alice
cache age=1 lifetime=60 fresh=True
HIT origin-request=no
user=Alice
7
python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Alice --at 10 python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Bob --at 11
origin=200 transferred-body=11 bytes
store=no Cache-Control: private, max-age=60
user=Alice
origin=200 transferred-body=9 bytes
store=no Cache-Control: private, max-age=60
user=Bob
8
python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Alice --mode private --cache alice.json --at 12 python3 cache.py get "http://127.0.0.1:18080/profile?policy=private" --user Alice --mode private --cache alice.json --at 13
origin=200 transferred-body=11 bytes
store=yes Cache-Control: private, max-age=60
user=Alice
cache age=1 lifetime=60 fresh=True
HIT origin-request=no
user=Alice
9
python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode shared --cache shared.json --at 20 python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode private --cache private.json --at 20
origin=200 transferred-body=10 bytes
store=yes Cache-Control: public, max-age=60, s-maxage=5
price=120
origin=200 transferred-body=10 bytes
store=yes Cache-Control: public, max-age=60, s-maxage=5
price=120
10
python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode shared --cache shared.json --at 25 python3 cache.py get "http://127.0.0.1:18080/price?policy=shared" --mode private --cache private.json --at 25
cache age=5 lifetime=5 fresh=False
If-None-Match: "v1"
origin=304 transferred-body=0 bytes
reuse stored body
store=yes Cache-Control: public, max-age=60, s-maxage=5
price=120
cache age=5 lifetime=60 fresh=True
HIT origin-request=no
price=120

演習完了