ブログに戻る

ゲームアセットのCDNスケーリング: ローンチ当日のトラフィックスパイクを乗り切るためのランクック

公開日 2026年8月4日
ゲームアセットのCDNスケーリング: ローンチ当日のトラフィックスパイクを乗り切るためのランクック AIを活用して生成

要点まとめ

ゲームアセットのCDNスケーリングを徹底解説。ローンチ当日のトラフィックスパイクで発生するオリジン飽和、キャッシュスタンピード、地域エッジ不足の検知方法と即時復旧手順、再発防止のためのマルチティアキャッシュアーキテクチャ設計とロードテスト手法まで、cdnjsの移行事例を交えながら網羅します。

あなたのCDNは限界を迎える — その兆候を見抜く方法

すべてのゲーム開発者が恐れる、同じローンチ当日のシナリオがあります。Steamのページが公開され、同時接続プレイヤー数が10,000を超えた瞬間、テクスチャのダウンロードが通常の12msから一転してp99レイテンシ200msで停止する。プレイヤーからはモデルが読み込めないという報告が来る。パッチダウンロードは43%で停止する。モニタリングダッシュボードは赤く染まるが、どのレイヤーで障害が起きているのか皆目見当がつかない。

これは仮定の話ではありません。世界で最も広く使われているオープンソースCDNネットワークの1つであるcdnjsは、1日90億リクエストを処理するために、先日CloudflareのDeveloper Platformへの完全なインフラストラクチャ移行を完了しました。

この移行の事例は、ゲームアセット配信に直接応用できるアーキテクチャパターンを明らかにしています。ゲームアセット配信では、4Kテクスチャパックの1回のアップデートで、数分のうちにテラバイト規模のトラフィックが発生することがあります。

核心となる教訓は、ゲームアセットのCDNスケーリングは帯域幅を多く購入することではないということです。キャッシュ階層、フォールバックロジック、オリジンシールディングを設計し、トラフィックスパイクを障害ではなく「何も起こらない出来事」にすることが重要なのです。

このランクックでは、CDNが飽和したときに何が壊れるのか、Discordが怒りの声で埋め尽くされる前に飽和を検知する方法、本番環境での復旧手順、再発を防ぐためのアーキテクチャ設計について解説します。


CDNが飽和したときに何が壊れるのか

ゲームアセット配信には、標準的なWebコンテンツとは異なる独自のトラフィックプロファイルがあります。障害モードを理解するには、そのプロファイルを理解する必要があります。

トラフィック形状の問題

典型的なインディーズマルチプレイヤーゲームでは、以下のようなトラフィックパターンが発生します。

  • ベースライン: ロビーアセット、UIスプライト、設定JSONに対する毎秒50〜200リクエスト
  • パッチ当日のスパイク: Steamが自動アップデートをトリガーし、3分間で毎秒15,000〜80,000リクエスト
  • 地域的なカスケード: アジア太平洋地域のプレイヤーがNAの8〜12時間後にCDNにアクセスし、第二の波が発生
  • アセットバージョンの爆発的増加: パッチごとにキャッシュされたオブジェクトが無効化され、新しいハッシュのオリジンプルが強制される

cdnjsがCloudflareのインフラストラクチャに移行した際も、同様のバージョン爆発問題に直面しました。npmスタイルのバージョニングでは、ライブラリのアップデートごとに新しいキャッシュキーが作成され、4,200以上のライブラリが毎日更新されるため、オリジンシールディングの設計は単なる静的コンテンツではなく、継続的なキャッシュチャーンを処理する必要がありました。

3つの障害モード

1. オリジンプル飽和

エッジキャッシュがミスした場合(新規パッチ、コールドキャッシュ、キャッシュ期限切れ)、すべてのリクエストがオリジンサーバーに到達します。1 Gbpsのスループットを持つ単一のオリジンは、約1,250の同時1 MBアセットダウンロードを処理できます。80,000人の同時接続プレイヤーがそれぞれ2 GBのパッチをダウンロードする状況では、ほとんどのインディーズ環境が持っていないオリジン容量が必要になります。

2. キャッシュスタンピード

最もリクエストの多いアセットがエッジキャッシュから期限切れになると(TTL設定ミス、デプロイによるパージ)、数千のエッジノードが同時にオリジンから同じオブジェクトをリクエストします。これは「thundering herd(暴走群集)」問題であり、オリジンを数秒でクラッシュさせます。

