Skip to content

✍️ 필사 모드: CDN & Edge キャッシュ戦略 完全ガイド 2025: Cache Invalidation、ETag、Stale-While-Revalidate

日本語
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

TL;DR

  • コンピュータサイエンスの 2 大難問: cache invalidation、naming things、off-by-one errors (Phil Karlton)
  • HTTP cache ヘッダをマスター: Cache-ControlETagVaryIf-None-Match
  • CDN cache = 世界分散 cache: ユーザーに近い PoP から応答 → 50ms 以下の latency
  • Stale-While-Revalidate (SWR): 古い応答を即座に返し、background で更新 = 体感上 100% の cache hit ratio
  • Edge computing: cache を超えてコード実行 → Cloudflare Workers、Fastly Compute@Edge

1. なぜ cache は難しいのか

1.1 Phil Karlton の名言

"There are only two hard things in Computer Science: cache invalidation and naming things."

この冗談は半分本当です。cache invalidation は本当に難しい。

1.2 2 つの本質的な難しさ

1. いつ invalidate するか?

  • 早すぎる → cache の効果がない
  • 遅すぎる → stale data 問題

2. どう invalidate するか?

  • 単一 key? パターン? 全体?
  • 同期? 非同期?
  • 失敗したらどうする?

1.3 cache 一貫性のトレードオフ

強い一貫性 ←─────────────────→ 高い性能
 (リアルタイム)              (cache hit)

すべてのシステムはこのスペクトル上のどこかに位置します。100% 両立はできません。


2. HTTP cache ヘッダをマスターする

2.1 Cache-Control — cache の中心

Cache-Control: public, max-age=3600, s-maxage=86400

ディレクティブ:

意味
publicCDN、ブラウザどちらも cache 可能
privateブラウザのみ(ユーザーごと)
no-cachecache 可能、毎回検証が必要
no-store絶対に cache しない(機密データ)
max-age=Nブラウザ cache の有効時間(秒)
s-maxage=NCDN cache の有効時間(ブラウザより優先)
must-revalidate期限切れ後は必ず検証
immutable絶対に変わらない(CSS、hash 付き JS)
stale-while-revalidate=N期限切れ後 N 秒は stale を使用可能
stale-if-error=Nエラー時に N 秒間 stale を使用

2.2 一般的なパターン

HTML(頻繁に変化):

Cache-Control: public, max-age=0, must-revalidate

API JSON(変化しうる):

Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400

静的ファイル(hash 含む):

Cache-Control: public, max-age=31536000, immutable

機密データ(ユーザー情報):

Cache-Control: private, no-store

2.3 ETag — 効率的な検証

サーバーがコンテンツの一意識別子(hash)を返します:

HTTP/1.1 200 OK
ETag: "abc123"
Cache-Control: max-age=3600

cache 期限切れ後、クライアントから:

GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"

コンテンツが同一なら:

HTTP/1.1 304 Not Modified
ETag: "abc123"

304 Not Modified = body なし。帯域節約。

2.4 Last-Modified — 時刻ベース

HTTP/1.1 200 OK
Last-Modified: Tue, 15 Apr 2025 10:00:00 GMT
GET /api/users/123 HTTP/1.1
If-Modified-Since: Tue, 15 Apr 2025 10:00:00 GMT

ETag より弱い: 1 秒未満の変化を検知できない、時計同期に依存。

2.5 Vary — cache の同一性の定義

HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Vary: Accept-Encoding, Accept-Language

意味: 「同じ URL でも Accept-Encoding(gzip vs br)や Accept-Language(en vs ko)が違えば別 cache」。

注意: Vary: User-Agent はほぼ無限のバリエーション → cache が無意味になります。絶対に使わないこと。

2.6 よくある誤解

1. no-cache は「cache しない」ではない

  • no-cache: cache 可、使う前に検証が必要
  • no-store: cache 絶対 NG

2. Pragma: no-cache は古いヘッダ

  • HTTP/1.0 の名残
  • ほぼ無視されるが、互換性のため残す場所もある

