봇은 Yandex 장애에서 살아남았다 — 당신의 게임 Backend 아키텍처도 그래야 한다
핵심 요약
알아보세요: Yandex 장애에서 봇이 살아남은 이유를 Cloudflare 분석으로 파악하고, 게임 Backend 아키텍처가 인프라 실패를 견디도록 멀티 제공업체 배포, 행동 탐지, 장애 조치 전략을 적용하는 방법과 실시간 봇 이주 감지 코드를 단계별로.
모니터링 대시보드에 트래픽이 하룻밤 사이 40% 감소한 것으로 표시됩니다. 당신은 안도의 한숨을 쉬며 DDoS가 멈춘 것이라고 생각합니다. 하지만 Cloudflare의 2023년 10월 Yandex 장애 분석은 이 가정이 치명적으로 틀렸음을 증명했습니다. 악성 봇은 Yandex 데이터 센터가 정전되었을 때 죽지 않았습니다. 그들은 몇 분 만에 OVH, Hetzner, DigitalOcean으로 이전했습니다. 공격자가 계속 실행되는 동안 집계 지표는 당신을 속였습니다.
게임 Backend가 IP 기반 봇 탐지를 사용하는 단일 클라우드 제공업체에서 실행된다면, 당신은 동일한 사각지대에서 단 하나의 인프라 이벤트만큼 떨어져 있습니다. 이 런북은 Yandex 장애가 인프라 실패 중 봇 동작에 대해 무엇을 드러내는지, 그리고 이를 견디는 게임 Backend를 설계하는 방법을 설명합니다.
무엇이 무너지는가: 인프라 제공업체가 다운되면
주요 인프라 제공업체가 중단되면 4가지 중요한 시스템이 동시에 실패합니다. 그리고 그 실패는 순진한 모니터링에는 개선처럼 보이는 방식으로 발생합니다.
인증 계층이 오프라인이 됩니다. 플레이어 인증이 단일 제공업체의 ID 서비스에 의존한다면 플레이어는 로그인할 수 없습니다. Yandex 장애 동안 Yandex 인프라에 밀접하게 결합된 서비스는 완전한 서비스 중단을 겪었습니다. 플레이어는 오류 화면을 보았고, 인증하지 않는 봇은 다른 곳의 엔드포인트를 계속 공격했습니다.
봇 탐지가 기준 프레임을 잃습니다. 대부분의 게임 Backend는 ASN 또는 호스팅 제공업체로 악성 IP를 추적합니다. 해당 IP가 꺼지면 탐지 시스템은 이를 "해결됨"으로 표시하고 더 이상 찾지 않습니다. 그 사이 동일한 봇 플릿이 다른 제공업체의 새 IP에 다시 나타납니다.
Rate limiting이 무용지물이 됩니다. IP 기반 Rate limit은 특정 주소 범위에 묶여 있습니다. 봇이 이전하면 깨끗한 평판 점수를 가진 새 IP를 얻습니다. Rate limiter는 이를 새로운 정상 트래픽으로 취급합니다.
트래픽 집계가 잘못된 정보가 됩니다. Cloudflare의 Yandex 분석에 따르면 AS13238(Yandex Search) 트래픽은 거의 전적으로 합법적인 크롤러로 구성되어 있었으며, 그곳의 감소는 실제였습니다. 그러나 AS200350(Yandex.Cloud)에서는 악성 취약점 스캔 및 프로빙이 감소한 반면, 대량 자동화는 단순히 이전했습니다. 99% 감소를 보여주는 집계 트래픽 차트는 잘못된 이야기를 전달했습니다.
핵심 실패 모드: 위협이 이동했을 때 모니터링이 상황이 나아졌다고 생각합니다.
탐지 방법: 봇 이주를 실시간으로 포착하기
탐지에는 단순한 집계 트래픽 볼륨이 아닌 여러 레이어에서의 모니터링이 필요합니다. 각 레이어는 이주 패턴의 다른 측면을 포착합니다.
레이어 1: 트래픽 소스 상관관계
초당 총 요청 수를 추적하는 대신 자율 시스템 번호(ASN) 전반의 트래픽 소스 분포를 모니터링하세요. 한 제공업체의 트래픽이 감소하면 다른 제공업체의 트래픽이 비례적으로 증가하는지 확인하세요. ASN 전반의 상관된 하락-상승 패턴은 봇 이주의 신호입니다.
이 패턴을 식별하는 Python 탐지기는 다음과 같습니다:
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}%")
이 탐지기는 여러 ASN 전반의 상관된 트래픽 하락과 상승을 감시합니다. AS13238이 92% 하락하고 AS16276이 동시에 270% 상승하면 정상적인 트래픽 변화가 아닌 가능성이 높은 봇 이주로 플래그를 지정합니다. 하락 및 상승 규모가 일치하면 신뢰도 점수가 증가합니다. 이는 조정된 이주의 특징입니다.
레이어 2: 행동 지문
트래픽이 어디서 오는지 추적하지 말고 어떻게 행동하는지 추적하세요. 제공업체 간에 이주하는 봇은 동일한 행동 시그니처를 유지합니다: 동일한 요청 간격, 헤더 패턴, 세션 지속 시간, 오류 재시도 동작. IP 평판은 이동 시 초기화되지만 행동은 그렇지 않습니다.
각 세션에 대한 행동 지문을 만들고 해시하세요:
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}")
원래 제공업체가 다운된 직후 다른 제공업체의 새 IP에서 동일한 행동 지문이 나타나면 이는 우연이 아닌 확인된 이주입니다.
레이어 3: 인증 상태 모니터링
시간에 따른 인증된 요청과 인증되지 않은 요청의 비율을 추적하세요. 인프라 실패 중에는 합법적인 플레이어가 인증에 어려움을 겪고(비율이 인증되지 않은 쪽으로 이동), 인증하지 않는 봇은 변경 없이 엔드포인트를 계속 공격합니다.
이 레이어는 소스 제공업체와 관계없이 실제 플레이어와 자동화된 트래픽을 분리하기 때문에 특히 가치가 있습니다. horizOn의 계정 바인딩 세션을 이메일, Google, Apple, 익명 등 여러 인증 방법과 함께 사용한다면 방법별 인증 성공률을 모니터링할 수 있습니다. 이메일 인증이 실패하지만 Google 인증이 성공하면 봇 이주가 아닌 부분적인 인프라 실패를 식별한 것입니다. 모든 인증이 성공하지만 새 제공업체에서 인증되지 않은 트래픽이 급증한다면 그것이 이주입니다.
해결 방법: 효과적인 장애 조치 전략
해결 없는 탐지는 실시간으로 게임이 죽는 것을 지켜보는 것일 뿐입니다. 이번 주에 구현할 수 있는 세 가지 장애 조치 메커니즘이 있습니다.
DNS 기반 장애 조치
DNS를 짧은 TTL과 상태 검사로 구성하세요. 기본 리전이 실패하면 DNS가 자동으로 트래픽을 보조 리전으로 라우팅합니다. 이는 애플리케이션 변경이 필요하지 않습니다.
권장 설정:
- TTL: 60-120초 (짧을수록 장애 조치가 빠르지만 DNS 부하가 증가)
- 상태 검사 간격: 10-15초
- 상태 검사 엔드포인트:
/health가 200 응답 및 < 500ms 응답 시간 반환 - 장애 조치 트리거: 상태 검사 3회 연속 실패
트레이드오프: 짧은 TTL은 정상 운영 중 DNS 쿼리 볼륨을 10-20배 증가시킵니다. 일일 활성 플레이어가 50,000명인 게임의 경우 DNS 비용이 월 약 $2-5 추가됩니다. 이는 방지하는 다운타임에 비해 무시할 수 있습니다.
애플리케이션 레벨 Circuit Breaker
서비스가 실패하는 시점을 감지하고 자동으로 대안으로 라우팅하는 Circuit Breaker를 게임 클라이언트에 구현하세요:
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);
이 패턴은 한 제공업체가 다운될 때 계단식 실패를 방지합니다. 서버 폴백 설계 분석에서 다루었듯이 자동 장애 조치는 30초의 일시적인 문제와 4시간의 중단 사이의 차이입니다.
분산 Rate Limiting
트래픽이 새 리전으로 장애 조치되면 Rate limiting도 그곳에서 작동해야 합니다. 각 서버에 로컬인 IP 기반 Rate limit은 장애 조치 중에 무용지물이 됩니다. 동일한 봇이 모든 엔드포인트에서 깨끗한 상태를 얻기 때문입니다.
모든 리전에서 접근 가능한 공유 저장소를 사용하여 분산 Rate limiting을 구현하세요:
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")
식별자가 IP 주소가 아닌 플레이어 ID를 사용한다는 점에 주목하세요. 새 IP로 이주하는 봇은 여전히 동일한 플레이어 지문을 가지고 있습니다. 이 접근 방식은 제공업체 레벨 이주 후에도 봇을 잡아냅니다.
예방 방법: 복원력을 위한 아키텍처 패턴
해결은 반응적입니다. 예방은 인프라 실패가 발생할 것이라고 가정하는 아키텍처를 구축하는 것을 의미합니다. 실제로 발생할 것이기 때문입니다.
패턴 1: 핵심 서비스를 위한 멀티 제공업체 배포
인증, 매치메이킹, 봇 탐지를 최소 두 개의 클라우드 제공업체에 배포하세요. 이는 전체 스택을 두 번 실행한다는 의미가 아니라 중단을 견뎌야 하는 서비스가 제공업체에 독립적이어야 한다는 의미입니다.
비용 현실: 핵심 서비스를 위한 멀티 제공업체 배포는 일반적으로 인프라 비용에 30-50%를 추가합니다. 10,000명의 동시 플레이어를 서비스하는 게임 Backend의 경우 월 약 $800-1,500이 추가됩니다. 동일한 게임의 2시간 중단 비용(손실된 IAP 수익, 플레이어 이탈, 지원 티켓 포함)은 평균 $5,000-12,000입니다.
최소 실행 가능한 멀티 제공업체:
- 인증: 기본 + 보조 제공업체
- 플레이어 데이터 / 저장 상태: 제공업체 간 복제
- 매치메이킹: 최소 하나의 폴백 리전
- 리더보드 및 분석: 짧은 중단 허용 가능 (대기열 및 재생)
패턴 2: 상태 비저장, 제공업체 비종속 봇 탐지
트래픽이 어디에서 발생하는지와 관계없이 동일하게 작동하도록 봇 탐지를 설계하세요. Yandex 장애는 IP 기반 탐지가 근본적으로 취약하다는 것을 증명했습니다. 봇은 이주하는 즉시 새 IP를 얻습니다.
인프라 변경에도 살아남는 행동 특징을 중심으로 탐지를 구축하세요:
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
이 분류기는 요청이 어떤 ASN에서 오는지 신경 쓰지 않습니다. 봇이 Yandex.Cloud에서 OVH로 이주하면 행동 지문은 동일하게 유지되며 분류기는 여전히 이를 잡아냅니다.
패턴 3: 인증을 인프라에서 분리
인증 시스템은 단일 인프라 실패에서도 살아남아야 합니다. 여러 인증 제공업체를 사용하여 플레이어가 계정에 접근할 수 있는 폴백 옵션을 제공하세요.
우아한 성능 저하(graceful degradation)로 인증을 구현하세요:
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)
}
이 패턴이 중요한 이유는 Yandex 장애 동안 Yandex ID 인프라에 밀접하게 결합된 서비스가 사용자를 전혀 인증할 수 없었기 때문입니다. 플레이어는 잠겼고, 인증하지 않는 봇은 계속 실행되었습니다. 멀티 제공업체 인증 전략은 기본 인증 제공업체가 다운되어도 플레이어가 계속 플레이할 수 있음을 의미합니다.
패턴 4: 제공업체에 걸친 관찰 가능성
모니터링은 모든 제공업체를 동시에 볼 수 있어야 합니다. 모니터링을 별도의 관심사로 분리하세요:
트래픽 분포 모니터링: ASN, 리전, 제공업체별 요청 볼륨을 추적하세요. 5분 창에서 분포가 20% 이상 이동하면 알림을 보내세요.
인증 상태 모니터링: 방법 및 제공업체별 인증 성공률을 추적하세요. 어떤 제공업체의 성공률이 95% 아래로 떨어지면 알림을 보내세요.
봇 탐지 모니터링: 제공업체별 플래그된 세션 대 전체 세션 비율을 추적하세요. 한 제공업체에서 비율이 기준선 아래로 떨어지고 다른 제공업체에서 상승하면 알림을 보내세요. 그것이 이주입니다.
교차 제공업체 상관관계: 이것이 핵심입니다. 개별 제공업체 지표는 전체 시스템이 공격받는 동안 건강해 보일 수 있습니다. 모든 제공업체를 나란히 보여주는 대시보드를 구축하여 이주 패턴을 한눈에 포착할 수 있게 하세요.
모범 사례: 봇에 강한 게임 Backend 아키텍처를 위한 5단계
볼륨이 아닌 이주를 모니터링하세요. 한 곳에서 트래픽이 감소하고 다른 곳에서 증가하는 시점을 감지하는 교차 제공업체 트래픽 상관관계를 설정하세요. 집계 지표만으로는 Yandex 장애 중에 속았을 것입니다. AS200350 트래픽은 감소했지만 악성 활동은 OVH와 Hetzner에서 계속되었습니다.
모든 외부 의존성에 Circuit Breaker를 구현하세요. 게임 클라이언트, 리더보드 서비스, 매치메이킹 로직 모두 자동 장애 조치가 필요합니다. Circuit Breaker 패턴은 한 제공업체가 다운될 때 계단식 실패를 방지합니다. 스테이징에서 엔드포인트를 죽여 매월 테스트하세요.
IP 탐지가 아닌 행동 탐지를 사용하세요. 행동 분석(요청 간격, 재시도율, 헤더 패턴)은 봇이 호스팅되는 위치와 관계없이 작동합니다. IP 기반 탐지는 봇이 깨끗한 IP 평판을 가진 새 제공업체로 이주하는 순간 실패합니다. 위의 행동 분류기는 99% 트래픽 감소를 견디는 봇을 잡아냅니다.
우아한 성능 저하를 위한 인증을 설계하세요. horizOn의 계정 바인딩 세션을 이메일, Google, Apple, 익명 등 여러 인증 방법과 함께 사용하세요. 하나의 인증 제공업체를 사용할 수 없을 때 플레이어는 다른 방법을 사용하여 게임 상태에 대한 접근을 유지할 수 있습니다. 이는 최악의 시나리오(봇이 계속 실행되는 동안 플레이어가 잠기는 것)를 방지합니다.
분기별로 장애 조치를 테스트하세요. 스테이징에서 제공업체 중단을 시뮬레이션하세요. DNS 장애 조치가 TTL 내에 트리거되고, Circuit Breaker가 올바르게 열리고, Rate limiting이 폴백 리전에서 작동하는지 확인하세요. 실패 감지부터 전체 복구까지의 시간을 문서화하세요. 테스트되지 않은 장애 조치는 장애 조치가 아니라 희망입니다.
결론
Yandex 장애는 악의적인 행위자가 대부분의 게임 Backend보다 더 나은 인프라 복원력을 가지고 있음을 증명했습니다. 봇은 Yandex 데이터 센터가 정전되었을 때 죽지 않았습니다. 몇 분 만에 다른 제공업체로 이주하여 계속 실행되었습니다. 집계 트래픽 대시보드는 개선을 보여주었습니다. 현실은 더 나빠졌습니다.
이 기사의 행동 봇 탐지기를 스테이징 환경에 배포하는 것부터 시작하세요. 기준선을 설정하기 위해 일주일 동안 실행한 다음, 하나의 ASN에서 트래픽을 차단하여 제공업체 중단을 시뮬레이션하고 이주 탐지기가 작동하는지 관찰하세요. 발견한 패턴은 현재 아키텍처가 정확히 어디에 취약한지 드러낼 것입니다.
인프라 실패를 견디는 인증 및 플레이어 데이터 시스템을 구축하는 게임 스튜디오를 위해 horizOn은 리비전 인식 클라우드 저장 기능이 있는 계정 바인딩 세션을 제공합니다. 예상치 못한 상황이 발생해도 플레이어가 진행 상황을 유지할 수 있도록 설계된 서비스입니다. 무료로 사용해 보거나 API 문서를 탐색하여 계정 바인딩 세션이 기존 Backend와 어떻게 통합되는지 확인하세요.