【 読書メモ】Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する

- no-caseはキャッシュしない
- publicは共有キャッシュ(shared)に格納する
実際のところ、そうではない

2.2
MDN CC-BY-SA 2.5より引用
Column RFC MUST/SHOULD-RFC 2119
RFC で要件レベルを示すために使用されるキーワードが記載されている

2.3 配信の経路
キャッシュの特徴・配信経路
- ブラウザキャッシュ
- Proxyでのキャッシュ
- CDNでのキャッシュ
ブラウザでWebページを閲覧するまで
- 大前提: Webページを閲覧するためには、インターネットにつながっている必要がある
- ネットワーク網を構築するISPが相互に接続することで、ISP内のネットワーク以外のサイトにも接続することができる
- 例:ドコモのスマートフォン(ISPはドコモ)でも、KDDIのISPにつながっていれば、KDDIのWebページを閲覧できる
- ISP同士(1:1)で接続するとコストがかるため、IXという相互接続施設で複数のISPやDataCenter同士が接続をしていることが多い
区間の単位
ファーストマイル -> ミドルマイル -> ラストマイル
- ファーストマイル(オリジンが繋がっているISPからの別のISPに接続するまでの部分)
- ミドルマイル(最初のISPの出口からユーザーが契約しているISPの入り口までの区間)
- ラストマイル(ユーザーが契約しているISPの出口から自宅やスマートフォンまでの足回りの区間)
- ラストワンマイル(基地局から家庭までの最終期間)
ブラウザでWebページを閲覧する時、何が起こっているのか
- ネットワーク機器やサーバーに割り当てられたIPアドレス(住所)に接続するために、ドメイン名(ブラウザで入力している
example.com
などの名前)をIPアドレスに変換する必要がある - DNSがドメイン名をIPアドレスに変換(名前解決)してくれる
- DNSで解決したIPアドレスにTCPで接続を行い、HTTPで取得する
インターネットアクセスの経路を体験する
traceroute
- ホップ数
- ルーターなどの機器を経由して別のネットワークを通過することをホップするといい、その個数をホップ数という
- RTT
- ネトワーク機器にリクエストを投げた場合に返ってくる反応時間
- このルーターで折り返すとXms程度かかる、などがわかる
tracerouteの比較を行う際は、RTTの数字に注目する
- TCPではデータの送信に対して「ここまで受け取ったよ」ということを示す返答がペアになる
- RTTが大きければ返答が返ってくるまでに時間がかかるため、次のデータ送信まで待ちが発生する
- そのため、RTTが大きければ通信速度が低下する
- RTTが増加する要因として、地理的要因がある
Coulmn
帯域
その回線が時間あたりどれだけデータを流せるかを示す
通信速度(スループット)
実際に時間あたりでどれだけデータが転送できたかを示す。近い言葉にグッドプット^1 がある。
レイテンシ
本書では、リクエストを送ってからレスポンスが帰ってくるまでの応答時間を指す
^1 パケットロスによって引き起こされる再送を除いたTCP層での実質的な転送速度のこと
- 通信速度はさまざまなオーバーヘッドを含んでいる
- TCPのヘッダは20バイト、IPv4ヘッダが20バイトあるが、通信速度はこの(ヘッダ情報の)オーバーヘッドも含まれる
- グッドプットはこのようなオーバヘッドを省いた通信速度

2.4 配信の高速化
何ができるのか
- クライアントがコンテンツを高速にダウンロードできる
- 突発的なリクエスト増に耐える
- 低コストで実現する
クライアント側の高速化
最速の配信とは、クライアントがローカルに持っているキャッシュを使うこと
クライアント側での最適化は、ローカルキャッシュを利用して、いかにリクエストを発生しない状況をつくること
サーバー側の高速化
- サーバーは、オリジンサーバーやProxyなどのキャッシュサーバー
- クライアントに対して低コストで高速にコンテンツを配信するために、コンテンツサイズを小さくするなどのアプローチが有効
- しかし、コンテンツを生成し小さくする処理をリクエストごとに行うとオリジンの負荷が増大する
- 結果、機器の強化や増設のコストが増える
- キャッシュサーバーが小さくしたコンテンツをキャッシュすることで、問題の軽減ができる
- オリジンの負荷を減らし、突発的なリクエスト増にも耐え、さらに低コストに配信できる
キャッシュサーバーはオリジンのコンテンツのコピーを配信する - オリジンではさまざまな処理を行いコンテンツを生成したり、自身のストレージに保管しているファイルをレスポンスをしたりする
- キャッシュサーバーはキャッシュあがあればそれをレスポンスし、なければオリジンに問い合わせを行いキャッシュしてレスポンスする
- キャッシュサーバーを増やしてしまえば、オリジンの負荷をさほどあげずに帯域^2を増やせる
- CDNはこの特性を活用し、大規模にしたもの
^2 その回線が時間あたりどれだけデータを流せるかを示す

