Skalowanie CDN dla assetów gier: runbook na przetrwanie skoków ruchu w dniu premiery
W skrócie
Zobacz, jak skutecznie skalować CDN dla assetów gier i przetrwać szczyty ruchu w dniu premiery dzięki sprawdzonym technikom z tego runbooka.
Twoja CDN ugnie się — oto jak rozpoznać ten moment
Każdy deweloper gier zna ten sam koszmar z dnia premiery: twoja strona na Steamie wystartowała, liczba graczy przekracza 10 000 jednoczesnych połączeń, a pobieranie tekstur nagle utyka na 200 ms p99 latencji zamiast zwykłych 12 ms. Gracze zgłaszają brakujące modele. Pobieranie patchy zawiesza się na 43%. Dashboard monitoringu zmienia kolor na czerwony, a ty nie masz pojęcia, która warstwa zawodzi.
To nie jest scenariusz hipotetyczny. cdnjs — jedna z najczęściej używanych otwartoźródłowych sieci CDN na świecie — niedawno zakończyła pełną migrację infrastruktury na platformę deweloperską Cloudflare, aby obsłużyć 9 miliardów żądań dziennie. Historia tej migracji ujawnia wzorce architektoniczne, które mają bezpośrednie zastosowanie do dostarczania assetów gier, gdzie pojedyncza aktualizacja pakietu tekstur 4K może wygenerować terabajty ruchu w ciągu minut.
Kluczowa lekcja: skalowanie CDN dla assetów gier nie polega na kupowaniu większego pasma. Chodzi o zaprojektowanie hierarchii cache, logiki fallback i osłony origin (origin shielding), aby skoki ruchu stały się niezdarzeniami zamiast awarii.
Ten runbook opisuje, co się psuje, gdy CDN osiąga nasycenie, jak wykryć nasycenie, zanim Discord wypełni się gniewem graczy, jak naprawić sytuację w produkcji i jak zaprojektować architekturę zapobiegającą nawrotom.
Co się psuje, gdy Twoja CDN osiąga nasycenie
Dostarczanie assetów gier ma unikalny profil ruchu w porównaniu do standardowych treści webowych. Zrozumienie trybów awarii wymaga zrozumienia tego profilu.
Problem kształtu ruchu
Typowa gra multiplayer od niezależnego studia charakteryzuje się następującymi wzorcami ruchu:
- Linia bazowa: 50-200 żądań/sek dla assetów lobby, sprite'ów UI, plików JSON konfiguracji
- Skok w dniu patcha: 15 000-80 000 żądań/sek w oknie 3 minut, gdy Steam uruchamia automatyczne aktualizacje
- Kaskady regionalne: gracze z Azji i Pacyfiku trafiają na CDN 8-12 godzin po Ameryce Północnej, tworząc drugą falę
- Eksplozja wersji assetów: każdy patch unieważnia zbuforowane obiekty, wymuszając pobrania z origin dla nowych hashy
Gdy cdnjs migrowała do infrastruktury Cloudflare, zmierzyła się z podobnym problemem eksplozji wersji. Ich wersjonowanie w stylu npm oznaczało, że każda aktualizacja biblioteki tworzyła nowe klucze cache, a przy 4 200+ bibliotekach aktualizowanych dziennie projekt osłony origin musiał obsługiwać ciągłą rotację cache — nie tylko treści statyczne.
Trzy tryby awarii
1. Nasycenie originu (Origin Pull Saturation)
Gdy cache na brzegu sieci (edge cache) nie trafia (nowy patch, zimny cache, wygaśnięcie cache), każde żądanie trafia na twój serwer origin. Pojedynczy origin z przepustowością 1 Gbps może obsłużyć około 1 250 jednoczesnych pobrań assetów o rozmiarze 1 MB. Przy 80 000 jednoczesnych graczy pobierających patch o rozmiarze 2 GB każdy potrzebujesz wydajności origin, której większość niezależnych konfiguracji po prostu nie ma.
2. Sztampeda cache (Cache Stampede)
Gdy najczęściej żądany asset wygasa z cache na brzegu sieci (błędna konfiguracja TTL, purge wyzwolony przez deploy), tysiące węzłów brzegowych jednocześnie żąda tego samego obiektu z origin. To problem „stada" (thundering herd), który w kilka sekund wali serwery origin.
3. Regionalne wygłodzenie brzegu sieci
Twoje węzły brzegowe w Ameryce Północnej są ciepłe. Twój węzeł brzegowy w Singapurze ma 60% współczynnik trafień cache, bo masz tylko 12 000 graczy z regionu APAC — dopóki YouTuber w Japonii nie poleci twojej gry i ta liczba nie skoczy do 300 000 z dnia na dzień. Węzeł brzegowy ściąga z origin na masową skalę, a gracze w APAC doświadczają czasów ładowania 2-4 sekund, podczas gdy gracze w NA widzą 40 ms.
Sygnały detekcji
# 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}'
Jeśli współczynnik braków cache przekracza 8% w dowolnym regionie w stanie ustalonym, dzieli cię jeden patch od zalewu originu.
Natychmiastowa naprawa: co zrobić teraz
Gdy CDN płonie, masz okno 15 minut, zanim gracze zaczną bombardować grę negatywnymi recenzjami. Oto sekwencja triage.
Krok 1: Aktywuj osłonę origin (Origin Shielding)
Większość dostawców CDN oferuje funkcję „origin shield" lub „shielding" — pośrednią warstwę cache pomiędzy węzłami brzegowymi a origin. Zamiast 200 węzłów brzegowych niezależnie uderzających w origin przy braku trafienia, tylko węzeł osłony kontaktuje się z origin i dystrybuuje odpowiedź.
Przykład konfiguracji (generyczny API CDN):
{
"shielding": {
"enabled": true,
"shield_region": "us-east-1",
"fallback_shield_region": "eu-west-1",
"shield_ttl_override": 86400,
"pass_on_shield_error": false
}
}
Ta pojedyncza zmiana może zredukować obciążenie originu o 95% podczas sztampedy cache. Migracja cdnjs opierała się na podobnej logice osłony — ich serwery origin odnotowały redukcję z milionów bezpośrednich pobrań do kilku tysięcy żądań z osłony na godzinę.
Krok 2: Wydłuż TTL assetów dla treści statycznych
Twoje tekstury 4K, banki audio i pliki mesh nie zmieniają się między patchami. Nie ma powodu, by TTL wynosił 1 godzinę.
# 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";
}
Kluczowy wgląd z architektury cdnjs: wersjonuj assety w ścieżce URL, nie w parametrach zapytania. Wiele węzłów CDN traktuje ?v=2 i ?v=3 jako ten sam klucz cache. Zamiast tego używaj /assets/v2/texture_pack.bin.
Krok 3: Włącz stale-while-revalidate
To pojedyncza konfiguracja o największym wpływie na ruch w dniu premiery. Gdy zbuforowany asset wygasa, CDN serwuje nieaktualną wersję graczowi, który o nią prosi, jednocześnie pobierając świeżą wersję w tle. Gracz otrzymuje odpowiedź w 12 ms zamiast 1 200 ms.
Cache-Control: public, max-age=3600, stale-while-revalidate=86400
To mówi CDN: „Ten asset jest świeży przez 1 godzinę. Po tym czasie serwuj nieaktualną wersję przez maksymalnie 24 godziny, podczas gdy rewalidujesz w tle."
Dla assetów gier, które nie są krytyczne dla bezpieczeństwa (tła lobby, podglądy kosmetyków, ścieżki audio), to rozwiązanie jest bezpieczne i drastycznie redukuje odczuwalne opóźnienie.
Krok 4: Zaimplementuj fallbacki z wyłącznikiem obwodu (Circuit Breaker)
Jeśli origin CDN jest naprawdę przeciążony, twój klient gry potrzebuje ścieżki łagodnej degradacji — a nie zamrożonego ekranu ładowania.
// 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>();
}
}
Ten wzorzec zapewnia, że gra pozostaje funkcjonalna nawet przy całkowitej awarii CDN. Gracze mogą widzieć tekstury o niższej rozdzielczości przez kilka minut, ale nadal mogą grać.
Prewencja: architektura wielopoziomowego cache
Naprawa ratuje cię w dniu premiery. Architektura sprawia, że nie będziesz jej potrzebować.
Wzorzec trzech poziomów
Migracja cdnjs do Cloudflare Workers zademonstrowała architekturę cache, która skaluje się do miliardów żądań. W adaptacji do assetów gier:
Poziom 1 — Cache brzegowy (PoP CDN)
- Obsługuje 95-99% żądań
- TTL: 365 dni dla wersjonowanych assetów, 60 sekund dla manifestów
- Obejmuje tekstury, meshe, audio, shadery
Poziom 2 — Osłona / cache środkowego poziomu
- Przechwytuje braki trafień z węzłów brzegowych
- TTL: taki sam jak na brzegu, ale działa jako proxy originu
- Redukuje obciążenie originu o 95%+
Poziom 3 — Serwer origin
- Generuje assety, podpisuje URL-e, serwuje manifesty
- Chroniony przez rate limiting i osłonę
- Powinien obsługiwać <0,1% całkowitego wolumenu ruchu
Wersjonowany pipeline assetów
Oto przepływ pracy wersjonowania assetów, który zapobiega burzom unieważniania 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
Przy takim podejściu:
- Stare assety nigdy nie są purgowane. Pozostają w cache na brzegu bezterminowo, ponieważ mają unikalne URL-e.
- Nowe assety otrzymują nowe URL-e. CDN automatycznie je buforuje przy pierwszym żądaniu.
- Jedyny plik, który się zmienia, to manifest. Mały plik JSON z TTL 60 sekund.
Dokładnie tak cdnjs obsługuje wersjonowanie bibliotek na dużą skalę. Każda wersja biblioteki otrzymuje unikalną ścieżkę URL, więc CDN nigdy nie potrzebuje operacji purge — najdroższej i najbardziej podatnej na błędy operacji CDN, jaka istnieje.
Ten wzorzec architektoniczny jest szczególnie ważny, jeśli prowadzisz dedykowane serwery, które muszą serwować dane konfiguracyjne wraz z logiką gry. Jak omówiliśmy w naszym poradniku jak opanować stripping assetów na dedykowanych serwerach Unreal Engine, oddzielenie statycznych assetów od danych krytycznych dla serwera to fundamentalna optymalizacja, która procentuje przy skalowaniu.
Dystrybucja geograficzna: rozwiązanie kaskad regionalnych
Migracja cdnjs ujawniła, że surowa liczba węzłów brzegowych jest mniej ważna niż inteligentny routing. Posiadanie 300 PoP-ów nic nie znaczy, jeśli logika routingu wysyła żądania APAC do originu w USA przy braku trafienia w cache.
Inteligentny wybór originu
{
"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"
}
]
}
Regionalne serwery origin kosztują 20-40 USD/miesiąc każdy u głównych dostawców chmurowych. Trzy regionalne originy kosztują mniej niż pojedynczy incydent, podczas którego twój origin w NA serwuje ruch APAC przez 4 godziny z obniżoną wydajnością — wraz z utratą graczy, która z tego wynika.
Ten rodzaj architektury wieloregionalnego failoveru odzwierciedla to, co omawiamy w naszej analizie architektury serwerów zero-waste ze strategiami hibernacji — zasadę niepłacenia za bezczynną infrastrukturę przy jednoczesnej gotowości do skalowania.
Najlepsze praktyki skalowania CDN dla assetów gier
1. Wersjonuj assety w ścieżkach URL, nie w parametrach zapytania.
/assets/{hash}/texture.bin gwarantuje unikalność cache. ?v=2 nie wystarczy — wiele węzłów CDN usuwa parametry zapytania z kluczy cache, co prowadzi do nieaktualnych treści lub zepsutych cache.
2. Oddziel TTL manifestu od TTL assetów.
Pliki manifestu powinny mieć TTL 30-60 sekund z stale-while-revalidate. Pliki assetów powinny mieć TTL 1 roku z immutable. Ta różnica to linia między płynnym wdrożeniem patcha a sztampedą cache.
3. Dołącz do binarek gry pakiet assetów zapasowych. 50-100 najbardziej krytycznych assetów (UI, domyślna skórka, środowisko lobby) powinno znajdować się w instalacji gry jako pakiet awaryjny o rozmiarze 200-500 MB. Twoja logika wyłącznika obwodu sięga po nie, gdy CDN jest nieosiągalna.
4. Monitoruj współczynnik trafień cache według regionów, nie globalnie. Globalny wskaźnik 97% trafień może maskować 72% wskaźnik w Azji Południowo-Wschodniej. Monitoring per-regionalny pozwala dostrzec regionalne wygłodzenie brzegu sieci, zanim stanie się incydentem zgłoszonym przez graczy.
5. Testuj obciążeniowo CDN przed premierą, nie w trakcie. Używaj narzędzi takich jak k6, Locust lub Vegeta, aby symulować oczekiwany wzorzec ruchu z dnia premiery na twoim punkcie końcowym CDN. 10-minutowy test z 50 000 wirtualnych użytkowników uderzających w manifest + 20 najważniejszych assetów ujawni błędnie skonfigurowane TTL-e, brakującą osłonę i wąskie gardła originu, zanim zrobią to prawdziwi gracze.
# 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
Kiedy zbudować własne rozwiązanie, a kiedy użyć platformy
Zbudowanie pełnej wielopoziomowej architektury cache opisanej powyżej jest w pełni wykonalne dla zespołu z dedykowanymi inżynierami infrastruktury. Komponenty są dobrze udokumentowane, a dostawcy CDN oferują surowe prymitywy.
Ale jeśli twój zespół to trzech deweloperów wydających grę, spędzenie 4-6 tygodni na budowaniu osłony origin, regionalnego failoveru, wersjonowania assetów i logiki wyłącznika obwodu w kliencie oznacza 4-6 tygodni nie poświęconych na gameplay. horizOn obsługuje infrastrukturę dostarczania assetów jako część swojego backendu, dając ci to samo wieloregionalne buforowanie i automatyczny failover bez narzutu operacyjnego. Wgrywasz assety; platforma od ręki obsługuje wersjonowanie, dystrybucję brzegową i monitoring zdrowia.
Zasady architektoniczne z tego artykułu pozostają kluczowe niezależnie od wyboru infrastruktury. Zrozumienie, dlaczego wersjonowane ścieżki URL mają znaczenie, dlaczego stale-while-revalidate zapobiega sztampom i dlaczego regionalne originy redukują opóźnienia, pozwala podejmować świadome decyzje — niezależnie od tego, czy konfigurujesz Cloudflare Workers ręcznie, czy oceniasz zarządzaną usługę backendową.
Następny krok: uruchom test obciążeniowy przed następnym patchem
Wybierz datę następnego patcha. Dwa tygodnie wcześniej uruchom powyższy skrypt k6 na swoim punkcie końcowym CDN. Jeśli p95 latencji przekracza 200 ms przy symulowanej skali premiery, masz czas na naprawę. Jeśli odkryjesz, że współczynnik trafień cache spada poniżej 90% podczas fazy utrzymania, aktywuj osłonę origin i wydłuż TTL-e assetów.
Różnica między płynną premierą a katastrofą w dniu premiery rzadko leży w kodzie gry. To infrastruktura, która serwuje 2 GB assetów pobieranych przez każdego gracza w pierwszych pięciu minutach. Dopracuj ją, a reszta to gameplay.