Mise à l'échelle du CDN pour les assets de jeu : un runbook pour survivre aux pics de trafic du jour de lancement
En bref
Découvrez comment scaler votre CDN pour les assets de jeu et survivre aux pics du jour de lancement grâce à ce runbook complet spécial développeurs.
Votre CDN va flancher — voici comment savoir quand
Chaque développeur de jeu redoute le même scénario de lancement : votre page Steam est en ligne, le nombre de joueurs dépasse 10 000 simultanés, et soudain les téléchargements de textures plafonnent à 200 ms de latence p99 au lieu des 12 ms habituelles. Les joueurs signalent des modèles manquants. Les téléchargements de patchs restent bloqués à 43 %. Votre tableau de bord de surveillance passe au rouge, et vous n'avez aucune idée de la couche qui pose problème.
Ce n'est pas une hypothèse. cdnjs — l'un des réseaux CDN open source les plus utilisés au monde — a récemment achevé une migration complète de son infrastructure vers la plateforme développeur de Cloudflare pour gérer 9 milliards de requêtes par jour. Cette migration révèle des schémas architecturaux qui s'appliquent directement à la livraison d'assets de jeu, où la mise à jour d'un simple pack de textures 4K peut générer des téraoctets de trafic en quelques minutes.
La leçon essentielle : mettre à l'échelle un CDN pour des assets de jeu ne consiste pas à acheter plus de bande passante. Il s'agit de concevoir des hiérarchies de cache, des logiques de repli et un origin shielding pour que les pics de trafic deviennent des non-événements plutôt que des pannes.
Ce runbook explique ce qui casse quand votre CDN sature, comment détecter la saturation avant que votre Discord ne se remplisse de rage, comment remédier en production, et comment architecturer pour éviter que cela ne se reproduise.
Ce qui casse quand votre CDN sature
La livraison d'assets de jeu a un profil de trafic unique par rapport au contenu web standard. Comprendre les modes de défaillance implique de comprendre ce profil.
Le problème de la forme du trafic
Un jeu multijoueur indépendant typique rencontre ces schémas de trafic :
- Ligne de base : 50 à 200 requêtes/s pour les assets du lobby, les sprites d'interface et le JSON de configuration
- Pic du jour de patch : 15 000 à 80 000 requêtes/s dans une fenêtre de 3 minutes lorsque Steam déclenche les mises à jour automatiques
- Cascades régionales : les joueurs d'Asie-Pacifique atteignent le CDN 8 à 12 heures après l'Amérique du Nord, créant une deuxième vague
- Explosion des versions d'assets : chaque patch invalide les objets en cache, forçant des pulls vers l'origine pour les nouveaux hashs
Lorsque cdnjs a migré vers l'infrastructure de Cloudflare, ils ont été confrontés à un problème similaire d'explosion de versions. Leur versionnage de type npm signifiait que chaque mise à jour de bibliothèque créait de nouvelles clés de cache, et avec plus de 4 200 bibliothèques mises à jour quotidiennement, la conception de l'origin shielding devait gérer un renouvellement continu du cache — pas seulement du contenu statique.
Les trois modes de défaillance
1. Saturation des origin pulls
Quand votre cache edge est en échec (nouveau patch, cache froid, expiration du cache), chaque requête atteint votre serveur d'origine. Une seule origine avec un débit de 1 Gbps peut servir environ 1 250 téléchargements simultanés d'assets de 1 Mo. Avec 80 000 joueurs simultanés téléchargeant chacun un patch de 2 Go, il vous faut une capacité d'origine que la plupart des configurations indie n'ont tout simplement pas.
2. Cache stampede
Quand votre asset le plus demandé expire du cache edge (mauvaise configuration TTL, purge déclenchée par un déploiement), des milliers de nœuds edge demandent simultanément le même objet à l'origine. C'est le problème du « troupeau tonitruant », et il fait s'écrouler les origines en quelques secondes.
3. Famine régionale du edge
Vos nœuds edge en Amérique du Nord sont chauds. Votre nœud edge à Singapour a un taux de hit de 60 % parce que vous n'avez que 12 000 joueurs APAC — jusqu'à ce qu'un YouTuber au Japon mette en avant votre jeu et que ce nombre passe à 300 000 du jour au lendemain. Le nœud edge tire depuis l'origine à grande échelle, et les joueurs APAC subissent des temps de chargement de 2 à 4 secondes tandis que les joueurs NA voient 40 ms.
Signaux de détection
# 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}'
Si votre taux de cache miss dépasse 8 % dans une région quelconque pendant une période stable, vous êtes à un patch d'une inondation de l'origine.
Remédiation immédiate : que faire maintenant
Quand le CDN est en feu, vous avez une fenêtre de 15 minutes avant que les joueurs commencent à bombarder de mauvaises critiques. Voici la séquence de triage.
Étape 1 : Activer l'origin shielding
La plupart des fournisseurs de CDN proposent une fonctionnalité « origin shield » ou « shielding » — une couche de cache intermédiaire entre vos nœuds edge et votre origine. Au lieu que 200 nœuds edge atteignent chacun l'origine indépendamment lors d'un cache miss, seul le nœud shield contacte l'origine et distribue la réponse.
Configuration d'exemple (API CDN générique) :
{
"shielding": {
"enabled": true,
"shield_region": "us-east-1",
"fallback_shield_region": "eu-west-1",
"shield_ttl_override": 86400,
"pass_on_shield_error": false
}
}
Ce seul changement peut réduire la charge de l'origine de 95 % pendant un cache stampede. La migration de cdnjs s'est appuyée sur une logique de shielding similaire — leurs serveurs d'origine ont vu leurs pulls directs passer de millions à quelques milliers de requêtes par heure provenant du shield.
Étape 2 : Étendre les TTL des assets pour le contenu statique
Vos textures 4K, banques audio et fichiers de maillage ne changent pas entre les patches. Aucune raison d'avoir un TTL d'une heure.
# 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";
}
Le point clé de l'architecture de cdnjs : versionnez vos assets dans le chemin de l'URL, pas avec des query strings. De nombreux nœuds CDN traitent ?v=2 et ?v=3 comme la même clé de cache. Utilisez plutôt /assets/v2/texture_pack.bin.
Étape 3 : Activer stale-while-revalidate
C'est la configuration la plus impactante pour le trafic du jour de lancement. Quand un asset en cache expire, le CDN sert la version stale au joueur qui fait la requête tout en récupérant la version fraîche en arrière-plan. Le joueur obtient une réponse en 12 ms au lieu de 1 200 ms.
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
Cela indique au CDN : « Cet asset est frais pendant 1 heure. Ensuite, servez la version stale jusqu'à 24 heures pendant que vous revalidez en arrière-plan. »
Pour les assets de jeu non critiques pour la sécurité (fonds de lobby, aperçus cosmétiques, stems audio), c'est sûr et réduit considérablement la latence perçue.
Étape 4 : Implémenter des replis par circuit breaker
Si l'origine du CDN est vraiment submergée, votre client de jeu a besoin d'une dégradation gracieuse — pas d'un écran de chargement figé.
// 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>();
}
}
Ce modèle garantit que votre jeu reste fonctionnel même quand le CDN est complètement down. Les joueurs verront peut-être des textures en basse résolution pendant quelques minutes, mais ils pourront toujours jouer.
Prévention : l'architecture de cache multi-niveaux
La remédiation vous sauve le jour du lancement. L'architecture vous évite d'en avoir besoin.
Le modèle à trois niveaux
La migration de cdnjs vers Cloudflare Workers a démontré une architecture de cache qui passe à l'échelle pour des milliards de requêtes. Adaptée aux assets de jeu :
Niveau 1 — Cache edge (PoPs du CDN)
- Sert 95 à 99 % des requêtes
- TTL : 365 jours pour les assets versionnés, 60 secondes pour les manifests
- Couvre textures, maillages, audio, shaders
Niveau 2 — Cache shield / intermédiaire
- Intercepte les cache misses des nœuds edge
- TTL : identique au edge, mais agit comme proxy de l'origine
- Réduit la charge de l'origine de 95 %+
Niveau 3 — Serveur d'origine
- Génère les assets, signe les URLs, sert les manifests
- Protégé par rate limiting et shielding
- Devrait voir moins de 0,1 % du volume total de trafic
Pipeline d'assets versionnés
Voici le workflow de versionnage d'assets qui prévient les tempêtes d'invalidation de cache :
# 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
Avec cette approche :
- Les anciens assets ne sont jamais purgés. Ils restent en cache au niveau du edge indéfiniment car ils ont des URLs uniques.
- Les nouveaux assets obtiennent de nouvelles URLs. Le CDN les met automatiquement en cache à la première requête.
- Le seul fichier qui change est le manifest. Un petit fichier JSON avec un TTL de 60 secondes.
C'est exactement ainsi que cdnjs gère le versionnage de bibliothèques à grande échelle. Chaque version de bibliothèque obtient un chemin d'URL unique, donc le CDN n'a jamais besoin d'opérations de purge — l'opération CDN la plus coûteuse et la plus sujette aux erreurs qui existe.
Ce schéma architectural est particulièrement important si vous exécutez des serveurs dédiés qui doivent servir des données de configuration en plus de la logique de jeu. Comme nous l'avons vu dans notre guide sur comment maîtriser l'asset stripping des serveurs dédiés Unreal Engine, séparer les assets statiques des données critiques du serveur est une optimisation fondamentale dont les effets se cumulent à l'échelle.
Distribution géographique : résoudre la cascade régionale
La migration de cdnjs a révélé que le nombre brut de nœuds edge est moins important que le routage intelligent. Avoir 300 PoPs ne sert à rien si la logique de routage envoie les requêtes APAC vers une origine américaine lors d'un cache miss.
Sélection intelligente de l'origine
{
"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"
}
]
}
Les serveurs d'origine régionaux coûtent 20 à 40 $/mois chacun sur les principaux fournisseurs cloud. Trois origines régionales coûtent moins cher qu'un seul incident où votre origine NA sert le trafic APAC pendant 4 heures avec des performances dégradées — sans compter les joueurs perdus.
Ce type d'architecture de basculement multi-régions fait écho à ce que nous analysons dans notre article sur l'architecture de serveurs zéro gaspillage avec des stratégies d'hibernation — le principe de ne pas payer pour une infrastructure inactive tout en restant prêt à passer à l'échelle.
Meilleures pratiques pour mettre à l'échelle un CDN pour les assets de jeu
1. Versionnez les assets dans les chemins d'URL, pas dans les query strings.
/assets/{hash}/texture.bin garantit l'unicité du cache. ?v=2 ne le garantit pas — de nombreux nœuds CDN retirent les paramètres de requête des clés de cache, ce qui donne du contenu stale ou des caches cassés.
2. Séparez le TTL de votre manifest du TTL de vos assets.
Les fichiers manifest devraient avoir un TTL de 30 à 60 secondes avec stale-while-revalidate. Les fichiers d'assets devraient avoir un TTL d'un an avec immutable. Cette distinction fait la différence entre un déploiement de patch fluide et un cache stampede.
3. Incluez un pack d'assets de secours avec votre binaire de jeu. Les 50 à 100 assets les plus critiques (interface, skin par défaut, environnement du lobby) devraient se trouver dans votre installation de jeu sous forme de pack d'urgence de 200 à 500 Mo. Votre logique de circuit breaker se replie dessus quand le CDN est injoignable.
4. Surveillez le taux de cache hit par région, pas globalement. Un taux de hit global de 97 % peut masquer un taux de 72 % en Asie du Sud-Est. Une surveillance par région vous permet de repérer la famine régionale du edge avant qu'elle ne devienne un incident signalé par les joueurs.
5. Testez la charge de votre CDN avant le lancement, pas pendant. Utilisez des outils comme k6, Locust ou Vegeta pour simuler votre schéma de trafic attendu le jour du lancement contre votre endpoint CDN. Un test de 10 minutes avec 50 000 utilisateurs virtuels qui frappent votre manifest + les 20 principaux assets révélera des TTL mal configurés, un shielding manquant et des goulots d'étranglement d'origine avant les vrais joueurs.
# 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
Quand le construire soi-même plutôt qu'utiliser une plateforme
Construire l'architecture de cache multi-niveaux complète décrite ci-dessus est tout à fait faisable pour une équipe avec des ingénieurs infrastructure dédiés. Les composants sont bien documentés et les fournisseurs de CDN offrent les primitives de base.
Mais si votre équipe est composée de trois développeurs qui livrent un jeu, passer 4 à 6 semaines à construire origin shielding, basculement régional, pipelines de versionnage d'assets et logique de circuit breaker dans votre client signifie 4 à 6 semaines non consacrées au gameplay. horizOn gère l'infrastructure de livraison d'assets dans le cadre de sa stack backend, offrant le même cache multi-régions et le même basculement automatique sans la charge opérationnelle. Vous uploadez vos assets ; la plateforme gère le versionnage, la distribution edge et la surveillance de santé out of the box.
Les principes architecturaux de cet article restent essentiels quel que soit votre choix d'infrastructure. Comprendre pourquoi les chemins d'URL versionnés comptent, pourquoi stale-while-revalidate évite les stampedes, et pourquoi les origines régionales réduisent la latence vous permet de prendre des décisions éclairées — que vous configuriez Cloudflare Workers à la main ou que vous évaluiez un service backend managé.
Prochaine étape : lancez le test de charge avant votre prochain patch
Choisissez la date de votre prochain patch. Deux semaines avant, exécutez le script k6 ci-dessus contre votre endpoint CDN. Si votre latence p95 dépasse 200 ms à l'échelle de lancement simulée, vous avez le temps de corriger. Si vous constatez que votre taux de cache hit tombe sous 90 % pendant la phase soutenue, activez l'origin shielding et étendez vos TTL d'assets.
La différence entre un lancement fluide et un désastre le jour du lancement tient rarement au code du jeu. C'est l'infrastructure qui sert les 2 Go d'assets que chaque joueur télécharge dans les cinq premières minutes. Si vous réussissez cela, le reste n'est que gameplay.