メモ
- 配信サービスのHTTP/3

2.5 キャッシュの格納場所についての分類
- クライアント側のキャッシュ(ローカルキャッシュ/ブラウザキャッシュ)
- スマートフォンで端末にDLしたアセットを利用するのもローカルキャッシュの一種
- 配信経路上のキャッシュ(CDNでのキャッシュ)
- キャッシュでオリジンの負荷軽減を目指す
- 上手く使えばCDNが非力なオリジンの負担を肩代わり(オフロード)し、大きなトラフィックをさばくことも可能
- オリジンのキャッシュ(ゲートウェイキャッシュ/Proxyキャッシュ)
- DNS方式とIP Anycast方式の2つがある
- DNS方式では、ドメイン名解決時に、より近いエッジサーバーのIPアドレスを返却することで実現
- IP Anycast方式では、同一のIPアドレスを別のエッジサーバーに割り振ることで実現
- IP Anycast方式では細かい振り分けが難しいなど一長一短
- DNS方式とIP Anycast方式の2つがある
Column RFC7871
- AkamaiのCDNはECSに対応しておらず、このような(ECS非対応の)DNSキャッシュサービスを使うと逆に遅くなることもあった
ゲートウェイ(サーバー/オリジン)のキャッシュ
-
nginx、Apache Traffic Server、Varnishなど
-
Appサーバーの至近など(オリジンのそば)でキャッシュする
-
Webサイトをインターネットに繋げるゲートウェイ部分で行うキャッシュのことを、ゲートウェイキャッシュと呼ぶ
-
ゲートウェイキャッシュはオリジンのサーバー近くに展開することが多い
-
経路上のキャッシュと合わせて、オリジン側でキャッシュを行う動機
- CDNを利用してもキャッシュの一貫性を維持しやすい
- オリジンはリクエストを受けるたびに更新されたオブジェクトをレスポンスするため、CDNの各エッジは異なるオブジェクトをキャッシュする可能性がある
- オリジン(のすぐ近く)でキャッシュをすることで、ストリーミングのような時系列で変化するオブジェクトのキャッシュを一貫させられる
-
複数のクライアントで共有ができるのがsharedキャッシュ
-
共有してはいけないのがprivateキャッシュ

3.2
HTTPメッセージ
非標準ヘッダ
- 慣例的にx-の接頭辞をつけるものが多い
-
x-goog-storage-class
: Google Cloud Storage の外レージクラスを示す独自の非標準ヘッダ
-
- X- がついたまま標準化されたものもある
- 一覧
配信に必要な標準ヘッダ
- RFC7230 サーバーは一部の例外を除き、同一名のヘッダを送ってはならない
- クライアントがそのようなものを受信した時、ヘッダの畳み込みをしてもよいことになっている
- カンマ区切りで畳み込みなど
- Set-Cookie ヘッダなど、畳み込みで意味が変わってしまうものもある
ボディ
- ボディは圧縮されていることがある
- ボディが多形式でエンコード(圧縮)されている場合はContent-Encodingヘッダで形式を示す
- Jsonなど
HTTP/2
- すべてのヘッダフィールドが小文字になった
- Cache-Controlはcache-control
- 標準仕様では大文字小文字を区別しないが、標準仕様から外れる(大文字小文字を区別する)と対策が必要になる
- 例として、PHPではgetallheaders() という関数でヘッダを取得できる
-
getallheaders()
は大文字小文字を区別するため、実装によっては影響を受けてしまう
- HTTP/2 では開始行は存在せず、同等の情報を擬似ヘッダとして送る
- 擬似ヘッダは
:
ではじまる
補足
- Amazon,TwitterはHTTP/2