3. 地域エッジスターべーション

NAのエッジノードはウォーム状態です。シンガポールのエッジノードは、APACプレイヤーが12,000人しかいないためキャッシュヒット率60%です。しかし、日本のYouTuberがあなたのゲームを取り上げ、その数が一夜にして300,000人に跳ね上がると、エッジノードは大規模にオリジンからプルすることになり、APACプレイヤーは2〜4秒のロード時間を経験する一方、NAプレイヤーは40msを維持します。

検知シグナル

# Cloudflare API: check cache hit ratio by region (run every 60 seconds)
curl -s -X POST "https://api.cloudflare.com/client/v4/graphql" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "{
      viewer {
        zones(filter: {zoneTag: \"YOUR_ZONE\"}) {
          httpRequests1hGroups(limit: 24, filter: {date_gt: \"2025-01-01\"}) {
            dimensions { datetime, cacheStatus, clientCountryName }
            sum { requests, bytes }
          }
        }
      }
    }"
  }' | jq '.data.viewer.zones[0].httpRequests1hGroups[] |
    select(.dimensions.cacheStatus == "miss") |
    {region: .dimensions.clientCountryName, misses: .sum.requests}'

定常状態の期間中に、いずれかの地域でキャッシュミス率が8%を超えた場合、次のパッチでオリジンフラッドが発生する可能性があります。


即時復旧: 今すぐやるべきこと

CDNが炎上しているとき、プレイヤーがレビュー爆撃を始めるまでに15分の猶予があります。以下がトリアージ手順です。

ステップ1: オリジンシールディングを有効化する

ほとんどのCDNプロバイダーは「オリジンシールド」または「シールディング」機能を提供しています。これは、エッジノードとオリジンの間の中間キャッシュレイヤーです。キャッシュミス時に200のエッジノードがそれぞれ独立してオリジンにアクセスする代わりに、シールドノードだけがオリジンに接続し、レスポンスを配信します。

設定例(汎用CDN API):

{
  "shielding": {
    "enabled": true,
    "shield_region": "us-east-1",
    "fallback_shield_region": "eu-west-1",
    "shield_ttl_override": 86400,
    "pass_on_shield_error": false
  }
}

この1つの変更だけで、キャッシュスタンピード時のオリジン負荷を**95%**削減できます。cdnjsの移行も同様のシールディングロジックに依存していました。彼らのオリジンサーバーは、毎時数百万のダイレクトプルから数千のシールド経由リクエストにまで削減されました。

ステップ2: 静的コンテンツのアセットTTLを延長する

4Kテクスチャ、オーディオバンク、メッシュファイルはパッチ間で変更されません。1時間のTTLを設定する理由はありません。

# nginx origin server: aggressive caching for immutable game assets
location /assets/v*/ {
    # Version-prefixed paths mean new versions get new URLs
    # No need to purge — old URLs stay cached forever
    add_header Cache-Control "public, max-age=31536000, immutable";
    add_header CDN-Cache-Control "max-age=31536000";
}

# Short TTL only for manifest files that change each patch
location /manifest.json {
    add_header Cache-Control "public, max-age=60, stale-while-revalidate=300";
}

cdnjsアーキテクチャからの重要な洞察: アセットはクエリ文字列ではなくURLパスでバージョニングすることです。多くのCDNノードは?v=2?v=3を同じキャッシュキーとして扱います。代わりに/assets/v2/texture_pack.binを使用してください。

ステップ3: Stale-While-Revalidateを有効化する

これはローンチ当日のトラフィックにとって最も影響力のある設定です。キャッシュされたアセットが期限切れになると、CDNはバックグラウンドで新しいバージョンを取得しながら、リクエストしてきたプレイヤーに古いバージョンを配信します。プレイヤーは1,200msのレスポンスではなく12msのレスポンスを受け取ります。

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

これはCDNに「このアセットは1時間フレッシュ。その後は、バックグラウンドで再検証している間に最大24時間は古いバージョンを配信せよ」と指示します。

セキュリティ上重要ではないゲームアセット(ロビー背景、コスメティックプレビュー、オーディオステム)にとって、これは安全であり、知覚レイテンシを劇的に削減します。

ステップ4: サーキットブレーカーフォールバックを実装する

