Web配信の技術(個人メモ)

第2章:配信の基礎
配信の捉え方
配信はケースごとに注意するべき点が異なる。
- ライブ配信であればリアルタイム性
- VODであれば高トラフィックを捌く技術
どちらも、サーバからクライアントにコンテンツを届けるという根底は変わらない。
標準仕様でやり取りする
どんな配信も何らかの標準仕様の上に成り立っている。
配信最適化に必要な視座。
- どんな配信も究極的にはクライアント/サーバ間のコンテンツのやり取り
- 土台の共通部分(標準仕様)と要件に応じて追加される部分がある
- 共通部分を正しく使うことと、要件を整理し構成や機能を考える必要がある
配信を高速なものにするために
配信最適化によって実現したいこと
- クライアントがコンテンツを高速にダウンロードできる
- 突発的なリクエスト増に耐える
- 低コストで実現する
配信経路の最適化
距離の最適化という文脈では、「サーバ側がクライアント側に近づく」ことが一つの答え。
- 経路上にCDNを挟んでキャッシュしたコンテンツを返す
クライアントとサーバでの最適化
前提として、クライアントはブラウザだけではない。
オリジンとクライアントの間にCDNがあれば、CDNはサーバにもクライアントにもなる。
最速の配信はクライアントがローカルに持っているキャッシュを使うこと。
キャッシュの格納場所による分類
- クライアント側のキャッシュ
- ブラウザキャッシュ
- 配信経路上のキャッシュ
- CDNでのキャッシュ
- オリジンのキャッシュ
- Proxyのキャッシュ
ブラウザキャッシュ
ローカルなキャッシュ。
CSSやJS、画像などをキャッシュすることで共通するコンテンツを毎回ネットワーク越しに取りに行く必要がなくなる。
近年はServiceWorkerなどによって細かく柔軟に制御できるようになってきた。
経路上のキャッシュ
オリジンの負荷軽減の目的に加えて、経路の最適化や大きな配信帯域の確保などのケースも考えられる。
オリジン側のキャッシュ
主にProxyを用いてキャッシュする。
インターネットに繋げるゲートウェイ部分でキャッシュすることで、キャッシュの一貫性を維持しやすいなどのメリットがある。