3.3 ステータスコードと説明区
代表的なステータスコードの紹介
Column
- Status Code 418
- RFC2324で定義されているジョークステータスコード
- HTCPCP

3.4
- キャッシュはHTTPのオプション機能であるが、キャッシュの再利用は望ましいものであり、それを防止するような要件・設定がない場合は「デフォルトの動作」である(RFC7234で定義)
- つまり、HTTPのレスポンスはいくつかの要件に合致していれば設定なしでもキャッシュされるもので、キャッシュを防ぎたい場合は
Cache-Control
で制御する必要がある
- つまり、HTTPのレスポンスはいくつかの要件に合致していれば設定なしでもキャッシュされるもので、キャッシュを防ぎたい場合は
- キャッシュは適切に制御してこそ価値を生む
キャッシュを行う・使う条件(RFC7234)
HTTPでは、いくつかの条件を満たしていれば特別な設定をしなくてもキャッシュが行われる
- リクエストメソッドが解釈できるもので、かつキャッシュ可能なメソッドとして定義されている
- ステータスコードが解釈できるもの
- リクエスト・レスポンスの
Cache-Control
にno-storeが含まれていない - 経路上のキャッシュ(shared)として格納しようとしている際、レスポンスの
Cache-Control
にprivateの指定がないこと - 経路上のキャッシュ(shared)として格納しようとしている際、リクエストにAuthorizationヘッダが含まれていないこと。ただし、明示的に許可している場合は除く
- レスポンスで以下の条件のうちどれかを満たす
- Expireヘッダを含む
- Cache-Controlにmax-ageを含む
- 経路上のキャッシュ(shared)として格納しようとしている際、レスポンスの
Cache-Control
にs-maxageを含む - 拡張ディレクティブで明示的に許可されているもの
- ステータスコードがデフォルトでキャッシュ可能なもの
- Cache-Controlにpublicを含む