3. Vary: * は cache 無効化

  • すべてのヘッダが違えば別 cache = 決して hit しない

3. CDN の仕組み

3.1 CDN とは

Content Delivery Network — 世界中に分散した cache サーバーのネットワーク。

ユーザー(ソウル)[CDN PoP ソウル] (cache hit!) → 応答
                            ↓ miss
                      [Origin サーバー(米国)]

効果:

  • ソウルのユーザー: 5ms (CDN PoP)
  • 米国 origin に直接: 200ms(往復 + 処理)

3.2 CDN の構成要素

コンポーネント役割
PoP (Point of Presence)都市別の cache サーバー(Cloudflare は 300+)
Origin元サーバー(あなたのサーバー)
Edge CachePoP の cache ストレージ
DNS最も近い PoP にルーティング
Anycast同じ IP が複数 PoP にルーティング

3.3 cache key

CDN は何で cache を識別するのか?

デフォルト: URL のみ。

GET /api/users/123  → cache
GET /api/users/123?lang=en  → 別 cache(query string を含む)

Cloudflare Workers:

const cacheKey = new Request(url, { method: 'GET' })
const cache = caches.default
const cached = await cache.match(cacheKey)

3.4 cache hit ratio

最重要メトリクス

hit_ratio = cache_hits / (cache_hits + cache_misses)
Hit Ratio意味
95%+優秀
90–95%良好
80–90%改善余地あり
80% 未満cache 戦略を見直す必要あり

1% の改善に大きな意味: 96% → 97% = origin トラフィック 25% 減。

3.5 CDN 比較

CloudflareFastlyCloudFrontAkamaiVercel/Netlify
PoP 数300+80+600+4000+100+
Edge computeWorkers (V8)Compute@Edge (Wasm)Lambda@EdgeEdgeWorkersEdge Functions
無料枠寛大なしなしなしあり
価格非常に安い高い普通高い普通
cache invalidation即時150ms (Instant Purge)分単位分単位分単位
DDoS 対策優秀良好良好優秀普通

Cloudflare: コスパチャンピオン。90% のユースケースに適合。 Fastly: 高速な invalidation、メディア/ニュース向けに強い。 CloudFront: AWS 連携が強み。 Akamai: エンタープライズ、最多の PoP。


4. Cache Invalidation — 最も難しい部分

4.1 4 つの invalidation 戦略

1. TTL ベース: 時間経過で自動期限切れ

Cache-Control: max-age=3600  // 1 時間後に期限切れ

長所: シンプル。短所: 1 時間は stale の可能性。

2. Push(即時 invalidation): コンテンツ変更時に CDN へ通知

curl -X POST https://api.cloudflare.com/zones/.../purge_cache \
  -H "Authorization: Bearer ..." \
  -d '{"files":["https://example.com/api/users/123"]}'

長所: 即時。短所: 複雑、コスト。

3. ETag ベース: リクエストごとに検証(304 応答)

  • 帯域は節約、遅延は残る

4. Versioned URL: 変更時に新 URL

/static/main.abc123.js/static/main.def456.js

長所: invalidation 自体が不要。CSS/JS の標準。

4.2 Cloudflare の invalidation API

# 単一 URL
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {api_token}" \
  -d '{"files":["https://example.com/page1"]}'

# タグベース(Enterprise)
curl -X POST ... -d '{"tags":["user-123"]}'

# 全体
curl -X POST ... -d '{"purge_everything":true}'

4.3 Cache Tags — 優雅な invalidation

Fastly と Cloudflare Enterprise が対応:

HTTP/1.1 200 OK
Cache-Tag: user-123, post-456, blog-list

invalidation:

curl -X POST https://api.fastly.com/service/{id}/purge/user-123

user-123 タグの付いたすべての cache を即座に無効化。

利用例: ユーザーがプロフィールを更新 → user-123 タグ付きの全ページを無効化。

4.4 invalidation の難しいシナリオ

シナリオ 1: ユーザーがコメントを投稿

影響を受けるページ:
- /post/123 (該当の記事)
- /post/123/comments
- /user/abc/comments (投稿者ページ)
- /sidebar/recent-comments
- ... さらに多数

