Servere de jocuri dedicate: Orchestrare cu GameLift și Agones
Fiecare joc multiplayer competitiv este susținut de o infrastructură invizibilă, dar critică: serverul de joc dedicat. Spre deosebire de o arhitectură peer-to-peer în care unul dintre jucători acționează ca gazdă (cu toate problemele de corectitudine și înșelăciunea rezultată), un server dedicat și o mașină neutră care rulează simularea autorizată a joc, primește informații de la toți jucătorii și distribuie starea actualizată fiecăruia dintre ei.
Gestionați zeci de mii de aceste servere în mod eficient, scalând dinamic ca răspuns la cerere și minimizarea costurilor, este una dintre cele mai complexe provocări de inginerie din backend-ul modern al jocurilor. In aceasta articol vom explora două soluții principale pentru orchestrarea serverului de jocuri: AWS GameLift, serviciul gestionat de Amazon, de ex Agones, cadrul open-source pe Kubernetes dezvoltat de la Google și Ubisoft. Vom vedea cum să alegem între cele două, cum să le configuram și cum să le integrăm cu sistemele de matchmaking pentru a crea o infrastructură de jocuri scalabilă și rentabilă.
Ce vei învăța
- Arhitectura și ciclul de viață al unui server de jocuri dedicat
- AWS GameLift: managementul flotei, sesiuni și integrare cu FlexMatch
- Agones pe Kubernetes: GameServer CRD, Fleet și Autoscaling
- Adaptor FleetIQ: Instanțe spot pentru a economisi până la 90% din costuri
- Model de implementare în mai multe regiuni cu Global Accelerator
- Monitorizare și verificare a stării de sănătate pentru serverele de joc cu o mare disponibilitate
Arhitectura unui server de jocuri dedicat
Înainte de a intra în instrumentele de orchestrare, este esențial să înțelegeți ce face exact un server de joc dedicat și modul în care își gestionează ciclul de viață. Un server dedicat tipic trece prin aceste stări:
| Stat | Descriere | Durata tipică |
|---|---|---|
| PORNIRE | Procesul începe, încarcă active și se înregistrează în sistemul de orchestrare | 2-10 secunde |
| GATA | Gata să accepte conexiuni, așteaptă să fie alocat unei sesiuni | variabilă |
| ALOCAT | Atribuit unui joc, acceptă jucătorii specificați | durata meciului |
| REZERVAT | Rezervat pentru o sesiune viitoare, nu este disponibil pentru alții | secunde-minute |
| ÎNCHIDERE | Game over, procesul se închide și eliberează resursa | 2-5 secunde |
Sistemul de orchestrare trebuie să urmărească starea fiecărui server, să distribuie jucătorii pe serverele disponibile, gestionați defecțiunile și scalați automat flota ca răspuns la încărcare. Provocarea este că aceste operațiuni acestea trebuie să se întâmple în câteva secunde: niciun jucător nu vrea să aștepte 30 de secunde pentru a găsi o potrivire.
AWS GameLift: Serviciul gestionat
AWS GameLift oferă un serviciu complet gestionat care face abstractie de complexitatea orchestrarii serverului. Arhitectura sa este împărțită în trei componente principale: the Flota (grupuri de instanțe EC2 care rulează serverele), the Sesiune de joc (instanțe de joc care rulează pe un server) și i Sesiune de jucători (sloturi rezervate fiecărui jucător dintr-o sesiune).
// 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);
}
}
Clientul trebuie să solicite o sesiune de joc prin GameLift Client SDK sau REST API. Iată cum structurați logica clientului pentru a începe o nouă sesiune sau a vă alătura uneia existente:
// 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: Matchmaking integrat
GameLift include FlexMatch, un motor flexibil de matchmaking care vă permite să definiți reguli complex pentru alcătuirea meciurilor. Suportă completarea automată (umple posturile vacante în timpul meciului), echilibrarea echipelor, filtrele geografice și cerințele de calificare.
// 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: Game Server pe Kubernetes
Agones și un cadru open-source, dezvoltat inițial de Google și Ubisoft și întreținut acum
din comunitate, care extinde Kubernetes cu Custom Resource Definitions (CRD) concepute special pentru jocuri
server. Agones introduce trei noi obiecte Kubernetes: GameServer, Fleet e
GameServerAllocation.
Principalul avantaj al Agones față de GameLift este flexibilitatea: îl puteți implementa pe orice cluster Kubernetes (GKE, EKS, AKS, on-premise) și aveți control total asupra infrastructurii. Dezavantajul este că trebuie să te ocupi singur de multe aspecte pe care GameLift le gestionează automat.
# 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
Integrarea Agones SDK în Game Server
Agones oferă SDK-uri pentru Go, C++, Rust, Node.js și alte limbi. Serverul de joc trebuie să comunice cu sidecar-ul Agones pentru a vă raporta starea și a primi notificări privind ciclul de viață.
// 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();
}
}
Adaptor FleetIQ: Economii de costuri cu instanțe Spot
Unul dintre cele mai mari costuri în găzduirea serverelor de jocuri este computerul. AWS a dezvoltat Adaptor GameLift FleetIQ pentru Agones, care vă permite să rulați servere Agones pe Instanțe spot EC2, economisind până la 90% în comparație cu instanțele On-Demand.
Riscul de instanțe Spot și de întrerupere bruscă atunci când AWS are nevoie de această capacitate. FleetIQ atenuează acest risc utilizând algoritmi de învățare automată pentru a prezice întreruperile și menținând întotdeauna un buffer de instanțe On-Demand ca rezervă.
| Configurare | Cost/oră (c5.xlarge) | Economii | Risc de întrerupere |
|---|---|---|---|
| La cerere | 0,192 USD | - | 0% |
| Instanță spot | ~0,025-0,060 USD | 70-87% | 5-15% |
| Spot + FleetIQ | ~0,030-0,070 USD | 63-85% | <1% (predicție ML) |
| Punctul Graviton | ~0,020-0,040 USD | 79-90% | <1% |
Arhitectură multi-regională cu Global Accelerator
Pentru un joc cu o bază globală de jucători, este esențială asigurarea unei arhitecturi multi-regionale latență scăzută pentru toți jucătorii. AWS Global Accelerator direcționează traficul către regiune mai aproape, reducând latența percepută și îmbunătățind stabilitatea conexiunii.
// 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
}
}
Comparație: GameLift vs Agones
| Criteriu | AWS GameLift | Agones pe Kubernetes |
|---|---|---|
| Configurare inițială | Rapid, complet gestionat | Complex, necesită expertiza K8 |
| Blocarea vânzătorului | Ridicat (numai pentru AWS) | Scăzut (multi-nori) |
| Costuri fixe | Prețul pe sesiune + instanță | Numai costuri calculate |
| Matchmaking | FlexMatch integrat | Necesită Open Match separat |
| Scalare | Automat, gestionat | Manual + FleetAutoscaler |
| Multi-nor | No | Da (GKE, EKS, AKS) |
| Protocolul UDP | Si | Si |
| Instanțe spot | FleetIQ integrat | K8s Spot + Adaptor FleetIQ |
Când să alegeți GameLift
GameLift este cea mai bună alegere dacă sunteți deja puternic integrat în ecosistemul AWS, aveți o echipă mică fără expertiză Kubernetes și doriți să minimizați timpul de funcționare. Costul suplimentar al serviciu gestionat și adesea mai mic decât costul personalului necesar pentru gestionarea Kubernetes în producție.
Când să alegeți Agones
Agones este ideal pentru echipele cu expertiză Kubernetes, pentru jocurile care trebuie să ruleze pe mai mulți furnizori de cloud, sau când doriți să maximizați controlul asupra infrastructurii dvs. Și, de asemenea, alegerea potrivită dacă ai deja o infrastructură Kubernetes consacrată și doresc să o folosească pentru serverele de jocuri.
Verificarea și monitorizarea sănătății
Un sistem robust de verificare a stării de sănătate este esențial pentru a detecta mai devreme serverele de joc degradate care afectează jucătorii. Atât GameLift, cât și Agones acceptă controale periodice de sănătate, dar Este important să implementați valori personalizate pentru aplicații.
// 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');
});
Cele mai bune practici și anti-modele
Cele mai bune practici
- Preîncălziți flota: Mențineți întotdeauna un buffer de server READY pentru a evita pornirile la rece în timpul vârfurilor
- Instanțe Spot cu rezervă: Utilizați Spot pentru a reduce costurile, dar păstrați 10-20% la cerere ca rezervă
- Oprire grațioasă: Oferă serverelor 30-60 de secunde pentru a finaliza jocurile curente înainte de oprire
- Jurnalele persistente: Scrieți jurnalele pe S3 sau pe un sistem centralizat înainte de oprire
- Versiune imagini: Folosiți etichete imuabile (niciodată
latest) pentru implementări reproductibile - Afinitate regională: Atribuiți jucători pe serverele din regiunea cu cea mai mică latență
Anti-modele de evitat
- Starea globală pe server: Fiecare server de joc trebuie să fie apatrid în ceea ce privește infrastructura; starea jocului este locală sesiunii
- Flotă prea mare sau prea mică: Flotele subdimensionate provoacă așteptări; bani risipi supradimensionați
- Controale de sănătate prea agresive: Verificați în fiecare secundă consumați resurse; 5-10 secunde sunt de obicei suficiente
- Fără perioadă de scurgere: Uciderea unui server fără perioadă de scurgere pune capăt brusc jocurilor în curs
Concluzii
Orchestrarea serverelor de joc dedicate este o disciplină care necesită un echilibru delicat între costuri, latență și fiabilitate. AWS GameLift oferă o soluție gestionată care reduce complexitatea operațional, în timp ce Agones oferă flexibilitatea necesară pentru arhitecturile multi-cloud și echipele cu expertiză Kubernetes puternică. Combinația Agones cu adaptorul FleetIQ reprezintă probabil cel mai bun compromis pentru majoritatea jocurilor AAA: flexibilitate open-source cu economii costul instanțelor spot.
În următorul articol al seriei vom vedea cum să construim un sistem matchmaking sofisticat cu algoritmi ELO și Glicko-2, integrând matchmaking bazat pe abilități cu mecanisme de orchestrare pe care le-am explorat astăzi.
Articole viitoare din seria Game Backend
- Articolul 03: Sistemul de potrivire cu ELO și Glicko-2
- Articolul 04: Sincronizarea stării în timp real și retragerea codului net
- Articolul 05: Arhitectura anti-cheat pe partea de server