CDNオリジンが完全に過負荷になった場合、ゲームクライアントには優雅な劣化パスが必要です。フリーズしたローディング画面ではなく。

// C# Unity: CDN circuit breaker with local fallback
public class AssetLoader
{
    private const int MAX_RETRIES = 3;
    private const int TIMEOUT_MS = 5000;
    private static int _failureCount = 0;
    private static DateTime _circuitOpened = DateTime.MinValue;
    private static readonly TimeSpan CIRCUIT_RESET = TimeSpan.FromMinutes(2);

    public async Task<byte[]> LoadAsset(string assetPath)
    {
        // Circuit breaker: skip CDN if recent failures exceeded threshold
        if (_failureCount >= MAX_RETRIES &&
            DateTime.UtcNow - _circuitOpened < CIRCUIT_RESET)
        {
            Debug.LogWarning($"CDN circuit open — loading {assetPath} from local cache");
            return LoadFromLocalStorage(assetPath);
        }

        try
        {
            using var client = new HttpClient { Timeout = TimeSpan.FromMilliseconds(TIMEOUT_MS) };
            var response = await client.GetAsync($"https://cdn.yourgame.com/{assetPath}");
            response.EnsureSuccessStatusCode();
            _failureCount = 0; // Reset on success
            return await response.Content.ReadAsByteArrayAsync();
        }
        catch (Exception ex)
        {
            _failureCount++;
            if (_failureCount >= MAX_RETRIES)
                _circuitOpened = DateTime.UtcNow;

            Debug.LogWarning($"CDN fetch failed ({_failureCount}/{MAX_RETRIES}): {ex.Message}");
            return LoadFromLocalStorage(assetPath);
        }
    }

    private byte[] LoadFromLocalStorage(string assetPath)
    {
        // Ship a minimal "emergency asset pack" with your game binary
        // This covers the 20 most critical assets: UI, default textures, lobby music
        var localPath = Path.Combine(Application.streamingAssetsPath, "fallback", assetPath);
        return File.Exists(localPath) ? File.ReadAllBytes(localPath) : Array.Empty<byte>();
    }
}

このパターンにより、CDNが完全にダウンした場合でもゲームの機能を維持できます。プレイヤーは数分間、低解像度のテクスチャを見ることになるかもしれませんが、プレイ自体は続けられます。


予防: マルチティアキャッシングアーキテクチャ

復旧手順はローンチ当日を救います。アーキテクチャは復旧手順を不要にします。

3ティアパターン

Cloudflare Workersへのcdnjs移行は、数十億リクエストにスケールするキャッシングアーキテクチャを実証しました。ゲームアセット用に適用すると:

ティア1 — エッジキャッシュ(CDN PoP)

  • リクエストの95〜99%を処理
  • TTL: バージョン管理されたアセットは365日、マニフェストは60秒
  • テクスチャ、メッシュ、オーディオ、シェーダーをカバー

ティア2 — シールド/ミッドティアキャッシュ

  • エッジノードからのキャッシュミスをインターセプト
  • TTL: エッジと同じだが、オリジンプロキシとして機能
  • オリジン負荷を95%以上削減

ティア3 — オリジンサーバー

  • アセットの生成、URL署名、マニフェスト配信
  • レート制限とシールディングで保護
  • 総トラフィック量の0.1%未満しか受け取らない

バージョン管理されたアセットパイプライン

キャッシュ無効化ストームを防ぐアセットバージョニングワークフローは以下の通りです:

# Python: asset pipeline that generates cache-safe versioned URLs
import hashlib
import json
import os

def build_asset_manifest(asset_dir: str, cdn_base: str) -> dict:
    """
    Walk asset directory, hash each file, and produce a manifest
    with versioned URLs that CDN edge nodes can cache forever.
    """
    manifest = {"version": "", "assets": {}}

    for root, _, files in os.walk(asset_dir):
        for filename in sorted(files):
            filepath = os.path.join(root, filename)
            relative_path = os.path.relpath(filepath, asset_dir)

            # Content hash — identical files get identical URLs
            with open(filepath, "rb") as f:
                file_hash = hashlib.sha256(f.read()).hexdigest()[:12]

            # Version in the PATH, not query string
            # CDN treats /assets/a3f9b2c1e8d4/texture.bin as a unique object
            versioned_url = f"{cdn_base}/assets/{file_hash}/{relative_path}"

            manifest["assets"][relative_path] = {
                "url": versioned_url,
                "hash": file_hash,
                "size": os.path.getsize(filepath),
            }

    # Manifest version = hash of the entire asset set
    all_hashes = "".join(
        a["hash"] for a in sorted(manifest["assets"].values(), key=lambda x: x["url"])
    )
    manifest["version"] = hashlib.sha256(all_hashes.encode()).hexdigest()[:16]

    return manifest


