Osservabilità di un game backend: misurare ciò che il giocatore sente
Un game backend può avere ogni dashboard verde e far giocare male. Il tasso di errore HTTP è basso, la CPU è al 40%, e intanto alcuni giocatori vedono il personaggio scattare perché il server ha saltato dei tick. Le metriche di un servizio web (richieste al secondo, errori, latenza media) non vedono questo tipo di problema. Servono misure del dominio: il tick del server, la coda lunga della latenza, e allarmi che scattano per ciò che consuma davvero la fiducia dei giocatori e non per ogni oscillazione.
L'articolo affronta quattro decisioni che si pagano care se prese male, ciascuna con un listato eseguito su dati sintetici con seme fisso: quali etichette mettere sulle metriche, come leggere il tick senza farsi ingannare dalla media, perché i percentili vengono approssimati e come scegliere i bucket, e quando un allarme merita di svegliare qualcuno. Prometheus, Grafana e un collettore OpenTelemetry non sono installati in questa macchina: dove un frammento è per quei sistemi lo dichiaro «schema da adattare» e non riporto un'uscita.
Il tick come unità di lavoro del server è introdotto in sincronizzazione dello stato in tempo reale; la parte di latenza vista dal giocatore, con il suo budget, è in cloud gaming con WebRTC. Gli esperimenti e le configurazioni che cambi in produzione, e che le metriche devono poter attribuire, sono in LiveOps con feature flag.
Che cosa misurare
Un esempio concreto aiuta a scegliere: un giocatore ha un'esperienza cattiva se sente comandi in ritardo (latenza), vede movimenti a scatti (tick saltati o perdita di pacchetti), aspetta troppo prima di giocare (coda del matchmaking) o abbandona la partita. A ciascuna sensazione corrisponde una misura.
- Rete: RTT per connessione e perdita di pacchetti, come istogramma, per ottenere la distribuzione e non la media
- Ciclo di gioco: durata di ogni tick e numero di tick che superano il proprio periodo
- Partita: abbandoni, durata, partite attive
- Matchmaking: profondità della coda e tempo di attesa, anche questo come istogramma
- Capacità: giocatori connessi e server occupati, per dimensionare
Nessuna di queste voci ha qui un valore «obiettivo» universale: le soglie vengono dal tuo gioco. Uno sparatutto competitivo e un gioco di carte non hanno la stessa tolleranza sulla latenza, e per questo l'articolo non riporta tabelle di target. Dove ho bisogno di un numero (un bucket, una soglia) lo dichiaro come scelta del progetto, da cambiare con i tuoi dati.
Le etichette decidono quanto costa la metrica
Prometheus memorizza una serie temporale per ogni combinazione di valori delle etichette. La sua documentazione di buone pratiche è netta: non usare etichette per
dimensioni ad alta cardinalità, come identificativi utente. Il motivo è aritmetico, e il listato lo mostra: un istogramma classico con 10 bucket crea
13 serie per combinazione (10 bucket, il bucket +Inf, e le serie _sum e _count), quindi mettere player_id fra le etichette
moltiplica le serie per il numero di giocatori.
import random
# Una metrica produce una serie temporale per ogni combinazione REALE di valori delle etichette.
# Un istogramma classico di Prometheus ne crea (bucket + 1 per +Inf + 2 per _sum e _count) per combinazione.
BUCKETS = 10
PER_HISTOGRAM = BUCKETS + 1 + 2
rng = random.Random(1)
players = [(f"player_{i}", rng.choice(["eu", "us", "asia", "sa"]), rng.choice(["pc", "console", "mobile"])) for i in range(20_000)]
per_player = len(set(players)) * PER_HISTOGRAM # etichette: player_id, region, platform
per_region = len({(r, p) for _, r, p in players}) * PER_HISTOGRAM # etichette: region, platform
per_match_gauge = 2_000 # un gauge con etichetta match_id, 2.000 partite attive
per_region_gauge = len({r for _, r, _ in players})
fmt = lambda n: f"{n:,}".replace(",", ".")
print(f"istogramma RTT con player_id (20.000 online): {fmt(per_player)} serie")
print(f"istogramma RTT per region e platform: {fmt(per_region)} serie")
print(f"gauge tickrate con match_id (2.000 partite): {fmt(per_match_gauge)} serie")
print(f"gauge tickrate per region: {fmt(per_region_gauge)} serie")
print(f"rapporto fra le due versioni dell'istogramma: {per_player // per_region}x")
# Output:
# istogramma RTT con player_id (20.000 online): 260.000 serie
# istogramma RTT per region e platform: 156 serie
# gauge tickrate con match_id (2.000 partite): 2.000 serie
# gauge tickrate per region: 4 serie
# rapporto fra le due versioni dell'istogramma: 1666x
Con 20.000 giocatori online l'istogramma dell'RTT per giocatore produce 260.000 serie, contro 156 se lo aggreghi per regione e piattaforma: oltre 1.600 volte di più per un'informazione che nessun dashboard legge giocatore per giocatore. Lo stesso vale per il gauge del tickrate con la partita come etichetta: 2.000 serie contro 4. Il caso del singolo giocatore va risolto con i log e le tracce, che sono pensati per questo, e le metriche restano aggregati. Lo schema in Go che segue mostra la versione corretta delle definizioni: etichette di regione e piattaforma, secondi come unità, e un bucket che coincide con la soglia dell'SLO.
// Schema da adattare, NON eseguito (Go non è installato): metriche con etichette a bassa cardinalità.
package metrics
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
var (
// Unità di base: secondi. Il bucket a 0,1 coincide con la soglia dell'SLO (100 ms).
PlayerRTT = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "gameserver", Subsystem: "network", Name: "rtt_seconds",
Help: "Round-trip time misurato dal server",
Buckets: []float64{0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.15, 0.2, 0.3, 0.5},
}, []string{"region", "platform"}) // niente player_id, niente match_id
// Quanto dura l'elaborazione di un tick: il bucket a 0,015625 è il periodo a 64 tick/s.
TickSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{
Namespace: "gameserver", Subsystem: "loop", Name: "tick_seconds",
Help: "Tempo di elaborazione di un tick",
Buckets: []float64{0.002, 0.004, 0.008, 0.015625, 0.03125, 0.0625},
}, []string{"region", "mode"})
// Tick che hanno superato il proprio periodo: il segnale che la media nasconde.
TickOverruns = promauto.NewCounterVec(prometheus.CounterOpts{
Namespace: "gameserver", Subsystem: "loop", Name: "tick_overruns_total",
Help: "Tick oltre il periodo",
}, []string{"region", "mode"})
)
Non ho potuto compilarlo: Go non è installato qui. Le firme (promauto.NewHistogramVec, HistogramOpts, CounterOpts) sono quelle
del client Go di Prometheus; verificale con la tua versione. La documentazione di Prometheus raccomanda anche le unità di base, e per il tempo sono i secondi,
che è il motivo per cui l'RTT qui è in secondi e il bucket dell'SLO è 0,1 e non 100.
Il tick: la media nasconde i picchi
Un server a 64 tick al secondo ha 15,6 ms per elaborare ogni tick. Se la media dei tick dura 8 ms sembra tutto a posto, ma un tick che dura 45 ms fa saltare quasi tre periodi, e il giocatore lo vede come uno scatto. Il listato simula un minuto di tick con costo per lo più basso e il 2% di picchi sei volte più costosi (garbage collection, una scena affollata, un accesso a disco), poi misura media, percentili e tickrate effettivo di un ciclo che non recupera il ritardo.
import random, statistics
# Un game loop a 64 tick/s (periodo 15,625 ms). Il costo di un tick è per lo più basso, ogni tanto un picco.
rng = random.Random(9)
PERIOD = 1000 / 64
def tick_cost():
base = rng.lognormvariate(1.9, 0.35) # ms: mediana ~6,7 ms
return base * (6 if rng.random() < 0.02 else 1) # il 2% dei tick costa 6 volte tanto (GC, picco di entità, I/O)
costs = [tick_cost() for _ in range(64 * 60)] # un minuto di tick
def late_loop(costs):
"""Loop senza recupero: ogni tick occupa almeno un periodo; uno lento ne occupa di più e fa slittare i successivi."""
t, ticks = 0.0, 0
for c in costs:
t += max(c, PERIOD) # un tick lento occupa più di un periodo e il resto slitta
if t > 60_000: break
ticks += 1
return ticks / 60
print(f"media del costo di un tick: {statistics.mean(costs):.1f} ms ({100*statistics.mean(costs)/PERIOD:.0f}% del periodo di {PERIOD:.1f} ms)")
qs = statistics.quantiles(costs, n=100)
print(f"p50 {qs[49]:.1f} ms, p99 {qs[98]:.1f} ms")
over = sum(1 for c in costs if c > PERIOD)
print(f"tick oltre il periodo: {over} su {len(costs)} ({100*over/len(costs):.1f}%)")
print(f"tickrate effettivo con loop che slitta: {late_loop(costs):.1f} tick/s (target 64)")
# Output:
# media del costo di un tick: 8.0 ms (51% del periodo di 15.6 ms)
# p50 6.8 ms, p99 45.1 ms
# tick oltre il periodo: 111 su 3840 (2.9%)
# tickrate effettivo con loop che slitta: 61.3 tick/s (target 64)
La media è 8 ms, il 51% del periodo; il p50 6,8 ms; il p99 sale a 45,1 ms. Il 2,9% dei tick (111 su 3.840) supera il proprio periodo, e il tickrate effettivo del loop
è 61,3 tick/s contro i 64 attesi. Il dato da sorvegliare è quindi la frazione di tick in ritardo e la coda dei tempi di tick, non la media. Nota anche
che un allarme sul tickrate che scatta sotto il 90% del target (57,6 tick/s) qui non scatterebbe mai, pur con tre scatti ogni cento tick: per questo lo schema Go
ha il contatore tick_overruns_total accanto all'istogramma. I numeri vengono dal mio modello, non da un server vero; la forma (media buona, coda cattiva) è quella da cercare nei tuoi dati.
Percentili: approssimazioni, e dove mettere i bucket
Un istogramma di Prometheus conserva solo quanti campioni cadono sotto ciascuna soglia. Il percentile lo stima histogram_quantile
interpolando linearmente dentro il bucket in cui cade il rango; la documentazione avverte che la stima è più accurata con bucket più fitti. Il listato
implementa quella regola e la confronta con il percentile calcolato sui dati grezzi, usando 50.000 RTT sintetici con l'1,5% di picchi fra 150 e 400 ms.
import random, bisect
rng = random.Random(4)
# RTT sintetici in ms: la maggior parte tra 20 e 70, l'1,5% sono picchi tra 150 e 400
rtts = [rng.lognormvariate(3.7, 0.35) if rng.random() > 0.015 else rng.uniform(150, 400) for _ in range(50_000)]
def to_histogram(values, bounds):
"""Contatori cumulativi come in Prometheus: per ogni 'le' quanti campioni sono <= le."""
s = sorted(values)
return [(le, bisect.bisect_right(s, le)) for le in bounds] + [(float("inf"), len(s))]
def histogram_quantile(q, hist):
"""Stessa regola di Prometheus: trova il bucket del rango e interpola linearmente al suo interno."""
total = hist[-1][1]
rank = q * total
prev_le, prev_count = 0.0, 0
for le, count in hist:
if count >= rank:
if le == float("inf"):
return prev_le # oltre l'ultimo bucket finito si restituisce il suo estremo
return prev_le + (le - prev_le) * (rank - prev_count) / (count - prev_count)
prev_le, prev_count = le, count
true_p99 = sorted(rtts)[int(0.99 * len(rtts)) - 1]
FINE = [10, 20, 40, 60, 80, 100, 150, 200, 300, 500]
COARSE = [50, 100, 500]
print(f"p99 vero sui campioni grezzi: {true_p99:.1f} ms")
for name, bounds in (("10 bucket", FINE), ("3 bucket ", COARSE)):
h = to_histogram(rtts, bounds)
print(f"{name}: p99 stimato {histogram_quantile(0.99, h):.1f} ms")
ok = dict(h)[100] / len(rtts)
print(f" quota con RTT <= 100 ms, letta dal bucket le=100: {100*ok:.2f}%")
exact = sum(1 for v in rtts if v <= 100) / len(rtts)
print(f"quota con RTT <= 100 ms sui campioni grezzi: {100*exact:.2f}%")
# Output:
# p99 vero sui campioni grezzi: 231.8 ms
# 10 bucket: p99 stimato 232.8 ms
# quota con RTT <= 100 ms, letta dal bucket le=100: 98.03%
# 3 bucket : p99 stimato 297.2 ms
# quota con RTT <= 100 ms, letta dal bucket le=100: 98.03%
# quota con RTT <= 100 ms sui campioni grezzi: 98.03%
Il p99 vero è 231,8 ms. Con i 10 bucket del nostro schema la stima è 232,8 ms; con soli 3 bucket (50, 100, 500) sale a 297,2 ms, un errore di 65 ms provocato solo dalla scelta dei confini. La quota di richieste sotto i 100 ms, invece, è esatta in entrambi i casi (98,03%), perché 100 è un confine di bucket. Ne segue una regola di progetto: metti un bucket esattamente sulla soglia del tuo SLO, e ogni conteggio su quella soglia sarà esatto. È ciò che la documentazione di Prometheus suggerisce, quando dice che se conosci in anticipo un SLO puoi usare confini fissi per ottenere un calcolo accurato.
Allarmi: quando svegliare qualcuno
Un allarme a soglia fissa («errori sopra lo 0,5%») scatta anche per un picco brevissimo che non intacca in modo significativo il mese. L'alternativa descritta nel workbook di Google sulla SRE è guardare la velocità di consumo del budget di errore (burn rate). Con un SLO del 99,5% il budget è lo 0,5% delle richieste del mese; un burn rate 14,4 su un'ora consuma il 2% del budget mensile, ed è la soglia che il workbook associa a una pagina. Per evitare falsi allarmi, la regola richiede che anche la finestra di 5 minuti sia sopra soglia, così l'allarme si spegne appena il problema finisce.
import random
rng = random.Random(12)
SLO, MIN_MONTH = 0.995, 30 * 24 * 60
BUDGET = 1 - SLO # quota di errori ammessa: 0,5%
# 24 ore di rapporto errori per minuto: rumore di fondo + tre eventi
err = [max(0.0, rng.gauss(0.0015, 0.0020)) for _ in range(1440)]
def add(start, minutes, ratio):
for m in range(start, start + minutes): err[m] = ratio
add(200, 3, 0.012) # B: picco di 3 minuti all'1,2%
add(600, 20, 0.40) # A: incidente vero, 20 minuti con il 40% di richieste in errore
add(900, 360, 0.008) # C: bruciatura lenta, 6 ore allo 0,8%
def avg(series, end, minutes):
w = series[max(0, end - minutes + 1): end + 1]
return sum(w) / len(w)
def firing_periods(rule):
out, active = [], False
for m in range(1440):
on = rule(m)
if on and not active: out.append(m)
active = on
return out
# Regola 1: soglia fissa. Media sugli ultimi 5 minuti sopra il budget (0,5%), per almeno 2 minuti consecutivi
def threshold(m): return m >= 1 and avg(err, m, 5) > BUDGET and avg(err, m - 1, 5) > BUDGET
# Regola 2: burn rate multi-finestra (SRE Workbook): 14,4 volte il ritmo di consumo, su 1 ora E su 5 minuti
def burn(m): return avg(err, m, 60) > 14.4 * BUDGET and avg(err, m, 5) > 14.4 * BUDGET
def consumed(start, minutes, ratio): return 100 * ratio * minutes / (BUDGET * MIN_MONTH)
print(f"budget mensile: {BUDGET * MIN_MONTH:.0f} minuti-equivalenti di errore totale")
for name, s, d, r in (("B picco 3 min all'1,2%", 200, 3, 0.012), ("A incidente 20 min al 40%", 600, 20, 0.40), ("C lenta 6 h allo 0,8%", 900, 360, 0.008)):
print(f" evento {name}: consuma {consumed(s, d, r):.2f}% del budget mensile")
for name, rule in (("soglia fissa 5m > 0,5% per 2m", threshold), ("burn rate 14,4x su 1h e 5m", burn)):
f = firing_periods(rule)
print(f"{name}: {len(f)} allarmi, inizi ai minuti {f}")
a = [m for m in f if 600 <= m < 640]
print(f" rileva A? {'sì, dopo ' + str(a[0] - 600) + ' minuti' if a else 'no'}; rileva B? {'sì' if any(200 <= m < 215 for m in f) else 'no'}; rileva C? {'sì' if any(900 <= m < 1260 for m in f) else 'no'}")
# Output:
# budget mensile: 216 minuti-equivalenti di errore totale
# evento B picco 3 min all'1,2%: consuma 0.02% del budget mensile
# evento A incidente 20 min al 40%: consuma 3.70% del budget mensile
# evento C lenta 6 h allo 0,8%: consuma 1.33% del budget mensile
# soglia fissa 5m > 0,5% per 2m: 3 allarmi, inizi ai minuti [202, 601, 903]
# rileva A? sì, dopo 1 minuti; rileva B? sì; rileva C? sì
# burn rate 14,4x su 1h e 5m: 1 allarmi, inizi ai minuti [610]
# rileva A? sì, dopo 10 minuti; rileva B? no; rileva C? no
L'evento B è un picco di 3 minuti all'1,2% che consuma lo 0,02% del budget mensile: la soglia fissa lo segnala (allarme al minuto 202), il burn rate no, ed è giusto. L'evento A è un incidente vero, 20 minuti con il 40% di richieste in errore, il 3,70% del budget: entrambi lo vedono, ma la soglia dopo 1 minuto e il burn rate dopo 10, e questo è il prezzo da pagare. L'evento C è una bruciatura lenta (6 ore allo 0,8%, l'1,33% del budget): la soglia fissa lo segnala, il burn rate rapido no, perché è fatto per i consumi veloci. Per le bruciature lente il workbook prevede una regola separata su finestre lunghe (10% del budget in 3 giorni, burn rate 1) che apre un ticket e non sveglia nessuno.
# Schema da adattare: sintassi YAML verificata, regole NON valutate da Prometheus (promtool non è installato).
groups:
- name: rtt-slo
rules:
- record: slo:rtt_over_100ms:ratio_rate5m
expr: 1 - sum(rate(gameserver_network_rtt_seconds_bucket{le="0.1"}[5m])) / sum(rate(gameserver_network_rtt_seconds_count[5m]))
- record: slo:rtt_over_100ms:ratio_rate1h
expr: 1 - sum(rate(gameserver_network_rtt_seconds_bucket{le="0.1"}[1h])) / sum(rate(gameserver_network_rtt_seconds_count[1h]))
# SLO 99,5% -> budget 0,005. Consumo 14,4 volte più veloce del ritmo sostenibile, su 1 ora E su 5 minuti.
- alert: RttBudgetBurnFast
expr: slo:rtt_over_100ms:ratio_rate1h > (14.4 * 0.005) and slo:rtt_over_100ms:ratio_rate5m > (14.4 * 0.005)
labels:
severity: page
annotations:
summary: "Il budget dell'SLO sull'RTT si consuma troppo in fretta"
Le due regole record precalcolano la frazione di richieste oltre 100 ms su 5 minuti e un'ora, usando proprio il bucket le="0.1" definito prima.
L'allarme scatta solo se entrambe superano 14,4 volte il budget (0,005). Il file è YAML valido (l'ho letto con un parser), ma le regole non sono state valutate da Prometheus,
perché promtool non è installato: provale su dati reali con promtool test rules prima di affidare loro una pagina.
Tracce: campionare senza perdere gli errori
Una traccia distribuita segue una richiesta da un servizio all'altro, utile per capire perché un matchmaking richiede 8 secondi invece di 2. Salvare tutte le tracce costa troppo, quindi si campiona. La documentazione di OpenTelemetry distingue il campionamento head, in cui la decisione è presa il prima possibile e non guarda la traccia nel suo insieme, dal campionamento tail, in cui si decide a traccia completa. Con un campionatore a probabilità fissa (per esempio il 10% degli identificativi di traccia) non puoi garantire di tenere le tracce con errori: al momento della decisione non si sa ancora che ci sarà un errore. Se vuoi tutte le tracce lente o fallite, il campionamento tail nel collettore è lo strumento adatto, al prezzo di far transitare e tenere in memoria le tracce fino alla decisione.
Un punteggio di esperienza: utile, ma attenzione alla media
Per i dashboard di gestione si vuole spesso un solo numero: un punteggio di esperienza da 0 a 100 che combini RTT, tickrate e perdita di pacchetti. È un buon strumento a due condizioni: ogni componente è normalizzato in modo esplicito, e la combinazione non nasconde il guasto grave. Il listato confronta una media pesata con il minimo dei componenti. Soglie e pesi sono scelte del team: i valori nel codice sono esempi, da validare confrontando il punteggio con gli abbandoni reali.
def lin(x, good, bad):
"""Punteggio 100 se x <= good, 0 se x >= bad, lineare in mezzo (vale anche se good > bad: allora più alto è meglio)."""
if good == bad: return 100.0
return max(0.0, min(100.0, 100 * (x - bad) / (good - bad)))
def components(rtt_ms, tick_ratio, loss_pct):
return {
"rtt": lin(rtt_ms, good=40, bad=150), # soglie scelte dal team, da validare sul proprio gioco
"tick": lin(tick_ratio, good=0.95, bad=0.70),
"loss": lin(loss_pct, good=0, bad=2),
}
WEIGHTS = {"rtt": 0.45, "tick": 0.35, "loss": 0.20}
def pes_mean(c): return sum(WEIGHTS[k] * c[k] for k in c)
def pes_min(c): return min(c.values())
cases = {
"tutto bene": (35, 0.99, 0.0),
"rete mediocre": (95, 0.97, 0.5),
"server a 50% del tickrate": (35, 0.50, 0.0),
"rete e server al limite": (110, 0.80, 1.2),
}
print(f"{'caso':<28}{'media pesata':>14}{'minimo':>8}")
for name, args in cases.items():
c = components(*args)
print(f"{name:<28}{pes_mean(c):>14.1f}{pes_min(c):>8.1f}")
# Output:
# caso media pesata minimo
# tutto bene 100.0 100.0
# rete mediocre 72.5 50.0
# server a 50% del tickrate 65.0 0.0
# rete e server al limite 38.4 36.4
Il caso «server a metà del tickrate» è la lezione: con RTT e perdita perfetti, la media pesata dà 65,0, un voto che sembra «accettabile», mentre il tickrate è dimezzato e il minimo dei componenti dice 0. Una media è comoda per i trend ma può nascondere un componente rotto; un minimo (o una media con un tetto sul componente peggiore) lo mette in evidenza. Mostra sempre il punteggio insieme ai suoi tre componenti, e non pubblicare tabelle del tipo «punteggio 60-74 uguale 5-10% di abbandoni» senza averle misurate sul tuo gioco.
Quando non serve tutto questo
Uno studio con un solo server di gioco e poche centinaia di giocatori può partire da un log strutturato per partita con RTT e durata dei tick, e da uno script che riassume. Prometheus, tracce e burn rate pagano quando hai più servizi, più regioni e qualcuno di turno a cui un allarme sbagliato costa il sonno.
Domande frequenti
Posso usare il player_id come etichetta di una metrica Prometheus?
No: la documentazione sconsiglia etichette ad alta cardinalità come gli identificativi utente. Nell'esempio dell'articolo l'istogramma dell'RTT passa da 156 a 260.000 serie con 20.000 giocatori. Per il singolo giocatore usa log e tracce.
Come si sceglie un allarme per un SLO di latenza?
Con il burn rate su due finestre, come nel workbook SRE di Google: un consumo di 14,4 volte il ritmo sostenibile su un'ora e su cinque minuti per le pagine, e finestre lunghe per i ticket. Rileva meno rumore di una soglia fissa, ma con qualche minuto di ritardo.
Perché il p99 di Prometheus non coincide con quello vero?
Perché histogram_quantile interpola dentro il bucket: più i bucket sono radi, più la stima si discosta. Nell'esempio, 3 bucket danno 297 ms contro 232 ms veri. Metti un bucket esattamente sulla soglia dell'SLO per avere i conteggi esatti.
Che cosa sai fare adesso
Sai scegliere le etichette di una metrica contando le serie che producono, leggere il tick dalla coda dei tempi e dalla frazione di tick in ritardo e non dalla media, e posizionare i bucket sulla soglia dell'SLO. Sai impostare un allarme sul consumo del budget di errore, sai quanto ritardo di rilevamento comporta rispetto a una soglia fissa e perché la campionatura delle tracce a probabilità fissa perde gli errori. Restano da fissare, sui tuoi dati, le soglie, i pesi del punteggio e la distribuzione reale dei tempi di tick.
Fonti
- Prometheus, Metric and label naming - il divieto di etichette ad alta cardinalità come gli identificativi utente e le unità di base, consultata il 2026-11-12
- Prometheus, Histograms and summaries - l'interpolazione di histogram_quantile e i confini fissi per un SLO noto, consultata il 2026-11-12
- Google SRE Workbook, Alerting on SLOs - burn rate, finestre multiple e la tabella 14,4 / 6 / 1, consultata il 2026-11-12
- OpenTelemetry, Sampling - campionamento head e tail, consultata il 2026-11-12
I listati Python sono eseguiti con Python 3 su dati sintetici con seme fisso: i conteggi e le percentuali descrivono il modello, non un gioco reale. Lo schema Go e le regole YAML non sono stati eseguiti (Go e promtool non sono installati); il file YAML è stato solo letto con un parser.

Commenti
Caricamento commenti...
Accedi per commentare