Salta al contenuto principale

02 - Game Server Dedicati: Orchestrazione con GameLift e Agones

Come orchestrare i game server dedicati: ciclo di vita, Fleet e FleetAutoscaler di Agones, Amazon GameLift e un modello dell'autoscaler da eseguire.

Cosa ne pensi di questo articolo?

02 - Game Server Dedicati: Orchestrazione con GameLift e Agones
Game BackendFoto:Brett SaylesPexels

In Breve

Come orchestrare i game server dedicati: ciclo di vita, Fleet e FleetAutoscaler di Agones, Amazon GameLift e un modello dell'autoscaler da eseguire.

Chi avvia, assegna e spegne i game server

Un'arena da dieci giocatori dura cinque minuti. Se alla sera ne giocano trecento in parallelo, ti servono trecento processi server, ognuno con le proprie porte e il proprio stato in memoria, e devono nascere e morire al ritmo delle partite. Nessuno può farlo a mano e nessun bilanciatore web lo fa per te, perché un server di partita non è intercambiabile: è lo stesso ragionamento dell'articolo sull'anatomia del game backend, portato alla scala della flotta. L'orchestratore è il componente che tiene il conto: quanti server esistono, quali sono liberi, quale assegnare a una nuova partita, quando creare o togliere capacità.

Qui confrontiamo i due approcci più diffusi, Agones (si installa nel tuo cluster Kubernetes) e Amazon GameLift Servers (un servizio gestito). I fatti vengono dalla documentazione ufficiale, letta il 2026-11-12 e citata in fondo; dove un dettaglio non l'ho potuto verificare l'ho tolto. Il codice che gira davvero qui è un modello dell'autoscaler: nessun cluster, nessun conto cloud.

Il ciclo di vita di un server dentro Agones

Dal punto di vista dell'orchestratore, un game server è una macchina a stati. Prima di installare qualunque cosa conviene avere chiari gli stati, perché ogni decisione dell'autoscaler e dell'allocatore è una frase su questi stati. La tabella usa i nomi che la documentazione di Agones e la sua SDK mostrano.

StatoChe cosa significaChi lo provoca
ReadyIl processo è su e può ricevere giocatori, ma nessuna partita gli è stata assegnataIl server chiama ready() sull'SDK
ReservedTenuto da parte per un tempo limitato in attesa di giocatoriIl server chiama reserve()
AllocatedAssegnato a una partita; l'autoscaler non lo toccaUn allocatore, o il server stesso con allocate()
ShutdownLa partita è finita, il pod viene eliminatoIl server chiama shutdown()
UnhealthyHa mancato i controlli di salute: non viene riavviato, esce dal servizioMancato health() entro la soglia

La colonna centrale spiega perché il modello regge: chi decide è sempre il server (che sa se è pronto o se ha finito) o l'orchestratore (che sa quanti ne servono), mai un terzo. L'asimmetria che conta è la riga Allocated: durante una partita il server non si può spostare né eliminare per fare spazio a un altro. Per questo, quando la flotta si riduce, Agones toglie solo server Ready (con la strategia Packed parte da quelli sui nodi meno occupati, per poter liberare nodi interi). Una partita in corso non viene mai sacrificata a un risparmio.

Fleet e FleetAutoscaler: il manifest riga per riga

Una Fleet è l'insieme di GameServer tutti uguali; è l'equivalente di un Deployment, ma i suoi pod non sono fungibili. Il FleetAutoscaler la ridimensiona in base a una politica. Il manifest sotto non l'ho applicato a nessun cluster (schema da adattare); l'ho solo letto come YAML valido e confrontato con la referenza del FleetAutoscaler, ed è la stessa che il modello della sezione successiva interpreta.

Fleet e FleetAutoscaler di tipo Buffer - YAML (schema da adattare)
apiVersion: agones.dev/v1
kind: Fleet
metadata:
  name: shooter-fleet
spec:
  replicas: 10
  scheduling: Packed
  template:
    spec:
      ports:
        - name: default
          portPolicy: Dynamic
          containerPort: 7777
          protocol: UDP
      health:
        initialDelaySeconds: 30
        periodSeconds: 5
        failureThreshold: 3
      template:
        spec:
          containers:
            - name: game-server
              image: registry.example.com/game-server:1.2.0
              resources:
                requests: { cpu: 500m, memory: 512Mi }
                limits: { cpu: "1", memory: 1Gi }
---
apiVersion: autoscaling.agones.dev/v1
kind: FleetAutoscaler
metadata:
  name: shooter-fleet-autoscaler