3.6 Cache-Conrtolにおける期限指定
- いつまでキャッシュを保持するのか?
- max-ageのディレクティブを使ってキャッシュ期限を設定する
- キャッシュの有効期限のことを TTLと呼ぶ
- キャッシュには複数の状態が存在する
- Fresh いわゆるキャッシュとしてそのまま使える
- 検証が成功してキャッシュが有効な状態
- Stale 期限切れだが条件付きで使える
- 検証が無効な状態
- StaleからFreshにするには、再検証に成功して検証が有効な状態にする必要がある
- キャッシュは指定した期間保存されるとも限らない
- 期限が切れたからといってすぐに消えない
- max-age オリジンがレスポンスを生成した時刻を起点とした相対的なキャッシュの期限
- max-age=N[sec]はオリジンがレスポンスを生成した時刻を起点とした、相対的なキャッシュの期限を示す。ここでのキャッシュの期限は、Freshな期限(*検証が成功してキャッシュが有効な状態)を示す
- s-maxage 経路上のキャッシュにのみ有効なmax-age
- max-ageとs-maxageは両方指定することができるが、経路上のキャッシュにおいてはs-maxageが優先される
- stale-while-revalidate 期限切れ時にバックグラウンドで再検証を行う間に古いキャッシュを使うことを許容する期限
- stale-if-error 期限切れ時の再検証でオリジンがダウンしていた場合に古いキャッシュを使うことを許容する期限
3.7 Cache-ControlとExpiresヘッダ
- Expiresヘッダは値に時刻を入れることで、期限切れを絶対時間で指定する
- Cach-Control: max-age と用途が近いが、基本的にはCache-Control: max-ageを優先すべき
- ExpiresはCache-Controlを介さないクライアント向けのもの
- Expiresを使うとしてもCache-Controlと併用するのがよい
3.8
- TTLが未定義の時の挙動-RFC 7234#4.2.2
- キャッシュはHTTPにおけるデフォルトの動作である
- TTLが指定されない場合も、キャッシュの条件に合致していればキャッシュは行われる
- このとき、以下のヘッダを利用して計算する
- Date
- Last-Modified
- TTL = Date-Last-Modified / ブラウザ実装による定数(一般的には10日)
3.9
- キャッシュをさせたくない場合のCache-Control
- 1お案的にキャッシュで事故が起こる背景にあるのは、経路上でキャッシュが行われること、それが本来見えるべきでないクライアントに見えてしまうこと
- 事故を防ぐには、まずprivateの設定で経路上のキャッシュを避けることが必要
- キャッシュへ保存させないために、no-storeの設定が必要
- キャッシュを行わないno-cacheも指定する
- 最後にオリジンがダウンしていた場合にキャッシュが使われることを防ぐため、ust-revalidateを指定する
- no-store以外にもいくつか指定を加えているのは、ProxyやCDN間の互換性の問題をなるべく軽減するため
- これらを踏まえると次のような設定になる
Chache-Control: priavte,no-sotre,no-cache,must-revalidate
- クライアントによって解釈される項目が違うのが難しい
- Last-Modifiedヘッダを消すことでTTL未指定んの動きも排除でき、安全性が高い
- max-age=0を設定することでキャッシュさせないことができるが、もし他の項目が解釈されなかった場合にはキャッシュがつくられるので注意が必要
- DateとLas-Modifiedがあればカッシュはされるため、誤りに気づきづらい
Column
- キャッシュ不可なステータスコードをキャッシュする
- 403はキャッシュ不可なステータスコード
- キャッシュ不可なものでも、たとえばmax-ageやpublicを指定すればキャッシュできる場合もある(キャッシュする条件3.4.1の一部を見ると、
レスポンスで以下の条件うちどれかをみたす
はOR条件のため回避できる条件が存在する) - 一方で、RFC通りに動作するとは限らない
- Chrome83ではキャッシュされるが、Firefox77ではされないなど
- 使用はすべて実装されているとは限らない
- Cache-Controlは事故を起こしやすい
- CDNによってはno-cache/no-storeを見ないこともある
- キャッシュ回避にはprivate指定が必要
- ほかにも、0やマイナスのキャッシュ期限がきた場合の動作もミドルウェアによって違う
3.10 さまざまなリクエスト
- 部分取得リクエストRFC 7233
- Rangeヘッダを利用する
- 条件付きリクエストRFC 7232
- キャッシュのTTLが切れた後のリクエストやリロードを行う際に、自動的に送信される
3.11 さまざまなヘッダ
Vary-RFC 7234
-
同じURLであったとしてもサーバーが違うコンテンツを返すことがある
-
代表例はコンテンツの圧縮(gzip)/無圧縮
-
Axxept-Encodingで地震が解釈できるエンコードをクライアントは列挙する
- accept-encoding: gzip,deflate,br(brotli)
3.12 HTTPヘッダの不適切な設定で起きた事例
- Cache-Controlが未定義
- なにが問題なのか?
- 通常サイトの負荷が高まるタイミングはコンテンツの更新をおこなったとき
- TTLが想定以上に長くなってしまうこともある
- Cache-Controlが未指定だと、そもそも干渉が難しいクライアントのキャッシュ管理がさらに何もできない状態になってしまう
- 動作を把握・管理するためにも、明示的に指定すべき
- 意図せずキャッシュされ問題がおこることもありえる
- なにが問題なのか?
- コンテンツが更新されていないにもかかわらずETagが変わる
- Etagはコンテンツが最新かどうかを問い合わせを行うために使う
- 環境によってはファイルに変更がないのに頻繁にETagが変わるという事態がおこる
- ミドルウウェアのETagの生成ルールへの理解不足と、ロードバランサー配したの複数サーバー設置に起因することが多い
3.13 ヘッダクレンジング
- 汚れたヘッダをヘッダをきれいにする
- 配信に影響をおよぼす設定
- コード上のheader関数で生成
- ディレクトリの.htaccessの設定
- サーバーのミドルウェア(Apacheなど0の規定の動作など
- どんなサイトにおいても変わらないところは出入り口(gateway)
- どんな構成であったとしても、クラおあんとがアクセスしてくるゲートウェイが存在する
- ゲートウェイでヘッダをきれいにしてしまえば、中の設定で多少まずいものがあったとしても、外に出ていくヘッダはきれいなものになります
- ヘッダクレンジングで意識すべきことは、ブラウザやCDNなど、ゲートウェイから見たクライアントに勘違いさせないヘッダを出力すること
- 最低3種類
- キャッシュさせたくない時のCache-Control
- ローカルのみにキャッシュさせたい場合のCache-Control(privateを指定)
- ローカル+系路上にキャッシュさせたい場合のCache-Control
- クライアントを混乱させることが多いレスポンスヘッダの対処策として、場合によってはオリジン側の設定変更で対応する
- Set-Cookie
- Age
- ETag
- Vary
- 注意すべきリクエストヘッダ(クライアントからの)
- Cookie
- Authorization

3.14
- gzipで配信ファイル(HTML/CSS,JS)を圧縮
- クライアントによってgzipに対応していない場合もある
- Varyヘッダをgzip_varyをつけることで有効化する
- ストレージサービスは圧縮転送漏れがある
- S3はクライアントの要求によって圧縮転送したり、圧縮ファイルをそのまま転送したりするようなものは少ない
- GCSには事前に圧縮ファイルを置いておくと、基本は圧縮転送/クライアントが圧縮転送に対応していないとき解凍して転送する機能がある
- ただし、無圧縮でファイルアップロードしても、圧縮などの面倒は見てくれない
Colmn 圧縮は万能ではない
- 画像フォーマットはすでになにかしら圧縮されているため圧縮に含めないほうが良い
- 圧縮にはCPUを使うので、最初から不要なものは対象から外したほうが良い
- フォーマットですでに圧縮されているようなものは対象から外す
- 圧縮する際の最小サイズを指定する
3.15
適切なメディアの選択によるコンテンツの改善
- ユーザーの回線品質を考慮する
- トップページと詳細ページで表示している画像が同一というサイトがあった
- このサイトでは、同一の画像をCSSの指定で小さくしていた
- 一覧表示ではページ1つを表示するyのに83.6MBもの通信が発生していた
- ユーザーの実環境(4G)に近い環境でテストをすると、トップページのファーストビューが 表示されるまでにかなり待たされる、などの問題に気がつくことができたはず
- 結果、トラフィックコストの削減・表示速度の向上が見込める
- 適切な画像サイズを見極める
- 適切な画像フォーマットを見極める
- (すでに解説している)キャッシュをうまく使う
- このケースではサムネイルを別に作ることで一気にサイトのトラフィック削減ができた
- 毎回サムネイルを手作業で作るのは大変なので、リクエストがあったタイミングでサムネイルを生成するサービスを使うの
- 自前で構築するのもあり。AWS API gateway/ALB+Lambda などのサービスを組み合わせる構成がとれる
- bppを考える
- 用途に応じて画像を切り出し分割することで総ファイルが削減できることもある
- 画像フォーマットは適切かを考える
- 非科学圧縮のファイルサイズ効率を考える
- JPEGでは許せる範囲 でqualityを下げる
- SSIM 元画像と比較してどの程度似ているかを比較し、数値化する方法
- JPEGはプログレッシブJPEGをなるべく使う
3.16 問題点を調査する
圧縮転送が有効になっていない
ETagに問題があるケース
Last-modifiedに問題があるケース
画像サイズが大きい
- 通常のサイトであれば300KBを超える画像、画像主体でも1MBを超えるものがあればチェックする
- ローカルにコンテンツリソースのフルセットがあったり、サーバーにアクセスしてコマンドをたたけるのであればアクアセス前にそれでファイルサイズを検索する
- 開発者ツールではサイズ順ソートが見やすい
キャッシュが有効に使われていない
- 開発者ツールでキャッシュから読み込んだことを確認する(chromeではdisc cache)
- キャッシュが有効化確認するにはステータスコードを確認する
- 304が頻発する->ドメインやサイズ(転送量)フィールドでソートをかけ、何をキャッシュしている/していないを把握する
- CSS/JS/画像といった静的コンテンツがキャッシュで返されているかに注目する
- 動的なコンテンツのキャッシュは難しくても性的コンテンツのキャッシュは行いやすい
- もしほとんどキャッシュされていなかったり、304ばかりが返ってきたりするのであれば改善できる可能性がある