第3章:HTTPヘッダ設定とコンテンツの見直し
HTTPヘッダの重要性
HTTPはサーバ・クライアントモデルを採用し、クライアントからのリクエストに対して、サーバがレスポンスを返すという構造。
このとき、HTTPメッセージを元にやり取りする。
HTTPメッセージは開始行、 HTTPヘッダ、HTTPボディの3つから構成される。
- HTTPヘッダにはHTTPのやり取りそのもののメタ情報が記載される
- 特にサーバからクライアントに送信されるレスポンスヘッダは重要
- どのようなコンテンツをどのように取り扱うかというメタ情報が入っている
HTTPとキャッシュ
HTTPのレスポンスはいくつかの要件に合致していれば設定なしでもキャッシュされるため、キャッシュを防ぎたい場合はCache-Controlで制御する必要がある。
キャッシュを行う条件
- リクエストメソッドが解釈できるもので、かつキャッシュ可能なメソッドとして定義されている
- その使用上にある指定されたキャッシュに関する動作を全て実装している状態
- ステータスコードが解釈できるもの
- リクエスト・レスポンスのCache-Controlにno-storeが含まれていない
- 経路上のキャッシュ(shared)として格納しようとしている際、レスポンスのCache-Controlにprivateの指定がないこと
- 経路上のキャッシュ(shared)として格納しようとしている際、リクエストにAuthorizationヘッダが含まれていないこ
- レスポンスで以下の条件のうちどれかを満たす
- Expiresヘッダを含む
- Cache-Control内にmax-ageを含む
- 経路上のキャッシュ(shared)として格納しようとしている際、Cache-Controlにs-maxageを含む
- 拡張ディレクティブで明示的に許可されているもの
- ステータスコードがデフォルトでキャッシュ可能なもの
- Cache-Control内にpublicを含む
キャッシュを使う条件
- リクエストとキャッシュのURIが一致する
- キャッシュ格納時のメソッドがリクエストのメソッドで使えることを許容している
- キャッシュ中にVaryでヘッダが指定されている場合は、キャッシュ中のセカンダリキーとリクエストの指定ヘッダの値が一致すること
- リクエスト中のCache-ControlかPragma内にno-cacheを含む場合はキャッシュの検証が成功していること
- キャッシュ中のCache-Control内にno-cacheを含む場合はキャッシュの検証が成功している
- キャッシュの状態が以下のどれか
- Fresh
- Staleでの利用が許容されている
- 検証が成功している
Cache-Controlによるキャッシュ管理
Cache-Controllはコンテンツの有効期限など、キャッシュポリシーを設定するヘッダである。
キャッシュの保存方法を指定
未指定 or public or privateが存在する。
注意として、publicはsharedキャッシュにprivateはprivateキャッシュに格納するだけの指定ではなく、通常キャッシュができない場合でもsharedキャッシュ、privateキャッシュに格納するという強い指定であること。
未指定でもキャッシュは行われる。実質、sharedキャッシュとして扱われる。
Cache-Control:publicの指定はsharedキャッシュとして扱いつつ、通常キャッシュができない場合でもキャッシュを行う指定となる。しかし、これは危険な設定で注意が必要。sharedキャッシュとすることだけのためにpublicを指定するのは間違い。そもそも未指定ならsharedキャッシュになる。
sharedキャッシュに格納するだけなら未指定のままで問題ない。privateキャッシュに格納する場合のみprivateを指定する必要がある。
キャッシュの使われ方を指定する
no-storeはキャッシュが行われる際に評価されるディレクティブで、キャッシュを格納してはならないという指定。キャッシュさせたくない場合はレスポンスヘッダに必ず指定する。
no-cacheはキャッシュを使う際に評価される。正確には、キャッシュを使う際にはオリジンへ問い合わせを行い、そのキャッシュが有効でない限り使用してはいけないという指定。
- 頻繁に更新されるコンテンツに設定すれば、毎回更新を確認し更新がなければキャッシュが使用され、更新があれば再取得する
キャッシュの更新の方法を指定する
キャッシュには期限を設定する。
no-cacheは毎回再検証が必要なオプション。
それに対して、must-revalidate・proxy-revalidateは期限切れ時に再検証を強制するオプション。
そのため、max-ageとno-cacheは共存できないが、must-revalidate・proxy-revalidateは共存できる。
Cache-Controlにおける期限指定
キャッシュの期限と状態
キャッシュの有効期限のことを一般的にTime-To-Live(TTL)という。
キャッシュには複数の状態が存在する。
- 新鮮(TTLの期限内)でそのまま使えるFresh
- 検証が成功してキャッシュが有効な状態
- 期限切れだが条件付きで使えるStale
- 検証が無効な状態
またTTLは検証結果の有効期間とも言える。
キャッシュは指定した期間保存されるとも限らない。
また、キャッシュが切れたからといってすぐに消されるわけではない。ストレージに余裕があればStaleキャッシュの再利用に備えて、キャッシュを保持することが多い。
- max-age
- オリジンがレスポンスを生成した時刻を起点とした相対的なキャッシュの期限
- s-maxage
- 経路上のキャッシュにのみ有効なmax-age
- stale-white-revalidate
- 期限切れ痔にバックグラウンドで再検証を行う間に古いキャッシュを使うことを許容する期限
- stale-if-error
- 期限切れ時の再検証でオリジンがダウンしていた場合に古いキャッシュを使うことを許容する期限
Cache-ControlとExpiresヘッダ
Expiresヘッダは値に時刻を入れることで期限切れを絶対時間で指定できる。
- グリニッジ標準時(GMT)以外のものが指定された場合は無効な値として扱われる
- 基本的にはmax-ageを優先するべき
- max-ageと同時に指定された場合はmax-ageが優先される
様々なリクエスト
部分取得リクエスト
大きなファイルのDLをレジュームしたり、長い動画を途中から再生する場合にファイルを先頭から一括取得せず、途中からDLする場合、Rangeヘッダを使う
Range: 1001-2000
- 206 Partial Contentを返す
条件付きリクエスト
ロゴ画像のように各ページで高頻度に表示されるコンテンツが存在する。これはCache-Controlをうまく使い、1回の滞在中はブラウザキャッシュで再リクエストを防ぎたい。
滞在中には十分なTTLを指定しても、一度サイトを離れ再訪したときにはTTLが切れていたり、キャッシュ自体が消えていたりすることもある。
運よく期限切れキャッシュが残っていたら再検証することで利用可能。
再検証を効率化するために使われるのが条件付きリクエスト。
- If-Modified-Sinceヘッダ
- Last-Modifiedであれば最終更新日から更新があったらコンテンツを送るようにリクエスト
- If-None-Matchedヘッダ
- ETagであればこのエンティティタグと一致しなかったらコンテンツを送るようリクエスト
どちらもコンテンツに更新がなければ304 Not Modifiedを返却し、ボディは何も返さない。
304が返ってくれば、保持しているキャッシュを使うだけ。
適切なメディアの選択によるコンテンツの改善
配信システムとファイルサイズ
- 適切な画像サイズを見極める
- 適切な画像フォーマットを見極める
- キャッシュをうまく使う
問題点を調査する
- 圧縮転送が有効になっていない
- Content-Encodingヘッダを確認
- ETag/Last-Modifiedがおかしい
- 画像コンテンツがすごく大きい
- 300KBを超える画像、画像主体でも1MBを超える場合はチェックするべき
- キャッシュが有効に使われていない