→ 全ページを一つずつ invalidate? タグベースが答え。

シナリオ 2: データベース一貫性

DB トランザクション commit → cache invalidation
                ↓ もし invalidation が失敗したら?
            stale data!

リトライ + dead letter queue。または eventual consistency を受け入れる。

シナリオ 3: invalidation の急増

ブラックフライデー = 価格変更が頻発 = invalidation 急増 → CDN API rate limit。

batch invalidationタグ活用TTL 短縮


5. Stale-While-Revalidate — ゲームチェンジャー

5.1 問題

従来の cache:

[cache 期限切れ][origin リクエスト][待機][応答]
ユーザーが待つ(遅い!)

5.2 Stale-While-Revalidate

Cache-Control: max-age=60, stale-while-revalidate=86400

意味:

  • 60 秒間: fresh な cache を使用
  • 60 秒~86460 秒: stale cache を即座に返す + background で更新
  • 86460 秒以降: 期限切れ(origin へ)

5.3 ユーザー体験

ユーザー 1 (60s):   [cache hit, fresh]   instant
ユーザー 2 (61s):   [cache hit, stale]   instant (background 更新開始)
ユーザー 3 (62s):   [cache hit, fresh]   instant (更新直後)

全員 instant 応答! origin はたまにしか呼ばれない。

5.4 Next.js の ISR (Incremental Static Regeneration)

export async function getStaticProps() {
  const data = await fetchData()
  return {
    props: { data },
    revalidate: 60  // 60 秒後に background で再生成
  }
}

内部的に SWR パターンを利用。静的並みの速度 + 動的データ。

5.5 stale-if-error

Cache-Control: max-age=60, stale-if-error=86400

意味: origin エラー時に 24 時間 stale データを返す。

効果: origin ダウン時もサイトが生きる。障害耐性の要


6. Cache パターン — コードレベル

6.1 Cache-Aside (Lazy Loading)

def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if user is None:
        user = db.query(f"SELECT * FROM users WHERE id={user_id}")
        cache.set(f"user:{user_id}", user, ttl=3600)
    return user

最も一般的。シンプルで安定。欠点: cache miss 時の初回は遅い。

6.2 Read-Through

cache が origin を直接呼ぶ(アプリは cache のみを呼ぶ)。

# cache ライブラリが自動処理
user = cache.get(f"user:{user_id}", loader=lambda: db.query(...))

長所: コードがシンプル。短所: cache ライブラリ依存。

6.3 Write-Through

書き込み時に cache も同時更新。

def update_user(user_id, data):
    db.execute(f"UPDATE users SET ... WHERE id={user_id}")
    cache.set(f"user:{user_id}", data, ttl=3600)

長所: cache が常に最新。短所: 書き込みが遅く、使われないデータも cache。

6.4 Write-Behind (Write-Back)

書き込みは cache のみ → 非同期で DB 反映。

def update_user(user_id, data):
    cache.set(f"user:{user_id}", data, ttl=3600)
    queue.push({"action": "update", "id": user_id, "data": data})

長所: 非常に高速な書き込み。短所: cache 障害時にデータ損失リスク。

6.5 Cache Stampede の防止

問題: 人気 key が期限切れになると数千リクエストが同時に origin を叩く。

解決策:

1. Mutex(分散ロック)

def get_user(user_id):
    user = cache.get(f"user:{user_id}")
    if user is None:
        with cache.lock(f"lock:user:{user_id}", timeout=10):
            user = cache.get(f"user:{user_id}")  # double-check
            if user is None:
                user = db.query(...)
                cache.set(f"user:{user_id}", user, ttl=3600)
    return user

2. Probabilistic Early Expiration TTL 直前から一部リクエストを早めに更新:

def get_with_recompute(key):
    value, ttl = cache.get_with_ttl(key)
    if random() < ttl_factor(ttl):
        # 一部リクエストだけ前倒しで再計算
        recompute_async(key)
    return value

3. Stale-While-Revalidate(前述)


7. Edge Computing — cache を超えて

7.1 Edge でのコード実行

