Day 4: キャッシュとCDN
今日学ぶこと
- キャッシュがなぜ重要か
- キャッシュ戦略(Cache-aside、Write-through、Write-behind、Read-through)
- キャッシュ追い出しポリシー(LRU、LFU、TTL)
- RedisとMemcachedの比較
- CDN(Content Delivery Network)のアーキテクチャ
- キャッシュ無効化の課題と対策
なぜキャッシュが重要か
キャッシュは、頻繁にアクセスされるデータを高速なストレージに保存することで、レスポンス時間を大幅に短縮します。
flowchart LR
Client["クライアント"] --> Cache{"キャッシュ\n(Redis)"}
Cache -->|"ヒット\n< 1ms"| Client
Cache -->|"ミス"| DB["データベース\n10-100ms"]
DB --> Cache
style Cache fill:#22c55e,color:#fff
style DB fill:#8b5cf6,color:#fff
アクセス速度の比較
| レイヤー | レイテンシ | 容量 |
|---|---|---|
| ブラウザキャッシュ | < 1ms | 数百MB |
| CDN | 10-50ms | 数TB |
| アプリケーションキャッシュ(Redis) | 1-5ms | 数百GB |
| データベース | 10-100ms | 数TB |
| ディスク | 1-10ms | 数PB |
80/20の法則: 多くのシステムでは、20%のデータが80%のアクセスを受けます。この20%をキャッシュするだけで大幅な改善が見込めます。
キャッシュ戦略
Cache-Aside(Lazy Loading)
最も一般的な戦略です。アプリケーションがキャッシュとDBを直接管理します。
flowchart TB
subgraph CacheAside["Cache-Aside パターン"]
App["アプリケーション"]
App -->|"1. キャッシュ確認"| Cache["キャッシュ"]
Cache -->|"2a. ヒット → データ返却"| App
App -->|"2b. ミス → DB読み取り"| DB["データベース"]
DB -->|"3. データ返却"| App
App -->|"4. キャッシュに書き込み"| Cache
end
style CacheAside fill:#3b82f6,color:#fff
style Cache fill:#22c55e,color:#fff
style DB fill:#8b5cf6,color:#fff
# Cache-aside pattern implementation
def get_user(user_id):
# Step 1: Check cache
user = cache.get(f"user:{user_id}")
if user:
return user # Cache hit
# Step 2: Cache miss - read from DB
user = db.query("SELECT * FROM users WHERE id = %s", user_id)
# Step 3: Write to cache with TTL
cache.set(f"user:{user_id}", user, ttl=3600)
return user
Write-Through
データの書き込み時に、キャッシュとDBの両方に同時に書き込みます。
flowchart LR
App["アプリケーション"] -->|"1. 書き込み"| Cache["キャッシュ"]
Cache -->|"2. 同期書き込み"| DB["データベース"]
Cache -->|"3. 応答"| App
style Cache fill:#f59e0b,color:#fff
style DB fill:#8b5cf6,color:#fff
Write-Behind(Write-Back)
キャッシュに書き込み後、非同期でDBに書き込みます。
flowchart LR
App["アプリケーション"] -->|"1. 書き込み"| Cache["キャッシュ"]
Cache -->|"2. 即座に応答"| App
Cache -->|"3. 非同期で\nバッチ書き込み"| DB["データベース"]
style Cache fill:#ef4444,color:#fff
style DB fill:#8b5cf6,color:#fff
Read-Through
Cache-Asideと似ていますが、キャッシュ自体がDBへの読み取りを行います。
flowchart LR
App["アプリケーション"] -->|"1. 読み取り"| Cache["キャッシュ\n(プロバイダー)"]
Cache -->|"2. ミス時\n自動でDB参照"| DB["データベース"]
Cache -->|"3. データ返却"| App
style Cache fill:#22c55e,color:#fff
style DB fill:#8b5cf6,color:#fff
戦略の比較
| 戦略 | 読み取り性能 | 書き込み性能 | 一貫性 | 適するケース |
|---|---|---|---|---|
| Cache-Aside | 高い(ヒット時) | DB直接書き込み | 結果整合性 | 汎用的、読み取り多い |
| Write-Through | 高い | 低い(同期書き込み) | 強い一貫性 | データの整合性が重要 |
| Write-Behind | 高い | 非常に高い | 弱い一貫性 | 書き込みが多い |
| Read-Through | 高い | − | 結果整合性 | シンプルな読み取りキャッシュ |
キャッシュ追い出しポリシー
キャッシュの容量は有限です。容量がいっぱいになったとき、どのデータを追い出すかを決めるのが追い出しポリシーです。
| ポリシー | 仕組み | メリット | デメリット |
|---|---|---|---|
| LRU(Least Recently Used) | 最も最近使われていないデータを追い出す | 実装がシンプル、多くの場合に有効 | スキャン耐性が低い |
| LFU(Least Frequently Used) | 使用頻度が最も低いデータを追い出す | 頻繁にアクセスされるデータを保持 | 新しいデータが追い出されやすい |
| TTL(Time To Live) | 有効期限切れのデータを追い出す | 鮮度を保証できる | 期限設定が難しい |
| FIFO(First In First Out) | 最初にキャッシュされたデータから追い出す | 非常にシンプル | アクセスパターンを考慮しない |
flowchart TB
subgraph LRU["LRU キャッシュの動作"]
direction LR
A["A(最新)"] --> B["B"] --> C["C"] --> D["D(最古)"]
end
subgraph After["Bにアクセス後"]
direction LR
B2["B(最新)"] --> A2["A"] --> C2["C"] --> D2["D(最古)"]
end
LRU -->|"Bにアクセス"| After
style LRU fill:#3b82f6,color:#fff
style After fill:#22c55e,color:#fff
面接でのポイント: ほとんどのケースでLRU + TTLの組み合わせを推奨しましょう。TTLでデータの鮮度を保ち、LRUで容量を管理します。
Redis vs Memcached
| 比較項目 | Redis | Memcached |
|---|---|---|
| データ構造 | String, Hash, List, Set, Sorted Set | String のみ |
| 永続化 | RDB / AOF | なし |
| レプリケーション | Leader-Follower | なし |
| クラスタリング | Redis Cluster | クライアント側で実装 |
| Pub/Sub | 対応 | 非対応 |
| Luaスクリプト | 対応 | 非対応 |
| メモリ効率 | やや低い(機能が多いため) | 高い(シンプル) |
| スレッドモデル | シングルスレッド(I/Oスレッド対応) | マルチスレッド |
| 適するケース | 汎用キャッシュ、セッション、ランキング | シンプルなキャッシュ |
flowchart TB
subgraph Decision["Redis vs Memcached 選択"]
Q1{"複雑なデータ構造\nが必要?"}
Q1 -->|"はい"| Redis["Redis"]
Q1 -->|"いいえ"| Q2{"永続化や\nレプリケーションが\n必要?"}
Q2 -->|"はい"| Redis
Q2 -->|"いいえ"| Q3{"シンプルな\nKey-Value\nキャッシュだけ?"}
Q3 -->|"はい"| Memcached["Memcached"]
Q3 -->|"いいえ"| Redis
end
style Decision fill:#3b82f6,color:#fff
style Redis fill:#ef4444,color:#fff
style Memcached fill:#22c55e,color:#fff
面接での現実: ほとんどの場面でRedisを選択して問題ありません。Memcachedを選ぶのは、純粋なKey-Valueキャッシュで最大限のメモリ効率が必要な場合のみです。
CDN(Content Delivery Network)
CDNは、静的コンテンツを世界中のエッジサーバーに配置して、ユーザーに最も近いサーバーからコンテンツを配信する仕組みです。
flowchart TB
User1["ユーザー\n(東京)"] --> Edge1["エッジサーバー\n東京"]
User2["ユーザー\n(NY)"] --> Edge2["エッジサーバー\nニューヨーク"]
User3["ユーザー\n(ロンドン)"] --> Edge3["エッジサーバー\nロンドン"]
Edge1 -->|"キャッシュミス時"| Origin["オリジンサーバー"]
Edge2 -->|"キャッシュミス時"| Origin
Edge3 -->|"キャッシュミス時"| Origin
style Origin fill:#ef4444,color:#fff
style Edge1 fill:#22c55e,color:#fff
style Edge2 fill:#22c55e,color:#fff
style Edge3 fill:#22c55e,color:#fff
Pull型 vs Push型
| 方式 | 仕組み | メリット | デメリット |
|---|---|---|---|
| Pull型 | リクエスト時にオリジンから取得 | 設定がシンプル | 初回アクセスが遅い |
| Push型 | あらかじめコンテンツをエッジに配置 | 初回アクセスも高速 | ストレージコストが高い |
CDNに適するコンテンツ
| コンテンツタイプ | CDN適性 | 理由 |
|---|---|---|
| 画像・動画 | 非常に高い | 大容量、変更頻度低い |
| CSS・JavaScript | 高い | 静的ファイル |
| APIレスポンス | 場合による | 動的だがキャッシュ可能な場合あり |
| ユーザー固有データ | 低い | キャッシュが難しい |
キャッシュ無効化の課題
"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton
flowchart TB
subgraph Problems["キャッシュの課題"]
P1["Thundering Herd\n大量のキャッシュミスが\n同時にDBへ"]
P2["Cache Penetration\n存在しないキーへの\n大量リクエスト"]
P3["Cache Avalanche\n大量のキャッシュが\n同時に期限切れ"]
P4["Stale Data\n古いデータが\n残り続ける"]
end
subgraph Solutions["対策"]
S1["ロック/セマフォで\nDB問い合わせを制限"]
S2["Bloomフィルタで\n存在しないキーを除外"]
S3["TTLにランダムな\nジッターを追加"]
S4["イベント駆動で\n能動的に無効化"]
end
P1 --> S1
P2 --> S2
P3 --> S3
P4 --> S4
style Problems fill:#ef4444,color:#fff
style Solutions fill:#22c55e,color:#fff
| 課題 | 説明 | 対策 |
|---|---|---|
| Thundering Herd | キャッシュ失効時に大量のリクエストがDBに殺到 | ロック機構で1リクエストのみDB参照 |
| Cache Penetration | 存在しないデータへの繰り返しリクエスト | Bloomフィルタ、null値のキャッシュ |
| Cache Avalanche | 大量のキャッシュが同時に期限切れ | TTLにランダムなジッターを追加 |
| Stale Data | データ更新後もキャッシュに古いデータ | イベント駆動の無効化、短いTTL |
キャッシュ無効化パターン
# Pattern 1: TTL-based (simplest)
cache.set("product:123", product_data, ttl=300) # 5 minutes
# Pattern 2: Event-driven invalidation
def update_product(product_id, data):
db.update(product_id, data)
cache.delete(f"product:{product_id}") # Invalidate cache
# Pattern 3: Versioned keys
version = db.get_version("product:123")
cache_key = f"product:123:v{version}"
まとめ
今日のポイント
| トピック | キーポイント |
|---|---|
| キャッシュの重要性 | 80/20の法則、レイテンシを100倍改善 |
| キャッシュ戦略 | Cache-Asideが最も汎用的、ユースケースに応じて選択 |
| 追い出しポリシー | LRU + TTLの組み合わせが一般的 |
| Redis vs Memcached | ほとんどの場合Redisで良い |
| CDN | 静的コンテンツ配信に不可欠、Pull型が一般的 |
| キャッシュ無効化 | Thundering Herd、Penetration、Avalancheに注意 |
キャッシュ設計チェックリスト
1. キャッシュすべきデータを特定(読み取り頻度 > 書き込み頻度)
2. キャッシュ戦略を選択(通常はCache-Aside)
3. 追い出しポリシーを決定(LRU + TTL)
4. キャッシュの容量を見積もり
5. キャッシュ無効化戦略を設計
6. Thundering Herd等の障害対策
7. CDNで静的コンテンツを配信
練習問題
基礎レベル
問題: 以下の4つのキャッシュ戦略(Cache-Aside、Write-Through、Write-Behind、Read-Through)のそれぞれについて、メリットとデメリットを1つずつ挙げてください。
中級レベル
問題: DAU 500万人のニュースサイトのキャッシュ戦略を設計してください。以下の要件を満たすこと。
- トップページは1分以内の鮮度を保つ
- 記事ページは5分以内の鮮度で許容
- 画像は変更されることがほぼない
- ピーク時(朝8時)にトラフィックが通常の5倍
- Thundering Herdへの対策を含めること
チャレンジレベル
問題: グローバルECサイト(Amazon規模)の多層キャッシュアーキテクチャを設計してください。
- ブラウザキャッシュ、CDN、アプリケーションキャッシュ、DBキャッシュの各層の役割
- 商品価格が更新された場合のキャッシュ無効化フロー
- フラッシュセール(タイムセール)時のキャッシュ戦略
- 在庫数のようなリアルタイム性が必要なデータの扱い
参考リンク
次回予告
Day 5: メッセージキューと非同期処理 では、同期処理の限界を突破する非同期アーキテクチャを学びます。Kafka、RabbitMQ、SQSの比較や、イベント駆動設計、メッセージ配信の保証レベルなど、マイクロサービス時代の必須知識を身につけましょう。