第4章:キャッシュによる負荷対策
経路上のキャッシュをうまく使うには様々な点の検討が必要。
- キャッシュキーの適切な取り扱い
- 動的コンテンツと静的コンテンツの違いについて
- Varyの取り扱い
- TTLの取り扱い(これはローカルでも同様)
- キャッシュの消し方
負荷を捌くこととキャッシュ
負荷を捌くとは「リクエストで使用する様々なリソースのキャパシティを超えないようにできている状態」をいう。ユーザがコンテンツを快適に受け取ることは必須条件で、それだけでなくシステムが想定通りに安定的に動作していることを重要視するべき。
キャッシュを使わない場合はどう捌くか
- 想定される流入に対して捌けるだけのリソースを準備する
- 流入が様相以上だった場合はオートスケールする
- 実際はオートスケールするのに分単位の時間がかかる
キャッシュを使う
負荷を捌くのに重要なのは、スケールアウトの基準に達してから実際にスケールアウトされるまでの時間をいかに稼ぐかということ。
経路上のキャッシュはアクセス増への対策として最も有効なものの一つ。スケーリングはリソースを増やすことで負荷に耐えるが、キャッシュは負荷そのものを軽減できる。
ProxyやCDNはなぜ大量のリクエストを捌けるのか
多くのAppサーバはアクセスを捌くための効率的な設計をしていない。
CDNはコンテンツをより早く効率的に配信するために構築されたネットワーク。
キャッシュの役割
- AppサーバやDBサーバなどリソースの負荷を全体的に低減する
- スケーリングの時間を稼ぐ
- 初期流入を吸収でき、負荷の伸びが緩やかになる
- コスト削減
- 静的ファイルのキャッシュは、オリジンと離れた場所でそのコピーを持つことができる
キャッシュ事故を防ぐ
- 誤ってSet-Cookieをキャッシュしてしまい、キャッシュされたページにアクセスしたユーザのログイン状態が共有されてしまって個人情報の漏洩
- Cache-Controlの不備とCDN側のCache-Controlの解釈で致しないキャッシュがされてしまう
これらは以下の原因が考えられる
- 中間(Proxy/CDN)を信頼しすぎる
- ステータス200であればCache-Controlを見なくてもキャッシュしてしまうようなCDNもある
- オリジンを信頼しすぎる
キャッシュ戦略・キャッシュキー戦略
重要なのは、1つのキャッシュするオブジェクトに対して複数の意味を持たせないこと。
動的なサイトでのキャッシュキー戦略
- フェイルセーフである
- 致命的な障害を確実に防ぎ、安全側に倒す
- オリジンを信用しない
- 基本的に許可リスト(Allowlist)で行う
- 確実にキャッシュされない方法を調べる
キャッシュのTTLとキャッシュを消す
キャッシュのTTLを指定する方法は2種類
- max-age=3600のような相対的な指定
- expires:Mon, …のような絶対的な指定
TTLが切れたキャッシュの状態をStaleという
これが存在する理由として、期限切れでも有効な使い方があったり。即時に消すのはコストが高いという理由がある。
TTL=0(max-age=0)とは何か
Stale状態でキャッシュを行うことで、キャッシュをしないという指定ではない。
no-cacheとの違い
- no-cacheは再検証が成功しないと再利用してはいけない点で異なる
- no-cache = ( max-age=0, must-revalidate )
TTLの決め方
TTLの指定はローカルキャッシュ、経路上のキャッシュで考えが変わる。
ローカルキャッシュは可能な限りボディ転送を減らしてできればリクエストを行わないことが目的
- 単一のクライアントからどの頻度でアクセスが来るかをベースに検討
経路上のキャッシュはオリジンへのリクエスト数減が目的
- 次のリクエストまでにキャッシュが残っていればいい
max-agemはローカルと経路上共通のTTLなので、経路上でTTLを消費するとローカルに残らずリクエスト数が増えてしまう。
- Ageヘッダを消す
- s-maxageを活用する
- 経路上とローカルで使えるTTLを使い分けられる
キャッシュの削除
コンテンツリソースを更新してすぐに入れ替えたいケースなど。
クエリ中に更新日時などを入れ、URLを変更することでキャッシュキーを変えるCache Bustingという手法。
無効化と削除
キャッシュの無効化は当該キャッシュをStale状態にすること
- 条件付き問い合わせ後、オリジンのコンテンツに変更がなければStale状態のキャッシュはFleshになる。
キャッシュの削除は、そのものを削除すること