CDN は単なる cache ではありません。コードも実行:

  • Cloudflare Workers (V8 isolate)
  • Fastly Compute@Edge (WebAssembly)
  • AWS Lambda@Edge (Node.js, Python)
  • Vercel Edge Functions (V8)
  • Deno Deploy (V8)

7.2 ユースケース

1. A/B テスト

export default {
  async fetch(request) {
    const variant = Math.random() < 0.5 ? 'a' : 'b'
    return fetch(`https://origin.com/page-${variant}`)
  }
}

2. geo ルーティング

const country = request.cf.country
const origin = country === 'KR' ? 'asia.api.com' : 'us.api.com'
return fetch(origin + new URL(request.url).pathname)

3. 認証/認可

const token = request.headers.get('Authorization')
const user = await verifyJWT(token)
if (!user) return new Response('Unauthorized', { status: 401 })
return fetch(origin)

4. HTML 変換

const response = await fetch(origin)
return new HTMLRewriter()
  .on('h1', { element(el) { el.setInnerContent('Modified!') } })
  .transform(response)

5. API 集約

const [user, posts] = await Promise.all([
  fetch('https://api.com/user/123').then(r => r.json()),
  fetch('https://api.com/posts?user=123').then(r => r.json())
])
return new Response(JSON.stringify({ user, posts }))

7.3 Edge computing の制約

  • CPU 時間制限: 通常 10–50ms (Cloudflare Workers は 50ms)
  • メモリ制限: 128MB (Cloudflare)、50MB (Fastly)
  • コールドスタート: V8 isolate はほぼ皆無(5ms 以下)
  • DB アクセス: edge から origin DB は遅い → edge DB が必要(Turso、D1、Neon)

7.4 Edge データストア

CloudflareFastlyVercel
KVWorkers KVObject StoreKV
DBD1 (SQLite)-Neon、Turso
ObjectR2-Blob
CacheCache APICache-
VectorVectorize--

8. CDN cache のベストプラクティス

8.1 静的アセット

# CSS、JS (hash 付きファイル名)
Cache-Control: public, max-age=31536000, immutable

# 画像(あまり変わらない)
Cache-Control: public, max-age=86400, stale-while-revalidate=604800

8.2 HTML

# 頻繁に変わるが cache 可能
Cache-Control: public, max-age=0, s-maxage=300, stale-while-revalidate=86400
  • max-age=0: ブラウザは毎回検証
  • s-maxage=300: CDN は 5 分 cache
  • stale-while-revalidate=86400: 24 時間 stale 使用可

8.3 API JSON

# ユーザー別データ (private)
Cache-Control: private, max-age=60, must-revalidate

# 公開 API
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=86400

8.4 ユーザー別コンテンツ

# 絶対に cache しない
Cache-Control: private, no-store

あるいは cache key に Cookie を含める(危険 — カーディナリティ爆発)。

8.5 圧縮 + cache

HTTP/1.1 200 OK
Content-Encoding: gzip
Cache-Control: public, max-age=3600
Vary: Accept-Encoding

Vary: Accept-Encoding は必須 — gzip vs br vs none は別 cache。


9. モニタリングとデバッグ

9.1 レスポンスヘッダの確認

curl -I https://example.com/api/users/123

HTTP/2 200
cf-cache-status: HIT
age: 234
cache-control: public, max-age=3600

主要ヘッダ:

  • cf-cache-status: HIT、MISS、EXPIRED、BYPASS、REVALIDATED
  • age: cache されてからの秒数
  • x-cache: AWS CloudFront ヘッダ

9.2 cache 状態

HIT           ← cache から返却
MISS          ← origin リクエスト
EXPIRED       ← 期限切れ、更新中
REVALIDATED304 で検証済み
BYPASS        ← cache 迂回 (no-cache 等)
DYNAMIC       ← cache 不可

9.3 cache hit ratio の追跡

多くの CDN ではダッシュボードに表示:

  • Cloudflare Analytics
  • Fastly Insights
  • CloudFront Reports

9.4 デバッグ手順