# Usage
manifest = build_asset_manifest("./build/assets", "https://cdn.yourgame.com")
with open("./build/manifest.json", "w") as f:
    json.dump(manifest, f, indent=2)

print(f"Manifest version: {manifest['version']}")
print(f"Total assets: {len(manifest['assets'])}")
# Output:
# Manifest version: a8f3e1c92b4d7061
# Total assets: 2,847

このアプローチでは:

  • 古いアセットは決してパージされません。 一意のURLを持つため、エッジに無期限にキャッシュされ続けます。
  • 新しいアセットは新しいURLを取得します。 CDNは最初のリクエストで自動的にキャッシュします。
  • 変更されるのはマニフェストだけです。 60秒のTTLを持つ小さなJSONファイルです。

これはまさにcdnjsが大規模なライブラリバージョニングを処理する方法です。各ライブラリバージョンは一意のURLパスを取得するため、CDNはパージ操作を必要としません。パージ操作は、CDNの中で最もコストが高くエラーが発生しやすい操作です。

このアーキテクチャパターンは、ゲームロジックと一緒に設定データを配信する必要がある専用サーバーを運営している場合に特に重要です。Unreal Engine専用サーバーのアセットストリッピングをマスターする方法のガイドで説明したように、静的アセットとサーバー重要なデータを分離することは、スケールに応じて効果が増幅される基礎的な最適化です。


地理的分散: 地域カスケードの解決

cdnjsの移行により、生のエッジノード数よりもインテリジェントルーティングが重要であることが明らかになりました。キャッシュミス時にAPACリクエストを米国のオリジンに送信するルーティングロジックでは、300のPoPを保有していても意味がありません。

スマートオリジンセレクション

{
  "origin_rules": [
    {
      "name": "us-primary",
      "origin_server": "origin-us.yourgame.com",
      "regions": ["NA", "SA"],
      "health_check": "/health",
      "failover_origin": "origin-eu.yourgame.com"
    },
    {
      "name": "eu-primary",
      "origin_server": "origin-eu.yourgame.com",
      "regions": ["EU", "AF"],
      "health_check": "/health",
      "failover_origin": "origin-us.yourgame.com"
    },
    {
      "name": "apac-primary",
      "origin_server": "origin-apac.yourgame.com",
      "regions": ["AS", "OC"],
      "health_check": "/health",
      "failover_origin": "origin-us.yourgame.com"
    }
  ]
}

主要クラウドプロバイダーでのリージョナルオリジンサーバーのコストは、それぞれ月額20〜40ドルです。 3つのリージョナルオリジンのコストは、NAオリジンが4時間にわたって劣化したパフォーマンスでAPACトラフィックを処理する1回のインシデントと、それに伴うプレイヤー離れのコストよりも低くなります。

この種のマルチリージョンフェイルオーバーアーキテクチャは、ハイバネーション戦略によるゼロウェイストサーバーのアーキテクチャの分析で議論した内容と通じるものがあります。アイドル状態のインフラストラクチャにコストを払わず、それでいてスケールに備えるという原則です。


ゲームアセットのCDNスケーリングのベストプラクティス

1. アセットはクエリ文字列ではなくURLパスでバージョニングする。

/assets/{hash}/texture.binはキャッシュの一意性を保証します。?v=2は保証しません。多くのCDNノードはキャッシュキーからクエリパラメータを除去するため、古いコンテンツや破損したキャッシュが発生します。

2. マニフェストTTLとアセットTTLを分離する。

マニフェストファイルはstale-while-revalidate付きで30〜60秒のTTLにすべきです。アセットファイルはimmutable付きで1年のTTLにすべきです。この区別が、スムーズなパッチ展開とキャッシュスタンピードの分かれ目です。

3. ゲームバイナリにフォールバックアセットパックを同梱する。

最も重要な50〜100のアセット(UI、デフォルトスキン、ロビー環境)は、200〜500 MBの緊急パックとしてゲームインストール内に配置すべきです。サーキットブレーカーロジックは、CDNに到達できない場合にこれらにフォールバックします。

