Dedykowane serwery gier: Orkiestracja za pomocą GameLift i Agones
Każda konkurencyjna gra wieloosobowa jest obsługiwana przez niewidzialną, ale krytyczną infrastrukturę: dedykowany serwer gier. W przeciwieństwie do architektury peer-to-peer, w której jeden z graczy pełni rolę gospodarza (ze wszystkimi kwestiami uczciwości i wynikające z tego oszustwa), serwer dedykowany i neutralna maszyna, na której przeprowadzana jest autorytatywna symulacja gra, otrzymuje informacje od wszystkich graczy i przekazuje każdemu z nich zaktualizowany stan.
Wydajnie zarządzaj dziesiątkami tysięcy tych serwerów, dynamicznie skalując się w odpowiedzi na zapotrzebowanie i minimalizacja kosztów to jedno z najbardziej złożonych wyzwań inżynieryjnych w nowoczesnym backendie gier. W tym W artykule przyjrzymy się dwóm wiodącym rozwiązaniom do orkiestracji serwerów gier: AWS GameLift, Usługa zarządzana Amazon, np Agony, opracowano platformę open source na platformie Kubernetes od Google’a i Ubisoftu. Zobaczymy, jak wybrać między nimi, jak je skonfigurować i jak zintegrować z systemami matchmakingu w celu stworzenia skalowalnej i opłacalnej infrastruktury gier.
Czego się nauczysz
- Architektura i cykl życia dedykowanego serwera gier
- AWS GameLift: zarządzanie flotą, sesje i integracja z FlexMatch
- Agones na Kubernetesie: GameServer CRD, Flota i Autoskalowanie
- Adapter FleetIQ: wykrywaj instancje, aby zaoszczędzić do 90% kosztów
- Wzorzec wdrażania w wielu regionach z programem Global Accelerator
- Monitorowanie i sprawdzanie stanu serwerów gier o wysokiej dostępności
Architektura dedykowanego serwera gier
Zanim przejdziemy do narzędzi orkiestracji, istotne jest zrozumienie, do czego dokładnie służy serwer gier jest dedykowany i jak zarządza swoim cyklem życia. Typowy serwer dedykowany przechodzi przez następujące stany:
| Państwo | Opis | Typowy czas trwania |
|---|---|---|
| STARTOWY | Rozpoczyna się proces, ładuje zasoby i rejestry do systemu orkiestracji | 2-10 sekund |
| GOTOWY | Gotowy do akceptowania połączeń, oczekujący na przypisanie do sesji | zmienny |
| ASYGNOWANY | Przypisany do gry, akceptuje określonych graczy | czas trwania meczu |
| SKRYTY | Zarezerwowane na przyszłą sesję, niedostępne dla innych | sekundy-minuty |
| ZAMKNIĘCIE | Gra się kończy, proces zostaje zamknięty i uwalnia zasób | 2-5 sekund |
System orkiestracji musi śledzić status każdego serwera, rozdzielać graczy na dostępne serwery, zarządzaj awariami i automatycznie skaluj flotę w odpowiedzi na obciążenie. Wyzwanie polega na tym, że te operacje muszą się one odbyć w ciągu kilku sekund: żaden gracz nie chce czekać 30 sekund na znalezienie dopasowania.
AWS GameLift: usługa zarządzana
AWS GameLift oferuje w pełni zarządzaną usługę, która eliminuje złożoność orkiestracji serwerów. Jego architektura jest podzielona na trzy główne komponenty: Flota (Grupy instancji EC2 które obsługują serwery), Sesja gry (instancje gier działające na serwerze) i i Sesja gracza (miejsca zarezerwowane dla każdego gracza w sesji).
// Integrazione GameLift SDK nel game server (Node.js)
import { GameLift } from 'aws-sdk';
import * as GameLiftServerSDK from 'gamelift-server-sdk';
class GameLiftIntegration {
private sdk = GameLiftServerSDK;
async initialize(): Promise<void> {
// Inizializza la connessione con il servizio GameLift
const initResult = await this.sdk.InitSDK();
if (!initResult.Success) {
throw new Error(`GameLift SDK init failed: ${initResult.Error}`);
}
// Registra i callback per gli eventi del lifecycle
this.sdk.ProcessReady({
onStartGameSession: this.handleStartGameSession.bind(this),
onUpdateGameSession: this.handleUpdateGameSession.bind(this),
onProcessTerminate: this.handleProcessTerminate.bind(this),
onHealthCheck: () => true, // Game server e in salute
port: 7777,
logParameters: { logPaths: ['/local/game/logs/'] }
});
console.log('GameLift: Server pronto, in attesa di sessioni');
}
private async handleStartGameSession(gameSession: GameSession): Promise<void> {
console.log(`Nuova sessione: ${gameSession.GameSessionId}`);
// Inizializza la logica di gioco
await this.initializeGameLogic(gameSession);
// Notifica GameLift che la sessione e attiva
this.sdk.ActivateGameSession();
}
private async handleProcessTerminate(): Promise<void> {
// Salva lo stato, disconnetti i giocatori
await this.saveGameState();
this.sdk.ProcessEnding();
process.exit(0);
}
// Aggiunge un giocatore alla sessione
async acceptPlayer(playerSessionId: string): Promise<void> {
const result = await this.sdk.AcceptPlayerSession(playerSessionId);
if (!result.Success) {
throw new Error(`Player non accettato: ${result.Error}`);
}
}
// Rimuove un giocatore alla sessione
async removePlayer(playerSessionId: string): Promise<void> {
await this.sdk.RemovePlayerSession(playerSessionId);
}
}
Klient musi zażądać sesji gry za pośrednictwem pakietu SDK klienta GameLift lub interfejsu API REST. Oto jak to zrobić ustrukturyzuj logikę po stronie klienta, aby rozpocząć nową sesję lub dołączyć do istniejącej:
// Client: creazione sessione di gioco (TypeScript)
import AWS from 'aws-sdk';
const gamelift = new AWS.GameLift({ region: 'eu-west-1' });
interface MatchResult {
serverIp: string;
serverPort: number;
playerSessionId: string;
}
async function joinGame(playerId: string, fleetId: string): Promise<MatchResult> {
// Cerca sessioni con slot disponibili
const searchResult = await gamelift.searchGameSessions({
FleetId: fleetId,
FilterExpression: 'hasAvailablePlayerSessions=true',
SortExpression: 'creationTimeMillis ASC',
Limit: 1
}).promise();
let gameSessionId: string;
if (searchResult.GameSessions?.length) {
// Unisciti a una sessione esistente
gameSessionId = searchResult.GameSessions[0].GameSessionId!;
} else {
// Crea una nuova sessione
const newSession = await gamelift.createGameSession({
FleetId: fleetId,
MaximumPlayerSessionCount: 10,
Name: `session-${Date.now()}`,
GameProperties: [
{ Key: 'map', Value: 'arena_01' },
{ Key: 'mode', Value: 'deathmatch' }
]
}).promise();
gameSessionId = newSession.GameSession!.GameSessionId!;
}
// Crea una player session per questo giocatore
const playerSession = await gamelift.createPlayerSession({
GameSessionId: gameSessionId,
PlayerId: playerId
}).promise();
return {
serverIp: playerSession.PlayerSession!.IpAddress!,
serverPort: playerSession.PlayerSession!.Port!,
playerSessionId: playerSession.PlayerSession!.PlayerSessionId!
};
}
GameLift FlexMatch: Zintegrowane dobieranie graczy
GameLift zawiera FlexMatch, elastyczny silnik dobierania graczy, który pozwala definiować zasady skomplikowane ze względu na skład meczów. Obsługuje automatyczne uzupełnianie (uzupełnia wolne miejsca w trakcie meczu), równoważenie zespołu, filtry geograficzne i wymagania dotyczące umiejętności.
// FlexMatch rule set - definisce le regole di matchmaking
const flexMatchRuleSet = {
"name": "competitive-5v5",
"ruleLanguageVersion": "1.0",
"playerAttributes": [
{
"name": "skill",
"type": "number",
"default": 1000
},
{
"name": "latency",
"type": "latencyMilliseconds"
}
],
"teams": [
{
"name": "team1",
"minPlayers": 5,
"maxPlayers": 5
},
{
"name": "team2",
"minPlayers": 5,
"maxPlayers": 5
}
],
"rules": [
{
"name": "FairTeamSkill",
"description": "Differenza skill media tra team < 150",
"type": "distance",
"measurements": [
"teams[team1].players.attributes[skill]",
"teams[team2].players.attributes[skill]"
],
"referenceValue": 150,
"maxDistance": 150
},
{
"name": "FastConnection",
"description": "Latenza max 100ms",
"type": "latency",
"maxLatency": 100
}
],
"expansions": [
{
"target": "rules[FairTeamSkill].maxDistance",
"steps": [
{ "waitTimeSeconds": 30, "value": 250 },
{ "waitTimeSeconds": 60, "value": 500 }
]
}
]
};
Agones: Serwer gier na platformie Kubernetes
Agony oraz framework open source, początkowo opracowany przez Google i Ubisoft, a obecnie utrzymywany
od społeczności, która rozszerza Kubernetes o niestandardowe definicje zasobów (CRD) zaprojektowane specjalnie dla gier
serwer. Agones wprowadza trzy nowe obiekty Kubernetes: GameServer, Fleet e
GameServerAllocation.
Główną przewagą Agones nad GameLift jest elastyczność: możesz wdrożyć go w dowolnym klastrze Kubernetes (GKE, EKS, AKS, on-premise) i masz pełną kontrolę nad infrastrukturą. Wadą jest to musisz sam zająć się wieloma aspektami, które GameLift obsługuje automatycznie.
# Agones GameServer - manifest Kubernetes
apiVersion: agones.dev/v1
kind: GameServer
metadata:
name: my-game-server
labels:
game: "shooter"
region: "eu-west"
spec:
ports:
- name: default
portPolicy: Dynamic # Kubernetes assegna la porta dinamicamente
containerPort: 7777
protocol: UDP
health:
initialDelaySeconds: 30
periodSeconds: 5
failureThreshold: 3
sdkServer:
logLevel: Info
grpcPort: 9357
httpPort: 9358
template:
spec:
containers:
- name: game-server
image: gcr.io/myproject/game-server:v1.2.0
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
env:
- name: GAME_MODE
value: "deathmatch"
- name: MAX_PLAYERS
value: "16"
# Agones Fleet - gestisce un pool di GameServer
apiVersion: agones.dev/v1
kind: Fleet
metadata:
name: shooter-fleet
spec:
replicas: 10 # Mantieni sempre 10 server pronti
scheduling: Packed # Impacchetta i server sullo stesso nodo
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
template:
metadata:
labels:
game: "shooter"
spec:
ports:
- name: default
portPolicy: Dynamic
containerPort: 7777
protocol: UDP
template:
spec:
containers:
- name: game-server
image: gcr.io/myproject/game-server:v1.2.0
resources:
requests:
memory: "512Mi"
cpu: "500m"
---
# FleetAutoscaler - scala automaticamente la Fleet
apiVersion: autoscaling.agones.dev/v1
kind: FleetAutoscaler
metadata:
name: shooter-fleet-autoscaler
spec:
fleetName: shooter-fleet
policy:
type: Buffer
buffer:
bufferSize: 5 # Mantieni almeno 5 server READY
minReplicas: 5
maxReplicas: 100
sync:
type: FixedInterval
fixedInterval:
seconds: 30
Integracja Agones SDK z serwerem gry
Agones udostępnia pakiety SDK dla Go, C++, Rust, Node.js i innych języków. Serwer gry musi się komunikować z wózkiem bocznym Agones, aby raportować swój status i otrzymywać powiadomienia o cyklu życia.
// Agones SDK integration - Node.js
import AgonesSDK from '@google-cloud/agones-sdk';
class AgonesGameServer {
private sdk: AgonesSDK;
private healthInterval: NodeJS.Timer | null = null;
constructor() {
this.sdk = new AgonesSDK();
}
async start(): Promise<void> {
// Connetti al sidecar Agones
await this.sdk.connect();
console.log('Connesso ad Agones');
// Ascolta le notifiche di allocazione
this.sdk.watchGameServer((gameServer) => {
console.log('Stato server aggiornato:', gameServer.status.state);
if (gameServer.status.state === 'Allocated') {
this.handleAllocation(gameServer);
}
});
// Invia health check ogni 5 secondi
this.healthInterval = setInterval(async () => {
try {
await this.sdk.health();
} catch (err) {
console.error('Health check fallito:', err);
}
}, 5000);
// Segnala che il server e pronto
await this.sdk.ready();
console.log('Server in stato READY');
}
private async handleAllocation(gameServer: any): Promise<void> {
const labels = gameServer.objectMeta.labels;
const sessionId = labels['session-id'] || 'unknown';
console.log(`Server allocato per sessione: ${sessionId}`);
// Inizializza la logica di gioco con i parametri dell'allocazione
const annotations = gameServer.objectMeta.annotations;
await this.initGame({
sessionId,
maxPlayers: parseInt(annotations['max-players'] || '10'),
mapId: annotations['map-id'] || 'default'
});
}
async shutdown(): Promise<void> {
if (this.healthInterval) {
clearInterval(this.healthInterval);
}
await this.saveGameResults();
await this.sdk.shutdown(); // Segnala ad Agones che il server sta terminando
}
// Allocazione via API
static async allocateServer(namespace: string): Promise<AllocationResult> {
const response = await fetch(`http://agones-allocator.${namespace}:8443/gameserverallocation`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
apiVersion: 'allocation.agones.dev/v1',
kind: 'GameServerAllocation',
spec: {
selectors: [
{ matchLabels: { 'agones.dev/fleet': 'shooter-fleet' } }
],
metadata: {
labels: { 'session-id': `session-${Date.now()}` },
annotations: {
'max-players': '10',
'map-id': 'arena_01'
}
}
}
})
});
return response.json();
}
}
Adapter FleetIQ: Oszczędności dzięki instancjom punktowym
Jednym z największych kosztów hostingu serwerów gier jest przetwarzanie danych. AWS opracował Adapter GameLift FleetIQ do Agones, który pozwala na uruchamianie serwerów Agones Instancje punktowe EC2, oszczędzając do 90% w porównaniu z instancjami On-Demand.
Ryzyko wystąpienia wystąpień typu Spot i nagłej awarii, gdy AWS potrzebuje takiej pojemności. FleetIQ minimalizuje to ryzyko, wykorzystując algorytmy uczenia maszynowego do przewidywania przestojów i zawsze utrzymuje bufor instancji On-Demand jako rezerwę.
| Konfiguracja | Koszt/godz. (c5.xlarge) | Oszczędności | Ryzyko przerwania |
|---|---|---|---|
| Na żądanie | 0,192 USD | - | 0% |
| Instancja punktowa | ~0,025-0,060 USD | 70-87% | 5-15% |
| Spot + FleetIQ | ~ 0,030-0,070 USD | 63-85% | <1% (przewidywanie ML) |
| Punkt grawitonowy | ~ 0,020-0,040 USD | 79-90% | <1% |
Architektura wieloregionalna z Globalnym Akceleratorem
W przypadku gry z globalną bazą graczy niezbędne jest zapewnienie architektury obejmującej wiele regionów niskie opóźnienia dla wszystkich graczy. AWS Global Accelerator kieruje ruch do regionu bliżej, redukując odczuwalne opóźnienia i poprawiając stabilność połączenia.
// Architettura multi-region - configurazione Terraform
resource "aws_globalaccelerator_accelerator" "game_accelerator" {
name = "game-global-accelerator"
ip_address_type = "IPV4"
enabled = true
}
resource "aws_globalaccelerator_listener" "game_listener" {
accelerator_arn = aws_globalaccelerator_accelerator.game_accelerator.id
client_affinity = "SOURCE_IP" # Affinita per sessioni di gioco
protocol = "UDP"
port_range {
from_port = 7000
to_port = 8000
}
}
# Endpoint group per EU-West-1
resource "aws_globalaccelerator_endpoint_group" "eu_west" {
listener_arn = aws_globalaccelerator_listener.game_listener.id
endpoint_group_region = "eu-west-1"
traffic_dial_percentage = 100
health_check_port = 8080
health_check_protocol = "HTTP"
health_check_path = "/health"
health_check_interval_seconds = 10
threshold_count = 3
endpoint_configuration {
endpoint_id = aws_lb.eu_game_lb.arn
weight = 100
}
}
# Endpoint group per AP-Southeast-1
resource "aws_globalaccelerator_endpoint_group" "ap_southeast" {
listener_arn = aws_globalaccelerator_listener.game_listener.id
endpoint_group_region = "ap-southeast-1"
traffic_dial_percentage = 100
endpoint_configuration {
endpoint_id = aws_lb.ap_game_lb.arn
weight = 100
}
}
Porównanie: GameLift vs Agones
| Kryterium | AWS GameLift | Agones na Kubernetesie |
|---|---|---|
| Konfiguracja wstępna | Szybki, w pełni zarządzany | Złożone, wymaga wiedzy K8 |
| Blokada dostawcy | Wysoka (tylko AWS) | Niski (wiele chmur) |
| Koszty stałe | Cena za sesję + instancję | Tylko obliczone koszty |
| Swatanie | Zintegrowany FlexMatch | Wymaga osobnego meczu otwartego |
| Ułuskowienie | Automatyczny, zarządzany | Instrukcja + FleetAutoscaler |
| Wiele chmur | No | Tak (GKE, EKS, AKS) |
| Protokół UDP | Si | Si |
| Instancje punktowe | Zintegrowany system FleetIQ | K8s Spot + adapter FleetIQ |
Kiedy wybrać GameLift
GameLift to najlepszy wybór, jeśli jesteś już mocno zintegrowany z ekosystemem AWS i masz mały zespół bez wiedzy Kubernetes i chcesz zminimalizować czas pracy. Dodatkowy koszt zarządzanej usługi i często niższy niż koszt personelu potrzebnego do zarządzania Kubernetesem w środowisku produkcyjnym.
Kiedy wybrać Agony
Agones jest idealnym rozwiązaniem dla zespołów posiadających wiedzę na temat Kubernetes, do gier, które muszą działać na wielu dostawcach usług w chmurze, lub gdy chcesz zmaksymalizować kontrolę nad swoją infrastrukturą. A także właściwy wybór, jeśli już to zrobiłeś masz ugruntowaną infrastrukturę Kubernetes i chcesz ją wykorzystać w serwerach gier.
Kontrola i monitorowanie stanu zdrowia
Aby wcześniej wykryć uszkodzone serwery gier, niezbędny jest solidny system kontroli stanu które wpływają na graczy. Zarówno GameLift, jak i Agones obsługują okresowe kontrole stanu zdrowia, ale Ważne jest wdrożenie niestandardowych metryk aplikacji.
// Health check endpoint per game server - Express.js
import express from 'express';
import { GameMetrics } from './metrics';
const app = express();
const metrics = new GameMetrics();
app.get('/health', (req, res) => {
const health = {
status: 'ok',
uptime: process.uptime(),
memoryMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
activePlayers: metrics.getActivePlayers(),
tickRate: metrics.getCurrentTickRate(),
avgLatencyMs: metrics.getAverageLatency()
};
// Considera degraded se tickrate scende sotto il 80% del target
if (health.tickRate < metrics.targetTickRate * 0.8) {
health.status = 'degraded';
res.status(503).json(health);
return;
}
res.json(health);
});
app.get('/metrics', async (req, res) => {
// Prometheus-compatible metrics
res.set('Content-Type', 'text/plain');
res.send(await metrics.toPrometheusFormat());
});
app.listen(8080, () => {
console.log('Health endpoint attivo su :8080');
});
Najlepsze praktyki i anty-wzorce
Najlepsze praktyki
- Rozgrzej flotę: Zawsze utrzymuj bufor serwera GOTOWY, aby uniknąć zimnego startu w godzinach szczytu
- Instancje punktowe z rezerwą: Użyj Spot, aby obniżyć koszty, ale zachowaj 10–20% płatności na żądanie jako rezerwę
- Eleganckie zamknięcie: Daj serwerom 30–60 sekund na dokończenie bieżących gier przed zamknięciem
- Trwałe logi: Przed zamknięciem zapisz dzienniki w S3 lub systemie scentralizowanym
- Wersjonowanie obrazu: Używaj niezmiennych tagów (nigdy
latest) dla powtarzalnych wdrożeń - Powinowactwo regionalne: Przypisz graczy do serwerów w regionie o najniższym opóźnieniu
Anty-wzorce, których należy unikać
- Globalny status na serwerze: Każdy serwer gier musi być bezstanowy pod względem infrastruktury; stan gry jest lokalny dla sesji
- Flota za duża lub za mała: Niewymiarowe floty powodują oczekiwanie; zbyt duże marnotrawstwo pieniędzy
- Zbyt agresywne kontrole stanu: Sprawdź co sekundę zużywają zasoby; Zwykle wystarczy 5-10 sekund
- Brak okresu drenażu: Zabicie serwera bez okresu drenażu powoduje nagłe zakończenie trwających gier
Wnioski
Orkiestracja dedykowanych serwerów gier to dyscyplina wymagająca delikatnej równowagi pomiędzy koszty, opóźnienia i niezawodność. AWS GameLift oferuje zarządzane rozwiązanie, które zmniejsza złożoność operacyjny, podczas gdy Agones oferuje elastyczność potrzebną w przypadku architektur wielochmurowych i zespołów solidna wiedza specjalistyczna w dziedzinie Kubernetes. Prawdopodobnie reprezentuje połączenie Agones z adapterem FleetIQ najlepszy kompromis w przypadku większości gier AAA: elastyczność open source i oszczędności koszt instancji Spot.
W kolejnym artykule z serii zobaczymy jak zbudować system wyrafinowane kojarzenie z algorytmami ELO i Glicko-2, integrującymi dobieranie graczy oparte na umiejętnościach z mechanizmami orkiestracji które dzisiaj badaliśmy.
Nadchodzące artykuły z serii Game Backend
- Artykuł 03: System dobierania graczy z ELO i Glicko-2
- Artykuł 04: Synchronizacja stanu w czasie rzeczywistym i przywracanie kodu sieciowego
- Artykuł 05: Architektura zapobiegająca oszustwom po stronie serwera







