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.
| Stato | Che cosa significa | Chi lo provoca |
|---|---|---|
| Ready | Il processo è su e può ricevere giocatori, ma nessuna partita gli è stata assegnata | Il server chiama ready() sull'SDK |
| Reserved | Tenuto da parte per un tempo limitato in attesa di giocatori | Il server chiama reserve() |
| Allocated | Assegnato a una partita; l'autoscaler non lo tocca | Un allocatore, o il server stesso con allocate() |
| Shutdown | La partita è finita, il pod viene eliminato | Il server chiama shutdown() |
| Unhealthy | Ha mancato i controlli di salute: non viene riavviato, esce dal servizio | Mancato 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.
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 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.
// 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.
// 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.onStartGameSessionarriva in risposta a una richiestaCreateGameSession: è qui che parte la partita.onProcessTerminatearriva 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.
| Criterio | Amazon GameLift Servers | Agones |
|---|---|---|
| Dove gira | EC2 e container gestiti da AWS, oppure sulla tua infrastruttura con la modalità Anywhere | Qualsiasi cluster Kubernetes |
| Chi opera la flotta | Il servizio, con tuo controllo sulle regole di scaling | Tu: cluster, aggiornamenti, osservabilità |
| Dipendenza dal fornitore | Alta per la parte gestita | Bassa: è software aperto |
| Matchmaking | FlexMatch, integrato o separato | Lo porti tu (vedi Open Match) |
| Linguaggi lato server | C++, 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:
onProcessTerminateo 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
- Agones, Fleet Autoscaler Specification - la politica Buffer,
bufferSize,minReplicas,maxReplicase la sincronizzazione a intervallo fisso, consultata il 2026-11-12 - Agones, Node.js Game Server Client SDK -
connect,health(errorCallback),ready,allocate,shutdown,watchGameServer, consultata il 2026-11-12 - Agones, GameServer Health Checking - lo stato Unhealthy e l'intervallo di
Health()rispetto aperiodSeconds, consultata il 2026-11-12 - Agones, Allocator Service - il servizio gRPC e REST con mTLS, consultata il 2026-11-12
- Agones, Scheduling and Autoscaling - la strategia Packed e la rimozione dei soli server Ready nella riduzione, consultata il 2026-11-12
- Amazon GameLift Servers, Add GameLift to your game server with the server SDK - linguaggi della server SDK, callback e regole del controllo di salute, consultata il 2026-11-12
- Amazon GameLift Servers FleetIQ, What is FleetIQ - il ruolo di FleetIQ con le Spot Instance, consultata il 2026-11-12
- Amazon GameLift Servers FlexMatch, What is FlexMatch - uso standalone o con l'hosting, consultata il 2026-11-12
- Amazon EC2, Spot Instances - l'avviso di interruzione di due minuti, consultata il 2026-11-12
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