Claude が書いた記事
キャッシュは実際どこに置かれてるか ── 自前ディスクのハッシュ名と、Redis に保管を肩代わりさせる構成
キャッシュってファイルで保管されてるん? という素朴な疑問から、ディスクに「ハッシュ名 + 索引」で置く自前型と、Redis にまるごと肩代わりさせる型の 2 つを並べて、Redis をキャッシュに使うときの定番構成 (Cache-Aside・同居 vs 中央共有・TTL と押し出し) まで辿った学習ログ。
俺の最初の疑問
前に Front Door のオブジェクトチャンキング を調べてて、「大ファイルを 8 MB のチャンク単位でキャッシュする」という話に行き着いた。そこで素朴に詰まった。
そのキャッシュって、結局どこに・どんな形で置かれてるん? ファイルなんか?
そしてもう一つ。キャッシュの保管といえば Redis という名前をよく聞く。あれを使うとき、みんなだいたいどう組んでるんや? という定番の構成も知りたかった。
まず一言でいうと
キャッシュの実体は、ディスク (ファイル) か RAM (メモリ) のどっちか。速くて消えやすいのが RAM、遅いけど大容量で消えないのがディスク。そして大事なのは、どっちに置くにしても、URL をそのまま置くんやなくて、間に必ず「変換」が挟まること。
- 自前ディスク型 … URL をハッシュ化した名前のファイルとして置く。例:
/video.mp4→a1b2c3d4...という名前のファイル。 - Redis 型 … 保管も変換も Redis という別ソフトに丸投げする。Web サーバー側は
cache:/video.mp4みたいなきれいなキー名でデータを指すだけ (読むときはGET キー、書くときはSET キー 値と、コマンドにキーを乗せて送る)。ハッシュ化も置き場所も Redis 任せ。
出発点はどっちも URL。違うのは「自分でディスクに置いて索引も自分で持つ」か、「Redis に置き場所ごと肩代わりさせる」かや。
何と比べるとわかるか
2 つの保管モデルを並べると、役割分担の差がはっきりする。その前に登場人物を 1 つ ── 自前ディスク型でキャッシュを持つ役は、前段に立つリバースプロキシや CDN エッジ (Front Door のような前段の中継役) や。以下これをプロキシと呼ぶ。一方 Redis 型でキャッシュを叩くのは Web サーバー (アプリ) 側になる。
| 自前ディスク型 (CDN エッジ等) | Redis 型 (インメモリ KVS) | |
|---|---|---|
| 置き場所 | ローカルディスク / SSD | 全部 RAM (メモリ) |
| 名前 / 鍵 | URL をハッシュ化した不可逆なファイル名 | Web サーバーはきれいなキーのまま (cache:/video.mp4) |
| ハッシュ化を誰がやるか | 自分 (プロキシ) がやる | Redis が裏でやる。Web サーバーは気にしない |
| 索引 (どこにあるか表) | 自分でメモリに持つ (URL → ハッシュ) | Redis 内部のハッシュテーブルが索引そのもの |
| 消えたら | 索引は Miss で再構築 or 永続化で復元 | Redis が裏でディスクに書いて再起動後に復活 |
| 向いてる規模 | 巨大ファイルの配信 (CDN) | アプリのデータ・セッション・小さめの値 |
ざっくり言うと、自前ディスク型は「保管も索引も DIY」、Redis 型は「保管屋に外注」。次の節からは、まず DIY 側の「ハッシュ名で置いたら、どうやって URL から探すんや?」という引っかかりを潰す。
何が問題なのか ── ハッシュ名は不可逆。じゃあどう探す?
自前ディスク型で、まず引っかかったのがここ。ファイル名が a1b2c3d4... というハッシュ値なら、そのファイル名から元の URL は復元できへん。ハッシュ関数 (MD5 / SHA-256 等) は一方向で、戻せないのが仕様や。
ここで誤解しやすいけど、そもそも「ファイル名から URL を復元」する必要はない。やりたいのは逆や。
ユーザーが
/video.mp4を要求してきた → このファイル、キャッシュにあるか?
つまり手元にあるのは常に URL の方。だから URL をその場でハッシュ化して a1b2c3d4... を計算し、「その名前のファイルがディスクにあるか」を見ればいい。これは順引き (URL → ハッシュ) であって、逆引き (ハッシュ → URL) やない。
そのために、プロキシは「URL → ハッシュ名」の対応表をメモリに索引として持つ。リクエストが来たら索引を引いて、ヒットすればディスクのファイルを返し、無ければオリジンに取りに行って同じハッシュ名で保存する。
ここで出てくる「索引 (インデックス)」という言葉に、もう一つ引っかかった。「メモリ上の索引」と言うけど、それをディスクに保存したら索引やなくなるんか? と。
答えはノー。索引は置き場所の名前やなくて、「目的のデータを速く見つける仕組み」の名前。本の索引と同じで、巻末に印刷されてても (ディスク = 消えない・遅い)、机に広げてても (メモリ = 速い・片付けたら消える)、どっちも索引や。役割は同じで、速さと永続性が違うだけ。
ついでに紛らわしいのが「索引」と「目次」。これは別物や。目次は本の前にあって、章が出てくる順に並んだ全体の地図。索引は本の後ろにあって、言葉 (キー) を引くとページ番号が分かる対応表。キャッシュで言う索引は、完全に後者 ──「URL (キー) → データの居場所 (ページ番号にあたる)」を引くための表や。
たとえ話 ── コインロッカーと、フロント預かり
2 つのモデルは、荷物の預け方で考えるとわかりやすい。
- 自前ディスク型 = コインロッカー。 自分で空きロッカーを選んで荷物を入れ、番号 (ハッシュ名) を覚えておく。どの荷物がどの番号かは自分のメモ (索引) で管理する。番号から中身は分からんけど、自分の荷物が何番かは分かってるから問題ない。
- Redis 型 = ホテルのフロント預かり。 荷物に名前 (キー) を付けてフロントに渡すだけ。どこにどうしまうかはフロント (Redis) が全部やる。受け取るときも名前を言うだけ。自分は棚番号を一切知らんでいい。
DIY で自由が利くのがコインロッカー、丸投げで楽なのがフロント預かり。キャッシュもこの 2 択や。
Redis をキャッシュに使うときの定番構成
ここからが二つ目の疑問。Redis をキャッシュに使うとき、現場でだいたい決まって出てくる組み方が 3 つある。Redis そのものの中身 (通信プロトコルやメモリ構造) には踏み込まず、「どう組むか」のレイヤーだけ見る。
その前に Redis を一言で。ここで 別々の 2 つを分けておきたい ── まとめて言うと「KVS やから速い」と誤解しやすいからや。
- 速さの正体は「メモリに置いてる」こと。Redis はデータを全部 RAM に持つ (インメモリ)。ディスクを読まんからマイクロ秒で返る。これは置き場所の話で、特別なハードやない ── ただの RAM や。
- KVS (Key-Value ストア) は「引き方」の名前。
キー → 値でしか引けへん、というアクセスの型のこと。速さともハードとも無関係で、RDB (テーブルを JOIN や WHERE で引く DB) と対比した分類名にすぎん。Redis でいえば、その「連想配列 (キー → 値 の表)」を自分のプログラムの中やなくて、外に別サービスとして立てて呼び出す形や (中の実装が何か ── ハッシュテーブルか木か ── や、どこに置いて何で繋ぐかは、KVS という分類とは別の話)。
念のため ── 同じ KVS でもディスクに置くやつ (DynamoDB 等) は数ミリ秒で、マイクロ秒やない。Redis が速いのは「KVS やから」やなくて「メモリに置いてるから」。そしてキャッシュは「この URL (キー) の値ある?」と聞くだけ ── キー一発引きしか要らんから、KVS の形がそのままハマる。メモリは高いので、何でも入れるんやなく「よく使う・速さが命のデータ」だけ置く。
ちなみに「キー」と聞くと施錠の鍵を思い浮かべるけど、KVS のキーはロックを開ける鍵やない。apple を引くと意味が返る辞書の見出し語や、番号を出すと荷物が返るコインロッカーの札みたいに、「それを言えば値が返ってくる名前・目印」のことや。英語の key が「鍵」と同じ綴りなだけ。
① Cache-Aside ── 一番よく見る基本形
Cache-Aside (キャッシュアサイド / 別名 Lazy Loading) は、Redis キャッシュの最頻出パターン。アプリが自分でキャッシュを見に行き、無ければ元データから読んで自分で書き戻すやり方や。
- リクエストが来たら、まず Redis に
GET cache:/video.mp4で聞く。 - あれば (Cache Hit) … その値をそのまま返す。元データ (DB やオリジン) は触らない。爆速。
- 無ければ (Cache Miss) … 元データから読んで、ユーザーに返すと同時に Redis へ
SET cache:/video.mp4 <値> EX 3600で書き戻す (次回からはヒットする)。
ポイントは、Redis はあくまで「脇に置く (aside) 速い層」で、本当の正は元データ側にあること。Redis が落ちても、Miss が増えて遅くなるだけでデータは失われへん。(Azure のアーキテクチャ集にも Cache-Aside パターンとして載ってる。)
② 同居 か 中央共有 か ── Redis をどこに置くか
Redis をどのマシンで動かすかで、定番が 2 つに分かれる。
- 同居 (小〜中規模) … Web サーバーと同じマシンに Redis を入れ、
localhost:6379(ループバック) で話す。ネットワークを跨がへんから速いし、構成も安い。難点は、同じマシンのメモリ・CPU を奪い合うことと、Web サーバーを増やしても各自バラバラのキャッシュになること。 - 中央共有 (中〜大規模) … Redis を独立したマシン (またはマネージドサービス) に分け、複数の Web サーバーから1 つの Redis を共通の保管庫として見る。Azure Cache for Redis や Amazon ElastiCache がこれ。Web サーバーを何台に増やしてもキャッシュは 1 つに揃う。難点はネットワーク越しのわずかな遅延とコスト。
Web サーバーを 1 台から複数台にスケールアウトする瞬間に、同居の弱点 (キャッシュがバラける) が効いてくる。だから「最初は同居、増えたら中央共有」が定石や。
③ あふれたら押し出す ── TTL と eviction
メモリは有限やから、何も考えずに入れ続けるといつか満杯になる。Redis キャッシュでは、これを 2 段構えでさばくのが定番。
- TTL で時間切れ失効 …
SET ... EX 3600のように鍵ごとに寿命を付ける。1 時間経ったら Redis が勝手に消す。古い情報を返さないための仕掛け。 - maxmemory + 押し出し (eviction) …
maxmemory 2gbで上限を決め、maxmemory-policy allkeys-lruを付けると、上限に達したとき一番長く使われてない鍵 (LRU = Least Recently Used) から自動で捨てる。これでメモリが溢れてクラッシュするのを防ぐ。(Redis 公式の eviction の説明。)
キャッシュは「消えても困らない」前提のデータやから、あふれたら古いものから容赦なく捨てるのが正しい。TTL が「鮮度切れで捨てる」、eviction が「場所が無いから捨てる」── 捨てる理由が違うだけで、どっちも「貯め込まない」ための仕組みや。
圧縮してから保存される? ── 加工するのは「誰か」
メモリでもディスクでも、キャッシュに入るデータが圧縮済みのことはある。でも キャッシュが既定で勝手に圧縮するわけやない。加工するのが誰かで決まる。
- HTTP / CDN キャッシュ (ディスク側) … オリジンから受け取ったレスポンスをその形のまま保存する。オリジンが gzip で返したら圧縮されたバイト列のまま置いて、同じ圧縮を受け付けるクライアントへ流す。これは前記事の
Content-Encodingそのもので、圧縮あり版と無し版は別のキャッシュエントリになる (Vary: Accept-Encodingでキーが割れる)。CDN 自身がエッジで圧縮する設定もある (Front Door なら 8 MB 未満が対象) ── これは「キャッシュ側が加工する」例。 - Redis (メモリ側) … 渡されたバイト列をそのまま置く。
SETの値を勝手に gzip したりはせん。圧縮したいならアプリがSETの前に圧縮しGETの後に解凍する (メモリ節約の定番)。やるかどうかはアプリの判断。
圧縮以外の「加工」も同じ筋で、やる主体が決まってる ── シリアライズ (アプリがオブジェクト → バイト列にする)、チャンク分割 (CDN が 8 MB 単位に割る)、保存時の暗号化 (マネージドの at-rest 暗号化)。要はキャッシュが魔法で変換するんやなくて、オリジン / アプリ / 明示の設定のどれかが加工した結果が、そのまま保存される。
出てきた言葉の早見表
| 言葉 | つまり何の話か |
|---|---|
| KVS (Key-Value ストア) | キー → 値 でしか引けない「引き方」の型。RDB と対比した分類名で、速さとは別軸 (速いのはメモリに置くから) |
| 索引 (インデックス) | 目的のデータを速く見つける仕組み。メモリでもディスクでもいい (置き場所の名前やない) |
| 順引き / 逆引き | 順引き = URL → ハッシュ (使うのはこっち) / 逆引き = ハッシュ → URL (ハッシュは不可逆なので無理) |
| Cache-Aside | アプリが自分でキャッシュを見て、無ければ元から読んで書き戻す基本パターン |
| Cache Hit / Miss | Hit = キャッシュにあった / Miss = 無かった (元データを読みに行く) |
TTL (EX) | 鍵ごとの寿命。時間が来たら自動で消える |
eviction (maxmemory + LRU) | メモリ上限に達したとき、古い鍵から押し出して捨てる仕組み |
| 同居 / 中央共有 | Redis を Web サーバーと同じマシンに置くか (localhost:6379)、独立させて複数 Web サーバーで共有するか |
で、自分のシステムに Redis キャッシュを足すとき、どこに置くんや?
- 同居 (
localhost:6379) か、中央共有 (Azure Cache for Redis 等のマネージド) か。 判断軸は「Web サーバーをスケールアウトするか」。1 台のままなら同居で十分 (速い・安い)。複数台に増やすなら、各自バラバラのキャッシュでヒット率が落ちるので、中央の 1 つに寄せる。増設の予定があるなら最初から中央共有に倒すのも手。 - 責任境界はどこか。 Redis が持つのは速い複製 (脇に置いた層) だけで、データの正は元データ (DB / オリジン) 側にある。だから Redis が落ちても「遅くなる」で済む設計にする ── キャッシュを正にしてはいけない。
これに答えられると強い ── 「とりあえず Redis 挟む」で止めず、規模に合わせて置き場所を選び、落ちても壊れない責任境界を引ける側に立てる。
この記事の続きとして、次に掘るといいトピック。
- Cache-Aside の兄弟 (Read-Through / Write-Through / Write-Behind) ── 「書き戻しを誰がやるか・いつやるか」で分かれる別パターン。Cache-Aside の次に押さえると書き込み側の設計が見える。
- キャッシュの一貫性問題 (stale data / cache invalidation) ── 元データが変わったのにキャッシュが古いまま、をどう防ぐか。TTL だけでは足りない場面の話。
- Redis の永続化 (RDB スナップショット / AOF) ── 「再起動で消えないための裏の書き出し」を一段深掘り。メモリなのに復活できる仕組み。
- thundering herd / cache stampede ── 人気の鍵が一斉に失効した瞬間、Miss がオリジンに殺到する問題。中央共有 Redis で効いてくる。