4. キャッシュヒット率はグローバルではなく地域ごとに監視する。

グローバルな97%のヒット率は、東南アジアの72%のヒット率を隠すことがあります。地域ごとの監視により、地域エッジスターべーションがプレイヤーからの報告インシデントになる前に発見できます。

5. CDNのロードテストはローンチ後ではなくローンチ前に行う。

k6、Locust、Vegetaなどのツールを使用して、CDNエンドポイントに対して想定されるローンチ当日のトラフィックパターンをシミュレーションしてください。50,000人の仮想ユーザーがマニフェストと上位20のアセットにアクセスする10分間のテストで、実際のプレイヤーよりも先にTTL設定ミス、シールディング欠如、オリジンボトルネックを発見できます。

# k6: simulate 50,000 concurrent players hitting the asset manifest
cat <<'EOF' > cdn_load_test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '2m', target: 10000 },  // Ramp to 10K VUs
    { duration: '3m', target: 50000 },  // Spike to 50K VUs
    { duration: '5m', target: 50000 },  // Sustain
    { duration: '2m', target: 0 },      // Ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<200'],   // 95th percentile under 200ms
    http_req_failed: ['rate<0.01'],     // Less than 1% errors
  },
};

export default function () {
  const manifestRes = http.get('https://cdn.yourgame.com/manifest.json');
  check(manifestRes, {
    'manifest status 200': (r) => r.status === 200,
    'manifest under 100ms': (r) => r.timings.duration < 100,
    'cache HIT': (r) => r.headers['Cf-Cache-Status'] === 'HIT',
  });

  // Simulate a player downloading 5 random assets
  for (let i = 0; i < 5; i++) {
    const assetPath = `assets/placeholder_${Math.floor(Math.random() * 100)}/mesh.bin`;
    const assetRes = http.get(`https://cdn.yourgame.com/${assetPath}`);
    check(assetRes, {
      'asset under 500ms': (r) => r.timings.duration < 500,
    });
  }

  sleep(1);
}
EOF

k6 run cdn_load_test.js

自前で構築するか、プラットフォームを使うか

上記の完全なマルチティアキャッシングアーキテクチャを構築することは、専任のインフラストラクチャエンジニアがいるチームにとっては十分に実現可能です。コンポーネントは十分にドキュメント化されており、CDNプロバイダーは生のプリミティブを提供しています。

しかし、チームが3人の開発者でゲームをリリースする場合、オリジンシールディング、リージョナルフェイルオーバー、アセットバージョニングパイプライン、クライアントのサーキットブレーカーロジックの構築に4〜6週間を費やすということは、ゲームプレイに4〜6週間を費やせないということです。horizOnは、バックエンドスタックの一部としてアセット配信インフラストラクチャを処理し、運用オーバーヘッドなしで同じマルチリージョンキャッシングと自動フェイルオーバーを提供します。アセットをアップロードすれば、プラットフォームがバージョニング、エッジ配信、ヘルスモニタリングを標準機能として処理します。

この記事のアーキテクチャ原則は、インフラストラクチャの選択にかかわらず重要です。なぜバージョン管理されたURLパスが重要なのか、なぜstale-while-revalidateがスタンピードを防ぐのか、なぜリージョナルオリジンがレイテンシを削減するのかを理解することで、Cloudflare Workersを手動で設定する場合でも、マネージドバックエンドサービスを評価する場合でも、情報に基づいた意思決定ができます。


次のステップ: 次のパッチの前にロードテストを実行する

次のパッチの日程を決めてください。2週間前に、上記のk6スクリプトをCDNエンドポイントに対して実行します。シミュレートされたローンチスケールでp95レイテンシが200msを超える場合、修正する時間があります。持続フェーズ中にキャッシュヒット率が90%を下回ることが判明した場合は、オリジンシールディングを有効化し、アセットTTLを延長してください。

スムーズなローンチとローンチ当日の大惨事の違いは、めったにゲームコードではありません。それは、すべてのプレイヤーが最初の5分間にダウンロードする2 GBのアセットを配信するインフラストラクチャです。それを正しくすれば、残りはゲームプレイだけです。

このダッシュボードは以下のチームによって愛情を込めて作られています Projectmakers

© 2026 projectmakers.de

unknown-v1.102.3 / unknown-v--