Webアプリケーションにおけるキャッシュ戦略¶
チェック¶
- [ ] 本文を確認した
- [ ] 概要を確認した
- [ ] タグを確認した
- [ ]
inbox/直下へ移行した
概要¶
Web アプリケーションにおけるキャッシュを、アプリケーション側で作るキャッシュと HTTP レスポンス全体のキャッシュに分けて整理した資料。 キャッシュは高速化に有効だが、不整合、障害点、キー設計ミス、Thundering herd problem、原因究明の難しさを持ち込む。 まずキャッシュを使わない改善を検討し、必要な場合に TTL、保存先、キー設計、更新方法、監視、HTTP ヘッダー、CDN/Proxy の挙動を設計する。
本文¶
資料は、ISUCON 本のキャッシュ章をベースに、Web アプリケーションでキャッシュをどう考えるかを整理している。 扱うキャッシュは大きく2系統。
- Web アプリケーション側で作ったキャッシュをミドルウェアなどに保存して利用する方法。
- HTTP レスポンス全体をクライアント、Proxy、CDN などにキャッシュさせる方法。
最初に考えること¶
キャッシュは高速化に有効だが、まず使うものではない。 最初に検討するのは、キャッシュなしで問題を解けないか。
スロークエリなら SQL、index、データ量、JOIN、取得範囲を見直す。 外部 API が遅いなら、本当にその API に頼る必要があるか、呼び出し回数を減らせないか、非同期化できないかを見る。
静的ファイル配信や HTTP cache のように最初から cache 前提で考える領域はあるが、アプリケーション内部の cache は複雑性を増やす。 必要性と許容できる不整合を見て導入する。
キャッシュの効果と難しさ¶
効果は明確。 DB や外部 API へのリクエスト数を減らせる。 レスポンスを高速化できる。 サーバー負荷とインフラコストを下げられる。 外部 API の rate limit 回避にも役立つ。 HTTP cache が効くと、静的ファイルの転送量と latency を大きく減らせる。
一方で難しさもある。 古いデータを返す。 不整合が起きる。 キャッシュミドルウェア、容量、再起動による揮発など、障害点が増える。 キー設計ミスはバグや情報流出につながる。 原因究明も難しくなる。 さらに cache miss が同時多発すると、Thundering herd problem が発生する。
導入判断では、次を考える。
- 不整合がどこまで許されるか。
- 決済や権限など致命的なデータではないか。
- ユーザーごとに分散しすぎて cache hit しないデータではないか。
- 更新頻度が高すぎないか。
- 生成コストが高すぎないか、逆に安すぎて cache する意味が薄くないか。
TTL¶
TTL は長いほど cache hit しやすく高速化効果が大きい。 しかし、その間は更新が反映されない。
対応は主に2つ。
- データ特性を見て十分短い TTL を設定する。
- 更新時に cache も同時に更新する。
資料では、基本的には短めの TTL を推奨している。 更新と cache の同時更新は、実装が複雑になりやすい。 DB 更新と cache 更新の順序、失敗時の rollback、不整合、retry、race を考える必要がある。
保存先: memcached、Redis/Valkey¶
キャッシュ用ミドルウェアに必要なのは、key から value を取得できる KVS と、TTL による expire。
memcached は高速で機能が少ない。 永続化や replication は基本的に想定されない。 消えても困らない cache のための道具として割り切る。
Redis/Valkey は高速で機能が多い。 永続化、replication、cluster 構成も可能。 ただし、単純な GET/SET 以外の重い command で全体が block する可能性があり、運用上の注意が必要。
依存ミドルウェアを増やすほど障害点は増える。 導入後は、本当に performance に寄与しているかを測る必要がある。
インメモリ・ファイル cache¶
補助的に、アプリケーション process 内の memory や file に cache する方法もある。 ただし注意点が多い。
Go で並行に読み書きするなら sync.Mutex などの lock が必要。
multi-process 構成では process 間で共有できない。
TTL を自前実装する必要が出ることもある。
また、deploy 直後や server 追加直後は cache が空になるため、負荷集中や performance 劣化を招く。 問題のあるデータを cache したときに、簡単に全 server から消せないこともある。
基本は専用 middleware に保存し、in-memory/file は middleware への request を減らしたいなど明確な理由がある場合に補助として使う。 補助 cache の TTL は短めにする。 middleware 側 TTL と補助 cache TTL が合算され、古い値が長く残りすぎることがある。
キー設計¶
cache key は混同を防ぐ必要がある。 たとえば user ID と item ID が同じ数字でも、別の意味を持つ。
区切り文字と prefix で意味を分ける。 また、同じデータを大量の異なる key で持たないようにする。 1対多に増殖していないかを見ないと、容量枯渇や invalidation 困難につながる。
キー設計ミスは、単なる performance 問題ではなく、他ユーザーの情報を返すような情報漏えいにもつながる。
cache-aside¶
アプリケーション側の基本形は、cache にデータがあれば返し、なければ重い処理を実行して cache に保存し、その結果を返す方式。
メリットは実装が単純で、アクセスされたものだけ cache するため効率がよいこと。 デメリットは、初回 request や cache 消失時に遅いこと。 cache miss のタイミングで大量 request が来ると、同じ重い処理が同時に走る。
古い値を返して非同期更新¶
別の方法は、cache がなければ default value や古い cache を返し、裏で非同期に cache 更新すること。 多くの request では高速に返せる。 アクセスがあったものだけ生成する点も効率的。
デメリットは、最初に request したユーザーには適切な値を返せない場合があること。 また、非同期実行基盤が必要になり、ロジックが複雑になる。 Thundering herd problem 自体は別途対策が必要。
Go なら golang.org/x/sync/singleflight で、同じ key に対する同時生成を1つにまとめられる。
バッチ更新¶
バッチで定期的に cache を更新する方法もある。 実装は比較的単純で、request 時の cache miss による Thundering herd problem は起きにくい。
一方で、バッチで生成できるデータにしか使えない。 アクセスされないデータまで生成すると無駄が増える。 cache が障害で揮発したときに、次の batch まで復旧できない場合もある。
Thundering herd problem¶
cache がない、または作成中の間に、大量 request が同じ重い処理を同時に実行する問題。 取得元の DB/API が高負荷になり、サービス継続が難しくなることがある。
緩和策は、更新処理の同時実行を抑制すること。
Go の singleflight、または条件が合えば nginx の proxy_cache_lock のような経路上の cache lock を使う。
監視¶
アプリケーション側 cache で見るべき metrics は主に2つ。
- expire していないのに削除された数。
- cache hit ratio。
memcached なら evicted items、Redis/Valkey なら evicted keys のような metrics を見る。 expire 前に削除が多いなら容量不足で cache が機能していない可能性が高い。 hit ratio が低いなら、対象、TTL、key 設計が適切でない可能性がある。
hit ratio は変更で急変するため、継続監視が必要。
HTTP cache¶
画像、CSS、JavaScript のように更新頻度が低く再利用される静的ファイルは、HTTP cache を活用する。
初回 request では通常 response を返し、Last-Modified や ETag を client に渡す。
期限切れ後、client は If-Modified-Since や If-None-Match を付けて再 request する。
変更がなければ 304 Not Modified を返し、body 転送を省ける。
変更があれば新しい content と新しい validator を返す。
Cache-Control で有効期間を指定する。
ファイルを更新するときは、ファイル名を変えるか query string を変え、古い cache が使われないようにする。
nginx で静的ファイルを配信する場合、expires 1d; により Cache-Control: max-age=86400 を返せる。
nginx の file 配信では Last-Modified や ETag も自動付与される。
CDN/Proxy cache¶
CDN 上に HTTP response を cache すると、client に近い edge から返せる。 海外配信の安定性や性能を改善しやすく、急な traffic 増にも従量課金で対応しやすい。 アプリケーションサーバーへの request 数を減らし、cost も下げられる。
ただし事故リスクも高い。 CDN の挙動に詳しくないと、ユーザー固有 response を cache して情報漏えいする可能性がある。 アプリケーション設計も、cache しやすい response を返すようにする必要がある。 各 CDN/Proxy の挙動は違うため、ドキュメント確認と実機検証が必須。
nginx の proxy_cache は安全側に倒れており、Set-Cookie を含む response はデフォルトで cache しない。
本来 cache 可能な response はユーザー固有ではないはずなので、不要な Set-Cookie が出るならアプリ側を直す。
Cache-Control は機能が多く複雑で、実装が仕様どおりとは限らない。
今後は browser 向け Cache-Control と CDN 向け CDN-Cache-Control を分ける方向も考えられる。
要点¶
- キャッシュは「まず使う」ものではなく、必要なら使うもの。
- キャッシュなしで SQL、外部 API、設計を改善できないかを先に見る。
- アプリケーション側 cache と HTTP response cache は分けて考える。
- TTL は短めにし、更新同時 cache 更新は複雑性を理解して使う。
- memcached は消えてよい cache、Redis/Valkey は機能が多いが運用注意。
- key 設計ミスは情報漏えいにつながる。
- cache miss 同時多発は Thundering herd problem を起こす。
- 監視は evicted items/keys と hit ratio。
- CDN/Proxy cache は強力だが、ユーザー固有 response を cache しない設計と検証が必須。