第5章:より効果的・大規模な配信とキャッシュ
キャッシュをうまく使えているかの指標として、ヒット率が用いられる。
キャッシュヒット率=キャッシュヒット/(キャッシュヒット+キャッシュミス)
どのようにリクエストを処理してレスポンスするのか
ProxyやCDNの動作には一連の流れがある。
- クライアントからリクエストを受ける
- キャッシュがなければオリジンにリクエストをする
- オリジンからレスポンスを受ける
- クライアントにレスポンスを行う
自身を中心として、受信する場合はRx(Receiver)、送信する場合はTx(Transmitter)と表記
- キャッシュあり
- RxReq: クライアントからをリクエストを受け取る
- Cache lookup: キャッシュを探して(Lookup)見つかった(Hit)
- TxResp: ストレージからオブジェクトを読み出して(Read cache)レスポンスする
- キャッシュなし
- RxReq: クライアントからをリクエストを受け取る
- Cache lookup: キャッシュを探して(Lookup)見つからなかった(Miss)
- Wait for cache: キャッシュストレージにオブジェクトが格納されるのを待つ
- TxReq: オリジンにオブジェクトを取得しに行く
- RxResp: 取得したオブジェクトをストレージに格納する(Write cache)
- TxResp: ストレージからオブジェクトを読み出して(Read cache)レスポンスする
RxReqークライアントからのリクエストを受信する
RxReqの次であるCache Lookupに影響するキャッシュキー操作やセカンダリキー関連の操作はRxReqで処理する。
- キャッシュキーに使われるようなホストやURLパスの編集
- Cookieなどから候補キーとなる項目を抽出してキャッシュキーへの追加
RxReqはクライアントからのリクエストを受けた際のイベントのため必ず呼ばれる。
そのため前章で触れたようなCookieの削除や、この後のヒット率を上げるで触れるクエリソートなどのリクエストに対するヘッダ・パスなどの操作を行うのに適している。安全のために確実にフィルタするには、入口のRxReqで処理を行うのが鉄則
キャッシュ可否の判定
通常二回行われる。
- RxReq
- 特定のパス以下をキャッシュするしない
- 特定のヘッダを含む場合はキャッシュしない
- RxResp
- Cache-Controlに従いキャッシュをしない
- エラーレスポンスなのでキャッシュしない
基本定期にCache-Controlやエラーレスポンス以外はRxReqに寄せるべき
Cache Lookupーキャッシュヒットの判定を行う
判定の流れ
- キャッシュキーから一致するキャッシュを選択する
- 選択されたキャッシュのヘッダにVaryが含まれている場合(Vary: Accept-Encodingなど)
- リクエスト中の対応するヘッダフィールド(Accept-Encoding)の値とキャッシュのセカンダリキーが一致するキャッシュを選択する
- 一致すればキャッシュヒット、不一致ならキャッシュミス
Wait for cacheーキャッシュができるまで待つ
キャッシュストレージにオリジンからのレスポンスが格納されるまで待機すること
mypageのように最初からキャッシュできないとわかっている場合はRxReqで指定して回避できる
TxReqーオリジンにリクエストを送信する
キャッシュミスしてProxyやCDNからオリジンに問い合わせを行う必要がある際のイベント
キャッシュキーに影響を与えないオリジン問い合わせをする場合はTxReqで行う
RxRespーオリジンからレスポンスを受信する
オリジンからレスポンスが返ってきたタイミングのイベント
- キャッシュするオブジェクトに対する操作
- Set-Cookieの削除
TxRespークライアントにレスポンスを送信する
キャッシュからオブジェクトを取得し、クライアントにレスポンスする直前のイベント
ここでは最終的なヘッダの操作をする
キャッシュキーをVaryで代用する
同一のURLに対してクライアントによって出したい内容が異なる場合がある。
- Vary(+キャッシュキー)で処理する
- キャッシュキーで内部的に処理する
Varyの利用例
- 同一URLで、ユーザーの選択した言語によって/asset/以下のパスで出しわけを行いたい
- 言語の情報はCookieのlangというパラメータが入っている(ja/en)
- 言語ごとにパスを変更する
- /asset/img/foo.jpg -(jaの場合)→ /ja/asset/img/foo.jpg
- ユーザーが言語をメニューから変更したら即適用できるようにしたい