Boty przetrwały awarię Yandex — Twoja architektura backendu gry też powinna
W skrócie
Poznaj wnioski z awarii Yandex i zabezpiecz swoją architekturę backendu gry przed migracją botów dzięki detekcji behawioralnej i failoverowi.
Twój dashboard monitoringu pokazuje, że ruch spadł o 40% w ciągu nocy. Z ulgą wzdychasz — atak DDoS musiał się skończyć. Jednak analiza Cloudflare dotycząca awarii Yandex z października 2023 roku dowodzi, że to założenie było katastrofalnie błędne: złośliwe boty nie zginęły, gdy centra danych Yandex zgasły. W ciągu kilku minut przeniosły się do OVH, Hetzner i DigitalOcean. Twoje zagregowane metryki kłamały, a atakujący działali dalej.
Jeśli Twój backend gry działa u jednego dostawcy chmury z detekcją botów opartą na IP, dzieli Cię jedno zdarzenie infrastrukturalne od tego samego martwego pola. Ten runbook pokazuje, co awaria Yandex ujawnia o zachowaniu botów podczas awarii infrastruktury i jak projektować backendy gier, które je przetrwają.
Co się psuje, gdy dostawca infrastruktury znika
Kiedy duży dostawca infrastruktury ma awarię, cztery kluczowe systemy zawodzą jednocześnie — i zawodzą w sposób, który dla naiwnego monitoringu wygląda jak poprawa.
Twoja warstwa uwierzytelniania przestaje działać. Jeśli uwierzytelnianie graczy zależy od usługi tożsamości jednego dostawcy, gracze nie mogą się zalogować. Podczas awarii Yandex usługi ściśle powiązane z infrastrukturą Yandex doświadczyły całkowitej przerwy w działaniu. Gracze widzieli ekrany błędów; boty, które się nie uwierzytelniają, nadal atakowały endpointy gdzie indziej.
Twoja detekcja botów traci punkt odniesienia. Większość backendów gier śledzi złośliwe IP według ASN lub dostawcy hostingu. Gdy te IP zgasną, systemy detekcji oznaczają je jako „rozwiązane” i przestają szukać. W międzyczasie ta sama flota botów pojawia się na nowych IP u innego dostawcy.
Twój rate limiting staje się bezużyteczny. Limitowanie oparte na IP jest powiązane z konkretnymi zakresami adresów. Gdy boty migrują, dostają świeże IP z czystą reputacją. Twój rate limiter traktuje je jak nowy, legalny ruch.
Twoje zagregowane dane o ruchu stają się dezinformacją. Analiza Cloudflare dotycząca Yandex pokazała, że ruch AS13238 (Yandex Search) składał się prawie wyłącznie z legalnych crawlerów — spadek tam był realny. Ale w AS200350 (Yandex.Cloud) złośliwe skanowanie podatności i sondowanie spadło, podczas gdy zautomatyzowany ruch o dużej skali po prostu się przeniósł. Zagregowane wykresy ruchu pokazujące spadek o 99% opowiadały Ci błędną historię.
Sedno problemu: Twój monitoring myśli, że jest lepiej, podczas gdy zagrożenie po prostu się przeniosło.
Jak to wykrywać: dostrzeganie migracji botów w czasie rzeczywistym
Wykrywanie wymaga monitorowania na wielu warstwach, nie tylko zagregowanego wolumenu ruchu. Każda warstwa wyłapuje inny aspekt wzorca migracji.
Warstwa 1: korelacja źródeł ruchu
Zamiast śledzić całkowitą liczbę żądań na sekundę, monitoruj rozkład źródeł ruchu między numerami systemów autonomicznych (ASN). Gdy ruch jednego dostawcy spada, sprawdź, czy ruch innych dostawców rośnie proporcjonalnie. Skorelowany wzorzec spadku i wzrostu między ASN to sygnatura migracji botów.
Oto detektor w Pythonie, który identyfikuje ten wzorzec:
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, List, Optional
@dataclass
class TrafficSnapshot:
timestamp: float
source_asn: str
request_count: int
unique_ips: int
error_rate: float
class BotMigrationDetector:
"""Detects correlated traffic drops and rises across ASNs
to identify bot farm migration during infrastructure failures."""
def __init__(self, migration_threshold: float = 0.3):
self.history: Dict[str, List[TrafficSnapshot]] = defaultdict(list)
self.migration_threshold = migration_threshold
self.window_seconds = 300 # 5-minute rolling window
def record_snapshot(self, snapshot: TrafficSnapshot):
self.history[snapshot.source_asn].append(snapshot)
self._prune_old_data()
def _prune_old_data(self):
cutoff = time.time() - self.window_seconds
for asn in self.history:
self.history[asn] = [
s for s in self.history[asn]
if s.timestamp > cutoff
]
def detect_migration(self) -> Optional[Dict]:
"""Returns migration report if suspicious pattern detected."""
declining_asns = []
rising_asns = []
for asn, snapshots in self.history.items():
if len(snapshots) < 2:
continue
mid = len(snapshots) // 2
first_half = snapshots[:mid]
second_half = snapshots[mid:]
avg_first = sum(s.request_count for s in first_half) / len(first_half)
avg_second = sum(s.request_count for s in second_half) / len(second_half)
if avg_first > 0:
change_ratio = (avg_second - avg_first) / avg_first
if change_ratio < -self.migration_threshold:
declining_asns.append({
'asn': asn,
'drop_pct': abs(change_ratio) * 100,
'baseline': avg_first
})
elif change_ratio > self.migration_threshold:
rising_asns.append({
'asn': asn,
'rise_pct': change_ratio * 100,
'current': avg_second
})
if declining_asns and rising_asns:
return {
'type': 'bot_migration',
'declining': declining_asns,
'rising': rising_asns,
'confidence': self._calculate_confidence(declining_asns, rising_asns)
}
return None
def _calculate_confidence(self, declining, rising) -> float:
"""Higher confidence when drop and rise magnitudes match."""
total_drop = sum(d['drop_pct'] for d in declining)
total_rise = sum(r['rise_pct'] for r in rising)
if total_drop == 0:
return 0.0
ratio = min(total_rise, total_drop) / max(total_rise, total_drop)
return round(ratio * 100, 2)
# Usage: detect Yandex-style migration
detector = BotMigrationDetector(migration_threshold=0.25)
# Yandex traffic collapsing
detector.record_snapshot(TrafficSnapshot(
timestamp=time.time() - 240,
source_asn="AS13238", request_count=15000,
unique_ips=8500, error_rate=0.02
))
detector.record_snapshot(TrafficSnapshot(
timestamp=time.time() - 120,
source_asn="AS13238", request_count=1200, # 92% drop
unique_ips=900, error_rate=0.45
))
# OVH traffic surging (bots migrated here)
detector.record_snapshot(TrafficSnapshot(
timestamp=time.time() - 240,
source_asn="AS16276", request_count=5000,
unique_ips=3200, error_rate=0.01
))
detector.record_snapshot(TrafficSnapshot(
timestamp=time.time() - 120,
source_asn="AS16276", request_count=18500, # 270% increase
unique_ips=2800, error_rate=0.08
))
migration = detector.detect_migration()
if migration:
print(f"Bot migration detected: {migration['confidence']}% confidence")
for d in migration['declining']:
print(f" {d['asn']}: -{d['drop_pct']:.0f}%")
for r in migration['rising']:
print(f" {r['asn']}: +{r['rise_pct']:.0f}%")
Ten detektor obserwuje skorelowane spadki i wzrosty ruchu w wielu ASN. Gdy AS13238 spada o 92%, a AS16276 jednocześnie rośnie o 270%, oznacza prawdopodobną migrację botów, a nie zmiany w legalnym ruchu. Wynik ufności rośnie, gdy wielkości spadku i wzrostu są zbliżone — to cecha charakterystyczna skoordynowanej migracji.
Warstwa 2: fingerprinting behawioralny
Nie śledź tylko tego, skąd pochodzi ruch — śledź, jak się zachowuje. Boty migrujące między dostawcami zachowują identyczne sygnatury behawioralne: te same interwały żądań, wzorce nagłówków, czasy trwania sesji i zachowanie przy ponawianiu po błędach. Reputacja IP resetuje się, gdy bot się przenosi; zachowanie nie.
Zbuduj odcisk behawioralny dla każdej sesji i zahashuj go:
import hashlib
import statistics
def fingerprint_session(requests: list[dict]) -> str:
"""Generate a behavioral fingerprint from a session's request pattern."""
intervals = []
for i in range(1, len(requests)):
delta = requests[i]['timestamp'] - requests[i-1]['timestamp']
intervals.append(round(delta, 3))
if len(intervals) < 2:
return "insufficient_data"
# Key behavioral features
interval_mean = round(statistics.mean(intervals), 3)
interval_stdev = round(statistics.stdev(intervals), 3)
total_duration = round(requests[-1]['timestamp'] - requests[0]['timestamp'], 3)
error_count = sum(1 for r in requests if r.get('status_code', 200) >= 400)
retry_count = sum(1 for r in requests if r.get('is_retry', False))
unique_paths = len(set(r['path'] for r in requests))
user_agent_count = len(set(r.get('user_agent', '') for r in requests))
fingerprint_data = (
f"{interval_mean}|{interval_stdev}|{total_duration}|"
f"{error_count}|{retry_count}|{unique_paths}|{user_agent_count}"
)
return hashlib.sha256(fingerprint_data.encode()).hexdigest()[:16]
# Example: same bot session detected at two different providers
session_provider_a = [
{'timestamp': 1000.0, 'path': '/api/login', 'status_code': 200},
{'timestamp': 1000.5, 'path': '/api/profile', 'status_code': 200},
{'timestamp': 1001.0, 'path': '/api/match/join', 'status_code': 429},
{'timestamp': 1001.5, 'path': '/api/match/join', 'status_code': 200, 'is_retry': True},
{'timestamp': 1002.0, 'path': '/api/score', 'status_code': 200},
]
session_provider_b = [
{'timestamp': 2000.0, 'path': '/api/login', 'status_code': 200},
{'timestamp': 2000.5, 'path': '/api/profile', 'status_code': 200},
{'timestamp': 2001.0, 'path': '/api/match/join', 'status_code': 429},
{'timestamp': 2001.5, 'path': '/api/match/join', 'status_code': 200, 'is_retry': True},
{'timestamp': 2002.0, 'path': '/api/score', 'status_code': 200},
]
fp_a = fingerprint_session(session_provider_a)
fp_b = fingerprint_session(session_provider_b)
print(f"Provider A fingerprint: {fp_a}")
print(f"Provider B fingerprint: {fp_b}")
print(f"Same bot: {fp_a == fp_b}")
Gdy widzisz ten sam odcisk behawioralny na nowych IP u innego dostawcy krótko po tym, jak pierwotny dostawca padł, to potwierdzona migracja — nie przypadek.
Warstwa 3: monitorowanie stanu uwierzytelniania
Śledź w czasie stosunek żądań uwierzytelnionych do nieuwierzytelnionych. Podczas awarii infrastruktury prawdziwi gracze mają problemy z uwierzytelnieniem (przesuwając stosunek w stronę nieuwierzytelnionych), podczas gdy boty, które nigdy się nie uwierzytelniają, nadal bez zmian atakują Twoje endpointy.
Ta warstwa jest szczególnie cenna, bo oddziela prawdziwych graczy od zautomatyzowanego ruchu niezależnie od dostawcy źródłowego. Jeśli używasz sesji przypisanych do konta horizOn z wieloma metodami uwierzytelniania (e-mail, Google, Apple, anonimowe), możesz monitorować skuteczność uwierzytelniania dla każdej metody. Gdy uwierzytelnianie e-mail zawodzi, a Google działa, zidentyfikowałeś częściową awarię infrastruktury — nie migrację botów. Gdy całe uwierzytelnianie działa, ale nieuwierzytelniony ruch skacze u nowego dostawcy, to migracja.
Jak naprawić: strategie failover, które działają
Wykrywanie bez naprawy to tylko obserwowanie, jak Twoja gra umiera w czasie rzeczywistym. Oto trzy mechanizmy failover, które możesz wdrożyć w tym tygodniu.
Failover oparty na DNS
Skonfiguruj DNS z krótkim TTL i health checkami. Gdy Twój główny region zawiedzie, DNS automatycznie kieruje ruch do regionów zapasowych. To nie wymaga żadnych zmian w aplikacji.
Zalecane ustawienia:
- TTL: 60–120 sekund (krótszy oznacza szybszy failover, ale większe obciążenie DNS)
- Interwał health checku: 10–15 sekund
- Endpoint health checku:
/healthzwracający 200 z czasem odpowiedzi < 500 ms - Wyzwalacz failover: 3 kolejne nieudane health checki
Kompromis: krótkie TTL zwiększają liczbę zapytań DNS o 10–20x podczas normalnego działania. Dla gry z 50 000 dziennie aktywnych graczy to koszt rzędu 2–5 USD miesięcznie za DNS — pomijalny w porównaniu z przestojem, któremu zapobiega.
Circuit breakery na poziomie aplikacji
Zaimplementuj circuit breakery w kliencie gry, które wykrywają, gdy usługa zawodzi, i automatycznie kierują ruch do alternatyw:
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class ServiceCircuitBreaker
{
private readonly string _primaryEndpoint;
private readonly string[] _fallbackEndpoints;
private readonly int _failureThreshold;
private int _failureCount;
private DateTime _lastFailure;
private CircuitState _state = CircuitState.Closed;
private int _currentFallbackIndex;
private readonly object _lock = new();
public enum CircuitState { Closed, Open, HalfOpen }
public ServiceCircuitBreaker(
string primary, string[] fallbacks, int failureThreshold = 5)
{
_primaryEndpoint = primary;
_fallbackEndpoints = fallbacks;
_failureThreshold = failureThreshold;
}
public async Task<HttpResponseMessage> SendRequest(HttpRequestMessage request)
{
lock (_lock)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow - _lastFailure > TimeSpan.FromSeconds(30))
_state = CircuitState.HalfOpen;
else
return SendToFallback(request).GetAwaiter().GetResult();
}
}
try
{
using var client = new HttpClient();
client.Timeout = TimeSpan.FromSeconds(5);
var response = await client.SendAsync(request);
if (response.IsSuccessStatusCode)
{
lock (_lock) { _failureCount = 0; _state = CircuitState.Closed; }
return response;
}
RecordFailure();
return await SendToFallback(request);
}
catch (TaskCanceledException)
{
RecordFailure();
return await SendToFallback(request);
}
}
private void RecordFailure()
{
lock (_lock)
{
_failureCount++;
_lastFailure = DateTime.UtcNow;
if (_failureCount >= _failureThreshold)
_state = CircuitState.Open;
}
}
private async Task<HttpResponseMessage> SendToFallback(HttpRequestMessage request)
{
for (int i = 0; i < _fallbackEndpoints.Length; i++)
{
int idx = (_currentFallbackIndex + i) % _fallbackEndpoints.Length;
try
{
var fallbackRequest = new HttpRequestMessage(
request.Method,
_fallbackEndpoints[idx] + request.RequestUri.PathAndQuery
);
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var response = await client.SendAsync(fallbackRequest);
if (response.IsSuccessStatusCode)
{
_currentFallbackIndex = idx;
return response;
}
}
catch { continue; }
}
throw new HttpRequestException("All endpoints failed");
}
}
// Usage in your game client
var breaker = new ServiceCircuitBreaker(
primary: "https://api-primary.game.example.com",
fallbacks: new[] {
"https://api-secondary.game.example.com",
"https://api-tertiary.game.example.com"
},
failureThreshold: 3
);
var request = new HttpRequestMessage(HttpMethod.Get, "/api/leaderboard/global");
var response = await breaker.SendRequest(request);
Ten wzorzec zapobiega kaskadowym awariom, gdy jeden dostawca pada. Jak omówiliśmy w naszej analizie projektowania fallbacków serwerowych, automatyczny failover to różnica między 30-sekundowym zacięciem a 4-godzinnym przestojem.
Rozproszony rate limiting
Gdy ruch przełącza się do nowego regionu, Twój rate limiting też musi tam działać. Limitowanie oparte na IP, lokalne dla każdego serwera, staje się bezużyteczne podczas failover, bo ten sam bot dostaje czystą kartę na każdym endpointcie.
Zaimplementuj rozproszony rate limiting przy użyciu współdzielonego magazynu dostępnego ze wszystkich regionów:
import redis
import time
class DistributedRateLimiter:
"""Sliding window rate limiter backed by Redis.
Works identically across all deployment regions."""
def __init__(self, redis_url: str, max_requests: int = 100,
window_seconds: int = 60):
self.redis = redis.from_url(redis_url)
self.max_requests = max_requests
self.window = window_seconds
def is_allowed(self, identifier: str) -> tuple[bool, int]:
"""Returns (allowed, remaining_requests)."""
key = f"ratelimit:{identifier}"
now = time.time()
window_start = now - self.window
pipe = self.redis.pipeline()
pipe.zremrangebyscore(key, 0, window_start) # Remove expired entries
pipe.zadd(key, {f"{now}:{id(identifier)}": now}) # Add current request
pipe.zcard(key) # Count requests in window
pipe.expire(key, self.window)
results = pipe.execute()
request_count = results[2]
remaining = max(0, self.max_requests - request_count)
if request_count > self.max_requests:
# Remove the request we just added — it's over limit
pipe.zremrangebyscore(key, now, now)
pipe.execute()
return False, 0
return True, remaining
# Usage: same limiter works from any region
limiter = DistributedRateLimiter(
redis_url="redis://shared-redis.internal:6379",
max_requests=60, # 1 request per second average
window_seconds=60
)
# Identify by player ID, not IP — survives migration
allowed, remaining = limiter.is_allowed("player:8f3a2b1c")
Zwróć uwagę, że identyfikator używa ID gracza, a nie adresu IP. Boty migrujące na nowe IP wciąż niosą ten sam odcisk gracza. To podejście wyłapuje je nawet po migracji na poziomie dostawcy.
Jak zapobiegać: wzorce architektury dla odporności
Naprawianie jest reaktywne. Zapobieganie oznacza budowanie architektury, która zakłada, że awaria infrastruktury nastąpi — bo nastąpi.
Wzorzec 1: wdrożenie u wielu dostawców dla kluczowych usług
Wdróż uwierzytelnianie, matchmaking i detekcję botów u co najmniej dwóch dostawców chmury. To nie znaczy, że cały stack działa podwójnie — chodzi o to, by usługi, które muszą przetrwać awarię, były niezależne od dostawcy.
Realny koszt: Wdrożenie u wielu dostawców dla kluczowych usług zwykle zwiększa rachunek za infrastrukturę o 30–50%. Dla backendu gry obsługującego 10 000 równoczesnych graczy to dodatkowe około 800–1500 USD miesięcznie. Koszt 2-godzinnego przestoju dla tej samej gry — uwzględniając utracone przychody z IAP, odejścia graczy i bilety wsparcia — wynosi średnio 5000–12 000 USD.
Minimalny wariant multi-provider:
- Uwierzytelnianie: dostawca podstawowy + zapasowy
- Dane graczy / zapis stanu gry: replikowane między dostawcami
- Matchmaking: co najmniej jeden region zapasowy
- Tablice wyników i analityka: mogą tolerować krótkie awarie (kolejkowanie i odtwarzanie)
Wzorzec 2: bezstanowa, niezależna od dostawcy detekcja botów
Zaprojektuj detekcję botów tak, by działała identycznie niezależnie od tego, skąd pochodzi ruch. Awaria Yandex udowodniła, że detekcja oparta na IP jest z gruntu krucha — boty dostają nowe IP w momencie migracji.
Zbuduj detekcję wokół cech behawioralnych, które przetrwają zmiany infrastruktury:
from dataclasses import dataclass
from typing import List
@dataclass
class SessionFeatures:
request_count: int
duration_seconds: float
interval_variance: float # Low variance = bot-like regularity
error_retry_rate: float # Bots retry aggressively; humans quit
unique_endpoints: int # Bots hit predictable endpoint patterns
header_consistency: float # Bots reuse identical headers
class BehavioralClassifier:
"""Bot classifier based on behavioral features, not IP addresses.
Survives provider migration because behavior doesn't change."""
# Weights calibrated from production bot traffic analysis
WEIGHTS = {
'interval_variance': 0.30, # Most discriminating feature
'error_retry_rate': 0.25,
'header_consistency': 0.20,
'endpoint_diversity': 0.15,
'session_duration': 0.10,
}
def classify(self, features: SessionFeatures) -> float:
"""Returns bot probability from 0.0 (human) to 1.0 (bot)."""
score = 0.0
# Low interval variance → bot (humans are irregular)
if features.interval_variance < 0.1:
score += self.WEIGHTS['interval_variance']
elif features.interval_variance < 0.3:
score += self.WEIGHTS['interval_variance'] * 0.5
# High retry rate → bot (humans give up after 1-2 failures)
if features.error_retry_rate > 0.8:
score += self.WEIGHTS['error_retry_rate']
elif features.error_retry_rate > 0.5:
score += self.WEIGHTS['error_retry_rate'] * 0.5
# Perfect header consistency → bot (humans have browser variations)
if features.header_consistency > 0.95:
score += self.WEIGHTS['header_consistency']
# Very few unique endpoints → bot (humans explore)
if features.unique_endpoints <= 3 and features.request_count > 20:
score += self.WEIGHTS['endpoint_diversity']
# Suspiciously precise session duration (e.g., exactly 300s)
if abs(features.duration_seconds % 60) < 2:
score += self.WEIGHTS['session_duration']
return min(score, 1.0)
# Usage
classifier = BehavioralClassifier()
bot_session = SessionFeatures(
request_count=150, duration_seconds=300.0,
interval_variance=0.02, error_retry_rate=0.95,
unique_endpoints=2, header_consistency=0.99
)
human_session = SessionFeatures(
request_count=45, duration_seconds=1847.3,
interval_variance=0.67, error_retry_rate=0.15,
unique_endpoints=12, header_consistency=0.73
)
print(f"Bot score: {classifier.classify(bot_session):.2f}") # ~0.85
print(f"Human score: {classifier.classify(human_session):.2f}") # ~0.10
Ten klasyfikator nie przejmuje się tym, z którego ASN pochodzi żądanie. Gdy bot migruje z Yandex.Cloud do OVH, jego odcisk behawioralny pozostaje taki sam — a klasyfikator nadal go wyłapuje.
Wzorzec 3: odseparuj uwierzytelnianie od infrastruktury
Twój system uwierzytelniania powinien przetrwać każdą pojedynczą awarię infrastruktury. Używaj wielu dostawców uwierzytelniania, aby gracze mieli opcje zapasowe dostępu do swoich kont.
Zaimplementuj uwierzytelnianie z łagodną degradacją:
class MultiProviderAuth:
"""Authentication with automatic provider fallback."""
def __init__(self, providers: list):
# Order by reliability — primary first
self.providers = providers
async def authenticate(self, credentials: dict) -> dict:
last_error = None
for provider in self.providers:
try:
result = await provider.authenticate(credentials)
return {
'success': True,
'provider': provider.name,
'player_id': result['player_id'],
'session_token': result['session_token']
}
except Exception as e:
last_error = e
continue
return {
'success': False,
'error': 'All authentication providers failed',
'last_error': str(last_error)
}
Ten wzorzec ma znaczenie, ponieważ podczas awarii Yandex usługi ściśle powiązane z infrastrukturą tożsamości Yandex w ogóle nie mogły uwierzytelnić użytkowników. Gracze zostali zablokowani; boty, które się nie uwierzytelniają, działały dalej. Strategia uwierzytelniania u wielu dostawców oznacza, że Twoi gracze grają dalej, nawet gdy główny dostawca uwierzytelniania pada.
Wzorzec 4: obserwowalność obejmująca wielu dostawców
Twój monitoring musi widzieć wszystkich dostawców jednocześnie. Podziel monitoring na osobne obszary:
Monitorowanie rozkładu ruchu: śledź wolumen żądań per ASN, per region, per dostawcę. Alertuj, gdy rozkład zmieni się o więcej niż 20% w oknie 5-minutowym.
Monitorowanie zdrowia uwierzytelniania: śledź skuteczność uwierzytelniania per metodę i per dostawcę. Alertuj, gdy skuteczność któregokolwiek dostawcy spadnie poniżej 95%.
Monitorowanie detekcji botów: śledź stosunek sesji oznaczonych do wszystkich sesji per dostawcę. Alertuj, gdy stosunek spada poniżej baseline'u u jednego dostawcy, a rośnie u innego — to migracja.
Korelacja między dostawcami: to kluczowy element. Metryki pojedynczego dostawcy mogą wyglądać zdrowo, podczas gdy cały system jest atakowany. Zbuduj dashboardy pokazujące wszystkich dostawców obok siebie, aby dostrzegać wzorce migracji na pierwszy rzut oka.
Najlepsze praktyki: 5 kroków do odpornej na boty architektury backendu gry
Monitoruj migrację, nie tylko wolumen. Skonfiguruj korelację ruchu między dostawcami, która wykrywa, gdy ruch spada w jednym miejscu, a rośnie w innym. Same zagregowane metryki oszukałyby Cię podczas awarii Yandex — ruch AS200350 spadł, ale złośliwa aktywność trwała w OVH i Hetzner.
Zaimplementuj circuit breakery dla każdej zewnętrznej zależności. Twój klient gry, usługa tablic wyników i logika matchmakingu potrzebują automatycznego failoveru. Wzorzec circuit breakera zapobiega kaskadowym awariom, gdy jeden dostawca pada. Testuj je co miesiąc, wyłączając endpointy w środowisku staging.
Używaj detekcji behawioralnej, nie detekcji IP. Analiza behawioralna (interwały żądań, współczynniki ponowień, wzorce nagłówków) działa niezależnie od tego, gdzie hostowane są boty. Detekcja oparta na IP zawodzi w momencie, gdy boty migrują do nowego dostawcy z czystą reputacją IP. Opisany wyżej klasyfikator behawioralny wyłapuje boty, które przetrwają spadki ruchu o 99%.
Zaprojektuj uwierzytelnianie pod kątem łagodnej degradacji. Używaj sesji przypisanych do konta horizOn z wieloma metodami uwierzytelniania (e-mail, Google, Apple, anonimowe). Gdy jeden dostawca uwierzytelniania jest niedostępny, gracze mogą użyć innej metody, aby zachować dostęp do stanu gry. To zapobiega najgorszemu scenariuszowi: zablokowanym graczom, podczas gdy boty działają dalej.
Testuj failover co kwartał. Symuluj awarie dostawców w środowisku staging. Zweryfikuj, czy failover DNS wyzwala się w czasie TTL, circuit breakery otwierają się poprawnie, a rate limiting działa w regionach zapasowych. Udokumentuj czas od wykrycia awarii do pełnego odzyskania sprawności. Nietestowany failover to nie failover — to nadzieja.
Podsumowanie
Awaria Yandex udowodniła, że złośliwi aktorzy mają lepszą odporność infrastrukturalną niż większość backendów gier. Boty nie zginęły, gdy centra danych Yandex zgasły — w ciągu kilku minut przeniosły się do innych dostawców i działały dalej. Twój zagregowany dashboard ruchu pokazywał poprawę. Rzeczywistość była gorsza.
Zacznij od wdrożenia behawioralnego detektora botów z tego artykułu w swoim środowisku staging. Uruchom go na tydzień, aby ustalić baseline'y, a następnie zasymuluj awarię dostawcy, blokując ruch z jednego ASN i obserwując, czy detektor migracji zadziała. Wykryte wzorce pokażą dokładnie, gdzie Twoja obecna architektura jest podatna na ataki.
Dla studiów gier budujących systemy uwierzytelniania i danych graczy, które przetrwają awarie infrastruktury, horizOn oferuje sesje przypisane do konta z zapisem w chmurze z obsługą rewizji — usługi zaprojektowane tak, aby gracze zachowywali postępy nawet wtedy, gdy wydarzy się coś nieoczekiwanego. Wypróbuj za darmo lub przejrzyj dokumentację API, aby zobaczyć, jak sesje przypisane do konta integrują się z Twoim istniejącym backendem.
Źródło: Awaria Yandex: dane pokazują, że zamiast zgasnąć, złośliwe boty ruszyły w drogę