Programmazione e strumenti dev

Come Cloudflare ha risparmiato 100 terabyte di RAM nella cache DNS di 1.1.1.1

Con 250 miliardi di voci in cache contemporaneamente, sprecare un solo byte per voce costa 250 gigabyte sull'intera infrastruttura. Ecco come cinque modifiche mirate alle strutture dati hanno dimezzato il costo in memoria, senza rallentare nulla — anzi.

Il problema: quando "un byte in più" moltiplicato per miliardi

Big Pineapple, la piattaforma dietro 1.1.1.1 e gli altri servizi DNS di Cloudflare, mantiene in cache oltre 250 miliardi di voci in un dato momento. È una scala in cui il ragionamento normale su "quanto pesa una struct" smette di essere un dettaglio accademico: un campo ridondante da 8 byte, moltiplicato per 250 miliardi di voci, diventa 2 terabyte buttati via per niente. Il team ha guardato da vicino due struct — la chiave di cache e il valore associato — e ha trovato margine ovunque.

Il risultato finale, dopo cinque modifiche successive, è stato un taglio di oltre il 50% della memoria per singola voce, per un risparmio complessivo stimato in circa 100 terabyte su tutta la flotta di server — l'equivalente della RAM di 130 macchine intere. E la parte controintuitiva: la cache è diventata anche più veloce, non più lenta, con un aumento del 43% nel throughput di inserimento e una riduzione del 19% nella latenza di lettura.

Il costo nascosto di Vec e String

Il primo intervento riguarda un dettaglio che chiunque scriva Rust (o conosca come funzionano gli array dinamici in qualsiasi linguaggio) dà per scontato: un Vec<T> non memorizza solo un puntatore e una lunghezza, ma anche una capacità — lo spazio riservato per crescere in futuro senza dover riallocare. Quel campo costa 8 byte anche quando non serve più a nulla.

// Una volta salvata in cache, una risposta DNS non cambia più:
// il campo "capacity" di Vec non serve a nessuno da quel momento in poi.
pub struct CacheEntry {
    answers: Vec<Record>,   // spreca capacità inutilizzata
    authority: Vec<Record>,
    additional: Vec<Record>,
}

// Box<[T]> non può crescere dopo la creazione:
// niente campo capacity, niente spazio riservato per il futuro.
pub struct CacheEntryOttimizzata {
    answers: Box<[Record]>,
    authority: Box<[Record]>,
    additional: Box<[Record]>,
}

La stessa logica si applica a String, che porta con sé lo stesso campo di capacità: convertirla in Box<str> quando sai che il contenuto non cambierà più elimina lo stesso spreco. Non è un trucco specifico del DNS: qualsiasi collezione che costruisci una volta e poi leggi soltanto — una configurazione caricata all'avvio, una lista di risultati già calcolata, i dati letti da un file — porta lo stesso peso morto se resta un Vec o una String invece di diventare immutabile.

Misurare prima di ottimizzare, sempre

La parte del lavoro che vale la pena copiare, più ancora della soluzione tecnica specifica, è il metodo. Cloudflare non ha ottimizzato "a sensazione": ha costruito un allocator personalizzato che avvolge l'allocator di sistema di Rust e registra numero e dimensione delle allocazioni per ogni voce di cache, poi ha riempito la cache con dati generati casualmente ma calibrati sulla distribuzione reale del traffico (56% record A, 25% AAAA, 19% TXT). Solo dopo aver misurato in modo ripetibile ha iniziato a cambiare le struct, verificando ogni volta memoria, throughput di inserimento e latenza di lettura insieme — proprio per accorgersi subito se un risparmio di memoria avesse un costo nascosto in velocità.

Per chi scrive software che deve reggere carico — non necessariamente alla scala di un resolver DNS globale, ma anche solo un servizio con una cache in memoria o una struttura dati che vive per tutta la durata del processo — la lezione pratica è duplice: primo, chiediti se ogni campo di ogni struct che tieni in memoria serve davvero dopo la creazione, o è overhead ereditato da un tipo generico pensato per un uso più flessibile di quello che ti serve. Secondo, non fidarti dell'intuizione su cosa "dovrebbe" pesare meno: misura con uno strumento che conta allocazioni reali, prima e dopo, altrimenti stai solo indovinando.

Fonte: How we saved 100 terabytes of memory by optimizing 1.1.1.1's DNS cache, sul blog di Cloudflare