spec:
  fleetName: shooter-fleet
  policy:
    type: Buffer
    buffer:
      bufferSize: 5
      minReplicas: 10
      maxReplicas: 100
  sync:
    type: FixedInterval
    fixedInterval:
      seconds: 30

Le righe che contano. portPolicy: Dynamic fa scegliere ad Agones una porta libera sul nodo, e il client la scopre dall'esito dell'allocazione: puoi quindi avere decine di server sullo stesso nodo. Il blocco health dice che il server ha 30 secondi prima del primo controllo, poi ne è atteso uno ogni 5, e dopo tre mancanze consecutive (failureThreshold) diventa Unhealthy. scheduling: Packed accumula i server sui nodi già usati; è la scelta pensata per cluster che crescono e calano con l'autoscaler dei nodi, mentre Distributed è per cluster di dimensione fissa. Infine l'autoscaler: bufferSize: 5 chiede di avere sempre cinque server Ready sopra gli Allocated, ma sempre tra minReplicas e maxReplicas, e lo valuta ogni 30 secondi.

Un dettaglio che il manifest dichiara solo per scelta: replicas: 10 è il valore di partenza, perché poi è l'autoscaler a riscriverlo. E l'immagine del server dev'essere un tag immutabile e mai latest (un tag mobile rende impossibile sapere quale versione sta giocando), così un rollback è un cambio di riga.

Che cosa succede quando la domanda sale: un modello dell'autoscaler

Per capire perché bufferSize e l'intervallo di 30 secondi non sono dettagli, ho scritto un modello. Legge i numeri direttamente da fleet.yaml (min, max, intervallo) e ci mette sopra tre ipotesi dichiarate: un nuovo server impiega 20 secondi per diventare Ready, una partita dura 300 secondi, e le richieste di partita arrivano a 0,05 al secondo con un picco di due minuti a 0,40. Sono numeri miei, non misure di un gioco. Ad ogni sincronizzazione l'autoscaler imposta le repliche a «Allocated più buffer», entro i limiti; se le richieste trovano zero server Ready, le conto come richieste senza server. Il generatore ha un seme fisso, quindi l'uscita si riproduce.

Modello di Fleet con buffer - Python
# Modello didattico di una Fleet con FleetAutoscaler di tipo Buffer.
# Legge i parametri dal manifest fleet.yaml; il resto (arrivi, durata partite) e' inventato.
import random
import yaml

docs = list(yaml.safe_load_all(open("fleet.yaml")))
policy = docs[1]["spec"]["policy"]["buffer"]
SYNC_S = docs[1]["spec"]["sync"]["fixedInterval"]["seconds"]
MIN_R, MAX_R = policy["minReplicas"], policy["maxReplicas"]
START_S = 20      # secondi tra "crea il server" e "Ready" (ipotesi del modello)
MATCH_S = 300     # durata di una partita
DURATION = 1800

def arrival_rate(t: int) -> float:
    return 0.40 if 600 <= t < 720 else 0.05   # richieste al secondo: picco di 2 minuti

def run(buffer_size: int, seed: int = 7):
    rng = random.Random(seed)
    starting, ready, allocated = [], 0, []   # starting: secondi di Ready; allocated: fine partita
    rejected = served = peak = 0
    for t in range(DURATION):
        starting_now = [s for s in starting if s <= t]
        ready += len(starting_now)
        starting = [s for s in starting if s > t]
        allocated = [e for e in allocated if e > t]
        for _ in range(sum(1 for _ in range(10) if rng.random() < arrival_rate(t) / 10)):
            if ready > 0:
                ready -= 1; allocated.append(t + MATCH_S); served += 1
            else:
                rejected += 1                  # nessun server Ready: il giocatore aspetta o riprova
        if t % SYNC_S == 0:                    # il FleetAutoscaler lavora a intervalli fissi
            desired = max(MIN_R, min(MAX_R, len(allocated) + buffer_size))
            total = ready + len(starting) + len(allocated)
            if total < desired:
                starting += [t + START_S] * (desired - total)
            elif total > desired:
                ready -= min(ready, total - desired)   # si tolgono solo server Ready, mai Allocated
        peak = max(peak, ready + len(starting) + len(allocated))
    return served, rejected, peak

print(f"sync ogni {SYNC_S} s, min {MIN_R}, max {MAX_R}, avvio server {START_S} s")
for buf in (5, 15, 40):
    served, rejected, peak = run(buf)
    print(f"bufferSize={buf:>2}: partite avviate {served:>3}, richieste senza server {rejected:>3}, flotta massima {peak}")