cache hit しない場合、以下を確認:

  1. Cache-Control ヘッダ: no-cacheprivate があるか?
  2. Set-Cookie: cookie があると cache されない
  3. Vary: バリエーションが多すぎる
  4. Method: POST はほぼ cache されない
  5. Status code: 200、301、302、404、410 のみ cache
  6. Query string: CDN が query string を無視する設定?

10. 実戦 — ブログサイトの cache

10.1 シナリオ

  • WordPress または Next.js ブログ
  • 1 日 100 万ページビュー
  • 記事が頻繁に更新される
  • コメントシステム

10.2 cache 戦略

HTML ページ:

Cache-Control: public, max-age=0, s-maxage=3600, stale-while-revalidate=86400

静的アセット:

Cache-Control: public, max-age=31536000, immutable

API JSON:

Cache-Control: public, max-age=300, stale-while-revalidate=86400

10.3 invalidation

記事更新時:

  1. タグ invalidation: post-idcategory-slughomepage
  2. 新 URL: 静的アセットは hash 変更
// Next.js + Cloudflare
async function updatePost(id, data) {
  await db.update(...)
  await cf.purgeByTags([`post-${id}`, 'homepage'])
}

10.4 結果

  • Cache hit ratio: 95%+
  • Origin requests: 5 万/日 (95% 削減)
  • 応答時間: 50ms (CDN) vs 500ms (origin)
  • コスト: 95% 削減

クイズ

1. no-cacheno-store の違いは?

答え: no-cache: cache 可能、ただし使う前に必ず origin へ検証リクエスト(If-None-Match)。304 を受け取れば cache を使える。意外にも cache が活用される。no-store: cache 絶対不可。リクエストと応答をどこにも保存しない(メモリ、ディスク)。機密データに使用。よくある間違い: 「cache しない」のつもりで no-cache を使う → 実際には cache されて検証されるだけ。

2. Stale-While-Revalidate がゲームチェンジャーである理由は?

答え: ユーザーに常に instant 応答を返せます。cache 期限切れ後も stale データを即座に返し、background で更新。ユーザーは待たず、次回リクエストから新データが見える。結果: 体感 cache hit ratio が 100%、origin トラフィックも少ないまま。Next.js ISR はこのパターンを使用。SWR は cache 期限切れの本質的なトレードオフ(新鮮さ vs 速度)をほぼ解消します。

3. Cache Stampede を防ぐ方法を 3 つ挙げよ

答え: (1) Mutex / 分散ロック — 期限切れの key に対して 1 リクエストだけ origin を呼び、他は結果を待つ(または stale を利用)、(2) Probabilistic Early Expiration — TTL 直前から一部リクエストを前倒しで再計算、(3) Stale-While-Revalidate — 期限切れ cache も使いつつ background 更新。人気 key ほどこの保護が重要。Reddit や Facebook も類似パターンを使用。

4. CDN cache hit ratio が 1% 改善するとどうなる?

答え: origin トラフィックが 25% 以上減ります。95% → 96% なら origin リクエストは 5% → 4% = 20% 削減。96% → 97% なら 4% → 3% = 25% 削減。cache hit ratio が高いほど 1% の価値が大きくなります。これは (1) サーバーコスト、(2) 帯域コスト、(3) 応答時間、(4) 信頼性すべてに影響。0.5% の改善でも大きなビジネス価値

5. Edge computing が cache を超えた意味とは?

答え: CDN はもはや静的ファイルの cache だけではありません。ユーザーに近いノードでコードを実行します。ユースケース: A/B テスト、geo ルーティング、認証、HTML 変換、API 集約。結果: 動的コンテンツも 50ms 以下。Cloudflare Workers は V8 isolate でコールドスタートが 5ms 以下。Fastly Compute@Edge は Wasm ベース。origin サーバーの役割は縮小し、edge がますます多くの仕事を担う。


参考資料

현재 단락 (1/353)

- **コンピュータサイエンスの 2 大難問**: cache invalidation、naming things、off-by-one errors (Phil Karlton)

작성 글자: 0원문 글자: 13,807작성 단락: 0/353