PRODPiattaforma BaaS europea sovranaApri Dashboard →

Prestazioni · 10 lettura minima

Perché nessun garbage collector modifica la latenza p99

Affane Daylami · Fondateur · 2 luglio 2026

Torniamo al blog

p99 misura la richiesta più lenta su cento, quella che infrange il tuo SLA mentre la media rimane perfetta. Su un backend ad alto traffico, questa manciata di richieste lente ha spesso un'unica causa: il garbage collector, che interrompe l'intero programma per liberare memoria. Rust non ha un garbage collector. Ecco il meccanismo, con fonti di terze parti datate piuttosto che una figura di marketing.

Questo testo inglese è stato generato automaticamente dall'originale francese e non è stato ancora rivisto.
Questa pagina è stata tradotta automaticamente. Fa fede la versione inglese.

Questo articolo si basa esclusivamente su fonti datate di terze parti, mai su un codice Aurabase inventato. Non è inclusa alcuna latenza p99 specifica per il nostro backend. Scriviamo il core della nostra applicazione in Rust, senza garbage collector: un fatto verificabile direttamente nel codice, nel workspace Cargo e nei servizi axum. Tuttavia, non abbiamo ancora pubblicato una metodologia di benchmark p99 riproducibile per dimostrarlo in cifre. Questo testo spiega un meccanismo, non un risultato misurato.

L'essenziale

  • p99 misura la query più lenta su cento: il luogo in cui la pausa di un garbage collector (GC) fa più male, non in media (Dean & Barroso, "The Tail at Scale", Google, 2013).
  • Un GC interrompe l'intero programma ("stop-the-world") per liberare la memoria inutilizzata. Rust non ha GC: la memoria viene liberata nel momento preciso in cui un valore esce dall'ambito, verificato dal compilatore.
  • Discord ha documentato un servizio di cache nel 2020 in cui Go attivava un ciclo di garbage collection almeno ogni due minuti, con ogni ciclo che causava un picco di latenza (Discord Engineering Blog).
  • Ridurre una pausa GC richiede anni di ingegneria, anche in Google: il collettore GB è passato da 300-400 ms a 500 µs tra il 2015 e il 2018, senza mai raggiungere lo zero (go.dev).
  • Il nucleo del backend di Aurabase è scritto in Rust, senza garbage collector, verificato nel codice. Ad oggi non sono stati pubblicati dati sulla latenza di p99 Aurabase: questo rimane un meccanismo, non una misurazione.
#
Latenza della coda

Perché p99 non è nella media

La media nasconde l’essenziale. Se 99 richieste su 100 rispondono in 5 ms e solo una impiega 500 ms, la media rimane bassa. Ma un utente su cento sperimenta un’attesa cento volte più lunga. Il p99 misura esattamente questa richiesta: il centesimo più lento, quello che viola il tuo SLA mentre la dashboard della latenza media rimane verde.

Presso Google, Jeffrey Dean e Luiz André Barroso hanno formalizzato questo problema in “The Tail at Scale” (Communications of the ACM, vol. 56, 2013). La loro osservazione, spesso citata da allora: “Episodi temporanei di latenza elevata che non sono importanti in sistemi di dimensioni moderate possono arrivare a dominare le prestazioni complessive del servizio su larga scala”. In breve: episodi occasionali di latenza, trascurabili su piccola scala, finiscono per dominare la performance percepita di un sistema distribuito.

Un backend che elabora migliaia di richieste al secondo è destinato a inviare, prima o poi, una richiesta che cade durante una pausa del GC. Su larga scala, questo non è un caso raro. Questa è una certezza statistica.

#
Meccanica GC

Cosa fa un garbage collector e perché mette in pausa tutto

Un garbage collector (GC) tiene traccia continuamente degli oggetti viventi di un programma – quelli ancora referenziati da qualche parte – e libera la memoria dagli oggetti che sono diventati inaccessibili. Questo tracciamento si chiama tracing: il GC attraversa il grafico di riferimento, contrassegna ciò che è ancora utilizzato, quindi spazza il resto.