# Output:
# sync ogni 30 s, min 10, max 100, avvio server 20 s
# bufferSize= 5: partite avviate 108, richieste senza server  38, flotta massima 34
# bufferSize=15: partite avviate 133, richieste senza server  13, flotta massima 66
# bufferSize=40: partite avviate 144, richieste senza server   2, flotta massima 100

L'uscita mostra il compromesso in tre righe. Con un buffer di 5, durante il picco ben 38 richieste su 146 trovano la flotta vuota, perché i server nuovi impiegano 20 secondi e l'autoscaler si sveglia solo ogni 30. Con un buffer di 15 le richieste respinte sono 13 e la flotta arriva a 66 server; con 40 sono 2, ma la flotta tocca il tetto di 100 e paghi server inutilizzati per tutto il resto della sera. Il buffer è quindi un prezzo che paghi in anticipo per coprire il tempo di avvio: la sua misura giusta è «picco atteso di richieste nell'intervallo di sincronizzazione più il tempo di avvio», non un numero tondo. Il modello non include il tempo di avvio del nodo Kubernetes, che se manca capacità sul cluster si somma e peggiora tutto.

Assegnare una partita e parlare con l'SDK

Un server Ready non fa nulla finché qualcuno non glielo assegna. Agones offre per questo un allocatore con interfaccia gRPC e REST protetto da mTLS: il tuo matchmaker deve presentare un certificato client accettato, e non può chiamare un semplice indirizzo HTTP come mostrano alcuni esempi (la documentazione del servizio descrive il certificato e il secret da predisporre). In alternativa il matchmaker crea una risorsa GameServerAllocation attraverso l'API di Kubernetes. Chi decide chi gioca con chi è il matchmaker, che è il tema dell'articolo sul matchmaking.

Lato server il codice usa l'SDK, che parla con un sidecar locale. Il blocco seguente è lo schema minimo con la libreria Node.js (@google-cloud/agones-sdk); non è stato eseguito perché richiede il sidecar. Due righe fanno la differenza: health è invocata ogni 2 secondi, meno dei 5 del manifest, come chiede la documentazione (se il ping arriva a intervalli uguali o maggiori, i controlli falliscono e il server passa a Unhealthy a metà partita); e ready() è l'ultima cosa che fai, dopo aver caricato mappe e risorse, perché da quel momento il server è assegnabile.

Server che segnala salute e stato ad Agones - Node.js (schema da adattare)
// Schema da adattare: richiede un cluster con Agones e il sidecar dell'SDK.
const AgonesSDK = require("@google-cloud/agones-sdk");
const agones = new AgonesSDK();

async function main() {
  await agones.connect();                     // si risolve a connessione avvenuta, rifiuta dopo 30 s
  // Il ping deve arrivare a intervalli MINORI di spec.health.periodSeconds (qui 5 s)
  setInterval(() => agones.health((err) => console.error("health", err)), 2000);
  agones.watchGameServer((gs) => {
    if (gs.status.state === "Allocated") startMatch(gs.objectMeta.labels, gs.objectMeta.annotations);
  });
  await agones.ready();                       // da qui l'allocatore puo' assegnarci una partita
}

async function endMatch() {
  await saveResults();
  await agones.shutdown();                    // Agones elimina il pod
}

Al termine della partita shutdown() chiede ad Agones di eliminare il pod: il processo non deve uscire da solo, o l'orchestratore lo conta come guasto. Il callback sull'allocazione legge etichette e annotazioni, ed è il canale con cui il matchmaker passa al server la mappa e il numero di giocatori attesi.

Salute: una domanda sola, «sei vivo?»

