Document
概要
ブラウザキャッシュを利用する仕組み。
それを実現するためのHTTPレスポンスHeader。
ブラウザにて、コンテンツに対するHashを持ち、これを最新のコンテンツ要求時に添える。
サーバ側は送られたHashと最新のコンテンツのHashを比較し、更新の有無をクライアントへ返却する。
更新が無ければコンテンツは返送しない。
仕組み
No | 処理内容 | 処理箇所 | 実装要否 |
---|---|---|---|
1 | クライアントからサーバへコンテンツを要求。 | クライアント | 必要 |
2 | HTTPサーバがレスポンスヘッダーにETag にコンテンツのハッシュ値を付加して返す。 |
サーバ | 必要 |
3 | ブラウザにてリクエスト時にリクエストヘッダーにIf-None-Match を付加する。 |
クライアント | クライアントがブラウザなら不要 |
4 | HTTPサーバにて、送信されたIf-None-Match と最新のコンテンツのハッシュ値を比較し、・ 一致すれば、HTTP Status Code 304 を返却する。ボディは返却しない。 ・ 一致しなければ、HTTP Status Code 200 および最新のコンテンツ、それに応じた ETag ヘッダー付加して、返却する。 |
サーバ | 必要 |
イメージ
上記の5では、いくつかの対応方法がある。
・そのときにコンテンツを計算して、それを元にHashを求める
・あらかじめコンテンツ計算して、そのHashをキャッシュしておく
導入方法
WEBサーバ
- コンテンツ(URL)ごとに
ETag
にハッシュを付加して、返すようにする。 - コンテンツごとのハッシュをサーバ側で管理する(例:キャッシュサーバ、バックエンドサーバメモリ)
- 上記のコンテンツごとのハッシュを更新する機構をどこかで設ける。
- リクエストを受けた際に、
If-None-Match
ヘッダーを確認し、- その値が最新のコンテンツのハッシュと同一の場合は304を返す。ボディは返さず、ブラウザキャッシュで描画させる。
- 上記以外の場合は、最新のコンテンツを返す。
そのコンテンツがユーザ別に変わりうるのか、全ユーザ共通なのかはあらかじめ明確にすること。それによって、ハッシュ計算の対象が変わるし、全体のコストが変わる。
ユーザ別に変わる場合、そもそものキャッシュの是非、キャッシュする場合の必要容量の見積などを慎重に検討する。
基本的には、Getメソッドが対象になるはず。
PostはURLは同一でもパラメータが異なるうえに求める結果も異なるケースがあるため、対応には精査が必要。基本しない、に寄せておくのが無難か。SSRアプリの場合は、Post&Getパターンが利用できるため、Postは対象外にしたうえで、Getにはキャッシュを活用という構えはとれる。
ブラウザ
Cacheを有効にする。
Chromeなら、開発者ツールからDisable Cacheをオフにする。
作って試す
環境
name | version | |
---|---|---|
OS | Ubuntu | 20.04.3 LTS |
Server | express(NodeJS) | 4.16.1 |
Template engine | pug(NodeJS) | 2.0.0-beta11 |
Etag tool | etag(NodeJS) | 1.8.1 |
Cache engine | 適当に手作り(5秒に1回更新) | - |
実際の動き
以下は、1回目と2回目のレスポンス内容
1回目
リクエスト
キャッシュ無し、初回、特になんの変哲もない内容。
GET /etag HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
sec-ch-ua-platform: "Linux"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/etag
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1911431475.1620094102; Idea-212d5d8b=f278ab51-9384-49ab-8e7b-98909abd3516; JSESSIONID=8FF8FC1D0AD60E0CB95ED1DCC6CC3269; _ga_55DN3ZP802=GS1.1.1627800089.42.0.1627800089.0; _ga_C7RLRNBP7H=GS1.1.1630922254.143.1.1630922306.0
レスポンス
ヘッダ
HTTP/1.1 200 OK
X-Powered-By: Express
ETag: "13-XqYPn2sNXor5g1n6J37wHO+av+k"
Content-Type: text/html; charset=utf-8
Content-Length: 1752
Date: Sun, 12 Sep 2021 10:00:08 GMT
Connection: keep-alive
ボディ
省略だが、中身はHTML。
2回目
リクエスト
If-None-Match
を付加して送付。
GET /etag HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="93", " Not;A Brand";v="99", "Chromium";v="93"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
sec-ch-ua-platform: "Linux"
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:3000/etag
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: _ga=GA1.1.1911431475.1620094102; Idea-212d5d8b=f278ab51-9384-49ab-8e7b-98909abd3516; JSESSIONID=8FF8FC1D0AD60E0CB95ED1DCC6CC3269; _ga_55DN3ZP802=GS1.1.1627800089.42.0.1627800089.0; _ga_C7RLRNBP7H=GS1.1.1630922254.143.1.1630922306.0
If-None-Match: "13-XqYPn2sNXor5g1n6J37wHO+av+k"
レスポンス
ヘッダ
HTTP/1.1 200 OK
X-Powered-By: Express
Date: Sun, 12 Sep 2021 10:00:08 GMT
ETag: "13-XqYPn2sNXor5g1n6J37wHO+av+k"
Content-Type: text/html; charset=utf-8
Content-Length: 1752
ボディ
空。ChromeのdebugモードだとPreviewがCacheに対して効いているようで確認しづらい。
一応、レスポンスのサイズは304のときはヘッダのみのサイズで、応答速度も高速になっている。
Postmanでも念の為確認。If-None-Match
相当のコンテンツが変わっていなければ、空と304ステータスが返るため、ブラウザキャッシュを採用する。
ちなみにChrome側でDisable Cacheを有効にすると、If-None-Match
は送信しない。
メリット、デメリット、その他
メリット
- バックエンドサーバリソースの節約
- 通信の効率化
本当にリソースの節約となるかは要確認。
デメリット
- アーキテクチャはやや複雑化する
最低限、
・redisなどの外付けキャッシュサーバを頼る
・ETag用の値生成ルールはトラブルの元にならないよう確認する
SSR(Server Side Rendering)アプリでの有効性
有用。
CSR(Client Side Rendering)アプリでの有効性
- API利用時においては、キャッシュの面ではやや有効
- やはりコンテンツがユーザ別かサービス共通かによって有効性が異なる
- APIコンテンツの楽観ロックにおいて非常に有効
- フロント側の制御をブラウザに委譲できるため、バックエンドで更新対象データのハッシュ照合を行えば良い
Service Worker のCache機構とWebWorkerによる非同期Fetchが勝りそう。
検討基準
以下に該当すればするほど、検討価値がある。
- 更新頻度が低い
- アクセス頻度が高い
- 通信量が多い
- リクエストあたりの処理量が多い
ホーム画面、日次更新データの一覧表示処理。
POSTはURLで一意にならないので辞めたは方が・・・。
画像などの静的コンテンツはCDNで十分なケースも多そう。
静的コンテンツについては、きめ細かいリソース単位の更新制御の要件があるならETag活用もあるか。
CDN + ETagの両方の活用案もある。
fetch API を使った結果
ブラウザ上で実行する限りはしっかりETag
周りの恩恵は受けられる。
関係ないが、304レスポンス時もfetchのstatusは200らしい。
アーキテクチャ例
アーキテクチャは大別すると2パターンある
方式 | 効果 |
---|---|
コンテンツの生成行い、その返却時にETag のハッシュを計算し、送信されていたIf-None-Match と比較して一致していれば、304ステータスだけ返す |
通信量の削減 |
コンテンツの生成を事前に行い、最新のETag とIf-None-Match が一致していれば、304ステータスのみ返す。 |
・通信量の削減 ・レスポンス高速化 |
上記の2点目のパターンを考えてみる。
Cache Serverにて、コンテンツ別のETag
値を持つようにし、
ETag
値の設定はETagHashCalculationProcess
にて行う。CacheCalculationProcess
は、不定期バッチの形態を採ったり、BackendServer
が担ったりする。疎結合を目指すならBackendServer
とは別の形で用意する。
サイト事例
価格.com
フューチャーアーキテクト株式会社
東京工業大学
価格.comは、トップページが条件にかなり合致している。広告とは相性が悪いと思われるが、そこは別途取得しているようだ。
APIでのETag事例
Redisを用いたシーケンス図もあるのでオススメ
フレームワーク事例(Spring Framework))
ETag
は作って返せるが、これはレスポンス生成後に行うから。節約できるのは帯域幅だけやぞ。と注意文言。
ミドルウェア事例
ApacheにFileETagディレクティブ がある。
これが有効化されるとファイルレスポンスについて、ETagを付加して返信する。
リクエスト時もApacheで検証して、Bodyなし304レスポンスが返せる。
検証コード
test.png的な画像ファイルは適当に用意。
docker run --rm httpd:2.4 cat /usr/local/apache2/conf/httpd.conf > my-httpd.conf
echo "FileETag ALL" >> my-httpd.conf
cat <<EOF > Dockerfile
> FROM httpd:2.4
>
> COPY ./my-httpd.conf /usr/local/apache2/conf/httpd.conf
> COPY ./test.png /usr/local/apache2/htdocs/
> EOF
docker build -t httpd-etag .
docker run -p 80:80 httpd-etag
クラウド事例
AWSのCloudFrontにETagに関する言及がある。
特に機能的なサポートはないように見える。
検証用コード(NodeJSサーバアプリ部分)