Il problema: attraversare questo grafico mentre il programma continua a creare nuovi riferimenti produce risultati incoerenti. La risposta storica, ancora utilizzata come ultima risorsa da molti GC moderni, è stop-the-world: l'intero programma viene messo in pausa durante la marcatura e la scansione. Più grande è l'heap, più lunga tende ad essere la pausa: la sua durata dipende dalla dimensione dei dati in tempo reale, non dal carico di lavoro corrente.

La maggior parte dei GC moderni utilizza una strategia generazionale: presuppone che la maggior parte degli oggetti muoia giovane. Le allocazioni recenti vengono quindi scansionate spesso, ma rapidamente, in una piccola area di memoria. Gli oggetti che sopravvivono a diversi cicli migrano in un'area più ampia, scansionata raramente, ma quando quell'area deve essere pulita, la pausa associata aumenta con le sue dimensioni. È questa interruzione “maggiore”, non le piccole interruzioni “minori”, che domina la pagina 99 di un servizio ad alto traffico e ad alta allocazione.

Informazioni

I moderni GC simultanei e generazionali riducono la frequenza e la durata di queste pause lavorando in parallelo con il programma. Ma quasi tutti mantengono un meccanismo di riserva “ferma il mondo” per i casi limite – e per ridurlo ci vogliono anni di ingegneria. La sezione 04 fornisce un esempio quantificato e fornito di fonti.

#
Caso reale

Discord, 2020: una pausa nella classifica generale diventa un incidente di produzione

Nel febbraio 2020, l'ingegnere Jesse Howarth ha pubblicato un post che è diventato un riferimento nel settore: “Perché Discord sta passando da Go a Rust” (Discord Engineering Blog). Il servizio pertinente, Read States, gestisce lo stato di lettura dei messaggi per milioni di utenti (decine di milioni di voci per cache) con centinaia di migliaia di aggiornamenti al secondo.

La diagnosi è diretta, citata così nell'articolo: “Go imporrà un'esecuzione della garbage collection almeno ogni 2 minuti”. In altre parole, Go attiva un ciclo di garbage collection almeno ogni due minuti su questo servizio e ogni ciclo produce un picco di latenza visibile nei grafici del team.

Il team ha innanzitutto ridotto la dimensione della cache per attenuare i picchi. Il compromesso è rimasto sfavorevole: meno pause GC, ma più richieste mancate nella cache che cadono sul database, quindi un p99 complessivo degradato altrove. La soluzione di base è stata la riscrittura del servizio in Rust, senza un garbage collector da monitorare.

Il post ha scatenato un vivace dibattito tecnico: più di 1.580 punti e 642 commenti su Hacker News nello stesso giorno della sua pubblicazione (4 febbraio 2020), segno che il problema va ben oltre il caso Discord.

#
Vai alla Storia

Tre anni di ingegneria presso Google per ridurre una pausa da 400 ms a 500 µs

Il garbage collector Go illustra la portata dello sforzo richiesto per domare una pausa GC, anche con le risorse di un team dedicato di Google. Rick Hudson, responsabile tecnico di Go GC, ha documentato questa storia in due post ufficiali del blog Go.

Prima di agosto 2015300-400 msStorico collezionista Go, prima della riprogettazione
Agosto 2015 · Vai 1.530-40 msPrimo collezionista concorrente, target < 10 ms impostato
2016 · Vai 1.6< 10 ms (SLO mantenuto)Obiettivo iniziale raggiunto in produzione
Marzo 2017 · Vai 1.8sotto il millisecondoRimozione della scansione dello stack Stop-the-world
Agosto 2017 · Vai 1.9100-200 µs (segno)Nuovo benchmark informale menzionato dal team
2018 · Annunciato SLO500 µs per cicloObiettivo di servizio formalizzato da Rick Hudson

Fonte: "Getting to Go: The Journey of Go's Garbage Collector", go.dev, 12 luglio 2018; e "Go GC: priorità alla bassa latenza e alla semplicità", go.dev, 31 agosto 2015.

Tre anni di lavoro dedicato hanno ridotto di mille volte la tipica pausa. Ma la pausa non è mai scomparsa: si tratta di un obiettivo di servizio (SLO), non di una garanzia assoluta di zero. Un tracciato GC deve, per costruzione, attraversare di volta in volta un grafico di oggetti viventi. L’unica variabile regolabile è la frequenza e la durata di questo viaggio, non la sua esistenza.

Questa scelta di priorità non è neutrale. Go si rivolge principalmente ai servizi di rete e ai backend web, dove una pausa di diverse centinaia di millisecondi interrompe direttamente l'esperienza dell'utente, da qui l'enorme sforzo investito nella latenza piuttosto che nel throughput GC grezzo. Altri runtime gestiti hanno ereditato diversi compromessi, modellati dai loro casi d'uso storici, prima di recuperare terreno con i propri raccoglitori a pausa bassa. Il punto in comune resta lo stesso: partono tutti dal tracciato GC, quindi da un meccanismo di pausa da ridurre al minimo – da non eliminare mai dalla costruzione.

#
Meccanica della ruggine

Perché Rust non ha questo problema di costruzione

La ruggine non riduce le pause GC: elimina il meccanismo che le provoca. Il compilatore tiene traccia, al momento della compilazione, di chi possiede ciascun valore di memoria: questo èownership. Quando il proprietario di un valore esce dall'ambito, Rust inserisce automaticamente la chiamata che libera quella memoria, nello stesso posto nel codice binario. Questo meccanismo è chiamato RAII (Resource Acquisition Is Inizializzazione): il rilascio è deterministico, non pianificato da un garbage collector in esecuzione in background.

Alexandru Nedelcu, autore di un blog tecnico riconosciuto nell'ecosistema Scala/Rust, riassume il compromesso in un recente articolo: "Il compromesso che Rust fa è quello della facilità d'uso, preferendo prestazioni con latenza prevedibile e sicurezza" (alexn.org, 21 luglio 2026). Rust scambia parte della semplicità della scrittura con una latenza prevedibile.

Lo stesso articolo riassume il motivo per cui i GC moderni non sono sempre sufficienti: "I GC moderni cercano di svolgere il loro lavoro in modo incrementale e concorrente, senza influenzare il programma. Ma la loro capacità è limitata, ricadendo in un ciclo GC stop-the-world che congela l'intero programma, influenzando così la latenza".

Ecco il meccanismo in una decina di righe – un esempio generico, non un estratto dal codice Aurabase:

example.rsrust
struct Connection {
    id: u32,
}

impl Drop for Connection {
    fn drop(&mut self) {
        println!("connexion {} fermée", self.id);
    }
}

fn handle_request() {
    let conn = Connection { id: 42 };
    // ...elabora la richiesta...
} // conn esce dall'ambito qui: drop() viene eseguito
   // nel momento preciso, controllato dal compilatore —
   // non attraverso un ciclo di raccolta dei rifiuti.

Sfumatura importante: non tutto è gratuito. I tipi con conteggio dei riferimenti (Rc, Arc) aggiungono un piccolo costo a ciascun clone e rilascio. Questo costo rimane locale e deterministico. Non c'è mai una pausa che congela l'intero programma mentre attraversa un heap di memoria.

Chiarimento utile per un backend asincrono: il runtime asincrono di Rust (tokio, utilizzato da tutti i servizi Aurabase) non ha nulla a che fare con un garbage collector. Pianifica attività cooperative su un pool di thread, ma non esegue mai l'iterazione attraverso un grafico di oggetti live per liberare memoria. La confusione è comune negli ecosistemi in cui il runtime asincrono e il GC sono gestiti dalla stessa macchina virtuale.

#
Implicazione architettonica

Cosa cambia per un backend ad alto traffico

Su un backend che serve migliaia di richieste simultanee, l'assenza di GC rimuove una variabile dall'equazione p99. Non è più necessario dimensionare un heap di memoria, regolare le generazioni di un raccoglitore o monitorare un ciclo che può interrompersi nel momento peggiore. La latenza di una richiesta individuale dipende dal suo stesso lavoro, non da qualche evento globale imprevedibile altrove nel programma.

Il core backend di Aurabase applica questo principio: tutti i servizi (aura-gateway, aura-auth, aura-db, aura-realtime, aura-storage, aura-functions, aura-ai…) sono scritti in Rust, organizzati in un unico Cargo workspace. Questo può essere verificato direttamente nel repository:

Cargo.tomltoml
[workspace]
resolver = "2"

members = [
    "services/aura-gateway",
    "services/aura-auth",
    "services/aura-db",
    # ...9 altri servizi + librerie condivise
]

[workspace.dependencies]
axum = { version = "0.8", features = ["ws", "multipart", "macros"] }

Estratto effettivo da Cargo.toml, edizione workspace 2021, risolutore v2 — verificato nel repository Aurabase.

Ciò che questo fatto non dimostra, in questa fase: una cifra di latenza p99 misurata per Aurabase. Non abbiamo ancora pubblicato una metodologia di benchmark riproducibile per il nostro backend: si tratta di un lavoro in corso, non di un risultato disponibile oggi. L'assenza di un garbage collector è un meccanismo verificato nel codice. Questo da solo non è una prova della latenza p99 misurata. Tieni presente questa distinzione di fronte a qualsiasi argomento di marketing sull'argomento, incluso il nostro: consulta il nostro confronto tecnico Aurabase vs Supabase per i dettagli dell'architettura.

Misurare correttamente un p99 richiede una propria disciplina: condizioni di carico rappresentative, percentili calcolati su una finestra scorrevole sufficientemente ampia e un ambiente di prova vicino alla produzione. Pubblicare una figura senza questa metodologia è come pubblicare una figura di marketing. Questo è esattamente ciò che ci rifiutiamo di fare in questo articolo.

#
Sfumatura

Ciò che l’assenza di GC non risolve

No-GC non è una bacchetta magica

La rimozione del garbage collector elimina solo una fonte di latenza della coda, non tutta. Un backend Rust può ancora mostrare un p99 degradato a causa dell'attesa della rete, di un pool di connessioni Postgres saturo, di un blocco del database contestato, di una query SQL scarsamente indicizzata o di una chiamata API di terze parti lenta. Il meccanismo descritto in questo articolo rimuove una causa strutturale. Non fornisce immunità contro gli altri.

In Aurabase, ad esempio, ogni servizio comunica con Postgres tramite un pool di connessioni (sqlx) e con altri servizi tramite NATS JetStream. Un pool sottodimensionato, un abbonamento NATS lento da consumare o una query SQL senza un indice adeguato producono ciascuno il proprio picco di latenza, indipendentemente dall'assenza di un garbage collector.

La conclusione pratica: l'assenza di GC è una buona ragione architetturale per scegliere un backend Rust per un sistema compatibile con p99. Questo non è, di per sé, una garanzia di latenza – né in Aurabase, né altrove. Il metodo che conta rimane lo stesso: misurare, pubblicare la metodologia, quindi correggere ciò che le misurazioni rivelano. Se stai migrando da un backend con GC, la nostra guida alla migrazione da Supabase ad Aurabase descrive dettagliatamente cosa cambia e cosa rimane invariato.

#
Domande frequenti

FAQ: Garbage Collector e latenza p99

L'assenza di garbage collector garantisce p99 bassi?+
No. Rimuove una fonte strutturale di pause imprevedibili, ma anche altri fattori (rete, pool di connessioni, blocchi del database) influenzano la latenza della coda. Consulta la sezione "Ciò che nessun GC non risolve" sopra.
Perché Discord non ha semplicemente messo a punto il suo GC Go in modo diverso?+
Il team ha provato diverse modifiche, inclusa una riduzione delle dimensioni della cache, documentata nel post del 2020. Il compromesso è rimasto sfavorevole: meno pause GC rispetto a più cache miss. La riscrittura in Rust ha rimosso il compromesso invece di spostarlo.
I GC moderni come Go hanno risolto il problema?+
L’hanno ridotto enormemente – il GC di Go è passato da 300-400 ms a 500 microsecondi tra il 2015 e il 2018 (go.dev) – ma non lo hanno eliminato. Un tracciato GC mantiene, per costruzione, un meccanismo di pausa per i casi limite.
Aurabase ha rilasciato un benchmark sulla latenza p99?+
Non ancora. Il fatto verificato nel codice è l'assenza del garbage collector (Rust, workspace Cargo, axum services). La cifra misurata sulla latenza p99 e la sua metodologia riproducibile devono ancora essere pubblicate: preferiamo nessuna cifra a una cifra senza fonte.

PRONTO PER L'IMPLEMENTAZIONE?

Il tuo backend in cinque minuti.

Nessuna carta di credito richiesta · 500 MB gratuiti · 50.000 MAU