È allettante far dire al controllo di salute «sono lento» quando il tick rate scende. Il listato calcola proprio quella regola (degradato sotto l'80% del target) su quattro casi.

Soglia di degrado sul tick rate - TypeScript
// Decisione di salute basata sul tick rate misurato. Funzione pura, nessun server.
interface Sample { readonly tickRateHz: number; readonly targetHz: number; readonly activePlayers: number }

function healthOf(s: Sample): { status: "ok" | "degraded"; httpCode: 200 | 503 } {
  const degraded = s.tickRateHz < s.targetHz * 0.8;
  return degraded ? { status: "degraded", httpCode: 503 } : { status: "ok", httpCode: 200 };
}

const cases: Sample[] = [
  { tickRateHz: 64, targetHz: 64, activePlayers: 10 },
  { tickRateHz: 52, targetHz: 64, activePlayers: 10 },  // 52 > 51,2: ancora ok
  { tickRateHz: 51, targetHz: 64, activePlayers: 10 },  // 51 < 51,2: degradato
  { tickRateHz: 0, targetHz: 64, activePlayers: 0 },    // processo bloccato
];
for (const c of cases) console.log(`${c.tickRateHz} Hz su ${c.targetHz}:`, healthOf(c));

// Output:
// 64 Hz su 64: { status: 'ok', httpCode: 200 }
// 52 Hz su 64: { status: 'ok', httpCode: 200 }
// 51 Hz su 64: { status: 'degraded', httpCode: 503 }
// 0 Hz su 64: { status: 'degraded', httpCode: 503 }

Il calcolo funziona, il suo uso è pericoloso. Un server a 51 Hz su 64 viene dichiarato degradato, ma se quel verdetto arrivasse all'orchestratore come Unhealthy, Agones lo sostituirebbe nel mezzo di una partita: peggiora l'esperienza che voleva proteggere. Meglio dividere i ruoli. Il controllo di salute dell'orchestratore risponde solo a «il processo risponde?» e lascia che sia l'unico a poter far morire un server. La degradazione diventa una metrica con un allarme per te, e al massimo un motivo per non assegnare nuove partite a quel server. Lo stesso criterio vale per onHealthCheck di GameLift: la documentazione dice che dopo tre risposte negative o assenti consecutive il servizio può chiudere il processo e avviarne un altro.

Amazon GameLift Servers: stesso problema, servizio gestito

GameLift risolve gli stessi problemi spostando l'orchestratore fuori dal tuo cluster. Il game server include una server SDK: la guida ufficiale elenca per ora C++, C#, C++ per Unreal e Go, più i plugin integrati di Unreal e Unity. Per un server scritto in Node.js non è elencata una SDK ufficiale: se il tuo gioco è lì, con GameLift scegli un altro linguaggio per il processo server, oppure ripieghi su Agones. Il contratto che la SDK impone è comunque chiaro e si descrive a parole.

  • InitSdk() stabilisce il collegamento con il servizio, prima di qualunque altra chiamata.
  • ProcessReady() dichiara il processo pronto, con la porta e i percorsi dei log; si chiama una volta per processo.
  • onStartGameSession arriva in risposta a una richiesta CreateGameSession: è qui che parte la partita.
  • onProcessTerminate arriva quando il servizio vuole fermare il processo: devi chiudere in modo ordinato.
  • onHealthCheck è opzionale; se non risponde entro 60 secondi, il processo è considerato non sano.

La lista ha un senso nell'ordine in cui compare, perché racconta il ciclo di vita: inizializzi, annunci di essere pronto, ricevi una partita, ne rispondi della salute, ti fermi quando ti viene chiesto. Rispetto ad Agones la differenza di filosofia sta in onProcessTerminate: qui è il servizio a «chiamarti», mentre in Agones sei tu a chiamare il sidecar. Il modello a callback è più comodo se parti da zero; quello a sidecar è più neutro rispetto al linguaggio.

GameLift offre anche il matchmaking FlexMatch, utilizzabile con l'hosting GameLift o come servizio a sé, per esempio davanti a giochi peer-to-peer o ospitati altrove. Le sue regole sono una vera lingua (squadre, attributi dei giocatori, espansioni nel tempo) che merita una sezione propria: qui basta sapere che è un componente separabile, e che trovi il quadro nella guida di FlexMatch.

Spot: risparmiare e sopravvivere all'interruzione

Il compute è la voce di costo maggiore, e le Spot Instance di EC2 sono la leva classica: capacità in eccesso a prezzo ridotto, con il rischio di interruzione. La guida di FleetIQ lo dice senza giri di parole: le Spot, pur scontate, in generale non sono adatte all'hosting di giochi perché la disponibilità varia e le interruzioni sono possibili; FleetIQ esiste per rendere quell'uso praticabile, ed è disponibile anche dentro GameLift gestito. Le cifre di sconto che si leggono ovunque («fino al 90%») non le ho potute verificare sulla pagina ufficiale e non le uso: i prezzi cambiano per regione e per tipo di istanza, e vanno letti nel listino il giorno in cui decidi.

Una cosa vale con qualsiasi sconto: l'interruzione Spot arriva con un avviso di due minuti (documentazione EC2). Una partita da cinque minuti non finisce in due minuti, quindi o la tolleri (rimborsi, riavvio, salvataggio dello stato) oppure tieni i server Allocated su capacità On-Demand e metti su Spot quelli in attesa. Le due politiche hanno costi diversi, e la scelta è un fatto di prodotto, non di infrastruttura: quanto fa arrabbiare un giocatore una partita persa?

GameLift o Agones: come scegliere

I criteri sotto sono quelli che separano davvero le due strade; nessuno dipende da un prezzo, perché i prezzi dei servizi gestiti si leggono nel listino e quelli di un cluster dipendono da chi lo gestisce.

CriterioAmazon GameLift ServersAgones
Dove giraEC2 e container gestiti da AWS, oppure sulla tua infrastruttura con la modalità AnywhereQualsiasi cluster Kubernetes
Chi opera la flottaIl servizio, con tuo controllo sulle regole di scalingTu: cluster, aggiornamenti, osservabilità
Dipendenza dal fornitoreAlta per la parte gestitaBassa: è software aperto
MatchmakingFlexMatch, integrato o separatoLo porti tu (vedi Open Match)
Linguaggi lato serverC++, C#, Go (più Unreal e Unity)Qualunque: l'SDK è un sidecar con gRPC/REST

La riga decisiva è «Chi opera la flotta». Se hai già un cluster Kubernetes e le persone che lo gestiscono, Agones ti dà portabilità e controllo senza un secondo sistema da imparare. Se il tuo team è di quattro persone e nessuna conosce Kubernetes, il costo del servizio gestito è spesso minore di quello del tempo che spenderesti a operare il cluster (ragionamento, non misura). In entrambi i casi la separazione resta valida: il gioco parla con un'interfaccia sottile (pronto, assegnato, fine), e se la tieni sottile puoi cambiare orchestratore senza riscrivere la simulazione.

Cinque abitudini che evitano le sorprese in produzione

  • Un buffer dimensionato sul tempo di avvio, come mostra il modello, non su un numero tondo.
  • Un avviso di chiusura ordinato: onProcessTerminate o la gestione del segnale di terminazione devono finire le partite, salvare i risultati e chiamare lo shutdown.
  • Immagini con tag immutabili, per poter dire quale versione ha prodotto un incidente.
  • Log fuori dal server: sul disco del nodo spariscono con il nodo.
  • Controlli di salute rari e leggeri: ogni controllo è lavoro sottratto al tick.

Le cinque voci hanno una radice comune: un game server è un processo di breve vita e va trattato come tale, con tutto ciò che deve sopravvivergli (log, risultati, versione) conservato altrove. Se ne adotti una sola, scegli la prima, perché è l'unica che il lettore finale percepisce: un buffer troppo piccolo si traduce in attese di matchmaking, e un buffer sproporzionato in una fattura. Il costo operativo del resto lo scopri con la metrica; il modo di leggerla sta nell'articolo sull'osservabilità del game backend.

Che cosa sai fare adesso

Sai descrivere gli stati di un game server e dire chi li fa cambiare. Sai leggere un manifest di Fleet e FleetAutoscaler spiegando ogni campo che conta, e sai simulare l'effetto di bufferSize prima di scegliere un numero. Sai distinguere un controllo di salute da una metrica di qualità, e dove mettere le Spot Instance. Sai infine confrontare GameLift e Agones sul criterio giusto, cioè chi opera la flotta. Il passo logico successivo è decidere chi entra in quale partita: matchmaking con Elo e Glicko-2.

Domande frequenti

Agones è alternativo a Kubernetes?

No, ci gira sopra. Agones è una libreria per ospitare, eseguire e scalare dedicated game server su Kubernetes, e aggiunge risorse come GameServer, Fleet e FleetAutoscaler. Senza un cluster Kubernetes non funziona.

Posso usare Node.js per il server su GameLift?

Non con una SDK server ufficiale: la guida AWS elenca C++, C#, Unreal e Go. Con Agones sì, perché l'SDK è un sidecar raggiungibile via gRPC e REST e esiste una libreria Node.js nella documentazione.

Quanti server devo tenere pronti in anticipo?

Almeno quanti ne consumi nel tempo che serve a crearne altri: il picco di richieste nell'intervallo di sincronizzazione più il tempo di avvio del server. Il modello di questo articolo mostra come cambia il numero di richieste senza server al variare del buffer.

Fonti

Il modello dell'autoscaler e il calcolo della salute sono stati eseguiti con Python 3 (PyYAML) e Node.js 24; le uscite sono quelle reali, ma gli arrivi, la durata delle partite e il tempo di avvio sono ipotesi del modello. Il manifest e il listato dell'SDK non sono stati applicati a un cluster: sono schemi da adattare. Nessuna cifra di prezzo o di sconto è riportata.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti