⏱️ Lectura: 14 min
Cloudflare acaba de liberar unos 100 terabytes de memoria en su flota global reescribiendo, campo por campo, las estructuras de datos que sostienen el caché DNS de 1.1.1.1. El equipo detrás de Big Pineapple, la plataforma interna que además opera Gateway DNS, DNS Firewall y AS112, aplicó cinco cambios sucesivos en Rust que redujeron el peso de cada entrada de caché en más del 50%.
📑 En este artículo
El resultado no fue solo espacio liberado. El throughput de inserción subió 43% y la latencia de lectura bajó 19%, según el post técnico publicado por Cloudflare. Achicar los structs, en este caso, también los hizo más rápidos.
TL;DR
- Cloudflare liberó unos 100 terabytes de memoria en su flota optimizando el caché DNS de Big Pineapple, la plataforma detrás de 1.1.1.1.
- Big Pineapple mantiene más de 250.000 millones de entradas de caché DNS en simultáneo en toda la infraestructura de Cloudflare.
- Cinco cambios sucesivos en las estructuras de datos en Rust redujeron el peso por entrada de caché en más del 50%.
- Cambiar Vec y String por Box<[T]> y Box<str> eliminó el campo de capacidad sobrante, ahorrando más de 15 terabytes en la flota.
- Fusionar tres listas de registros en una con offsets de 2 bytes en vez de punteros de 8 bytes ahorró 28 bytes por entrada.
- El insert throughput del caché subió 43% y la latencia de lookup bajó 19% tras los cinco cambios.
- El ahorro total equivale a la RAM de 130 servidores Gen 13 de Cloudflare.
- Los data centers con EDNS Client Subnet (ECS) activo se benefician más, porque cachean varias versiones de una misma consulta.
Introducción
1.1.1.1 es uno de los resolutores DNS públicos más usados del mundo, y detrás de cada consulta hay una capa de caché que decide si Cloudflare responde en microsegundos o tiene que preguntarle a un servidor autoritativo. Esa capa se llama Big Pineapple y, según Cloudflare, mantiene en todo momento más de 250.000 millones de entradas de caché DNS repartidas en su red. A esa escala, malgastar un solo byte por entrada cuesta más de 250 gigabytes de memoria en el conjunto de la flota.
Ese fue el punto de partida del equipo de ingeniería: auditar byte por byte las estructuras que representan cada entrada de caché DNS y eliminar todo lo que no aportara valor una vez que el dato ya estaba guardado.
Qué pasó con el caché DNS de 1.1.1.1
Cada entrada del caché es un par clave-valor. La clave identifica qué se consultó: el nombre de dominio, el tipo de registro, si la respuesta está autenticada con DNSSEC y una etiqueta opcional para variantes de EDNS Client Subnet (ECS). El valor guarda la respuesta completa: las secciones de respuesta (answer), autoridad (authority) y adicional (additional), más metadatos como la marca de tiempo, un contador de aciertos (hits) y el TTL.
El equipo detectó que varios de esos campos usaban tipos pensados para estructuras que crecen y cambian, como Vec<T> y String, cuando en realidad una entrada de caché DNS, una vez insertada, nunca vuelve a modificarse. Ese desajuste entre el tipo elegido y el uso real es exactamente el tipo de desperdicio que se puede medir y corregir.
Contexto e historia
Para medir el impacto de cada cambio sin depender de tráfico real, Cloudflare construyó un benchmark que llena el caché con entradas generadas aleatoriamente en una proporción parecida a la de producción: 56% registros A, 25% AAAA y 19% TXT, con entre uno y cuatro registros por entrada. Los TXT funcionan como representantes de todos los tipos de registro distintos a A/AAAA, con un tamaño aleatorio de entre 64 y 224 bytes.
Para contar bytes reales, no estimaciones, usaron un allocator propio que envuelve al System allocator de Rust y registra la cantidad y el tamaño de cada allocation por entrada de caché. Esa métrica se combinó con medición de resident memory en instancias de producción durante el rollout, porque el tráfico real depende también de la mezcla de consultas, la ocupación del caché y el estado del allocator, variables que un benchmark sintético solo aproxima.
Un factor que agrava el problema es EDNS Client Subnet (ECS): cuando está activo, los servidores autoritativos devuelven respuestas distintas según la subred del cliente, así que Big Pineapple termina cacheando varias versiones de la misma consulta. Eso multiplica tanto el número de entradas como la memoria por entrada, lo que hace que estas optimizaciones pesen todavía más en las ubicaciones con mucho tráfico ECS.
Detalles técnicos y rendimiento
El primer cambio, y el que más memoria liberó, fue reemplazar Vec<T> y String por Box<[T]> y Box<str>. Un Vec<T> guarda tres campos: un puntero al heap, la longitud actual y la capacidad reservada. Esa capacidad solo sirve para poder crecer sin reasignar memoria, algo que una entrada de caché DNS inmutable jamás necesita. Box<[T]> no puede crecer después de creado, así que no necesita capacidad ni reserva espacio de más: es un puntero fat de solo puntero y longitud.
use std::mem::size_of;
struct EntradaConVec {
respuestas: Vec<u8>,
ttl: u32,
}
struct EntradaConBox {
respuestas: Box<[u8]>,
ttl: u32,
}
fn main() {
println!("Vec<u8>: {} bytes", size_of::<EntradaConVec>());
println!("Box<[u8]>: {} bytes", size_of::<EntradaConBox>());
}
Corriendo ese ejemplo con cargo run se ve la diferencia: el campo Vec<u8> ocupa 24 bytes en un sistema de 64 bits (puntero, longitud y capacidad, 8 bytes cada uno), mientras que Box<[u8]> ocupa 16 (puntero y longitud). Cada struct de Big Pineapple tenía ocho campos de tipo Vec o String, así que el cambio ahorró 64 bytes por entrada de caché, más el espacio de heap que un Vec sobre-reserva para crecer. Multiplicado por 250.000 millones de entradas, la cuenta supera los 15 terabytes.
El segundo cambio atacó las tres listas de registros (answer, authority, additional). En vez de guardar tres Box<[T]> independientes, cada una con su puntero de 8 bytes y su longitud de 8 bytes, Cloudflare pasó a guardar una sola lista con offsets de 2 bytes (u16) que marcan dónde empieza cada sección. Como la cantidad de registros por sección siempre entra en un u16, dos offsets de 2 bytes reemplazan a dos listas completas de 16 bytes cada una, ahorrando 28 bytes por entrada.
El tercer cambio fue empaquetar varios campos booleanos sueltos en un solo bitflag. En Rust, el compilador agrega padding para respetar el alineamiento de cada campo y redondea el tamaño total del struct a un múltiplo de ese alineamiento. Sacar un campo booleano chico puede eliminar padding adicional, así que el struct se achica más de lo que suma el tamaño de los booleanos individuales.
| Cambio | Antes | Después | Ahorro estimado |
|---|---|---|---|
| Vec/String a Box | puntero + longitud + capacidad (24 bytes) | puntero + longitud (16 bytes) | 64 bytes por entrada, más de 15 TB en la flota |
| 3 listas de registros a 1 lista con offsets | 3 x (puntero 8B + longitud 8B) = 48 bytes | 1 puntero + longitud + 2 offsets u16 = 20 bytes | 28 bytes por entrada |
| Booleanos sueltos a bitflag | varios campos de 1 byte más padding | 1 solo byte | variable, reduce padding del struct |
| Owner duplicado a owner compartido | el nombre del dominio se repite en cada registro | referencia compartida al nombre consultado | variable según cantidad de registros por entrada |
| Total de los cinco cambios | tamaño original por entrada | más del 50% más chico | ~100 TB liberados en toda la flota |
Un quinto cambio, complementario a los anteriores, aprovechó que muchos registros de DNS comparten owner (el dominio dueño del registro) con la consulta original. Una consulta a example.com A devuelve registros cuyo owner también es example.com, así que en vez de guardar ese nombre una vez por registro, Big Pineapple lo referencia una sola vez y lo comparte entre todos los registros de la entrada.
La suma de los cinco cambios achicó cada entrada de caché DNS en más del 50%. En una flota que sostiene 250.000 millones de entradas, eso se traduce en unos 100 terabytes liberados, el equivalente a la memoria RAM de 130 servidores Gen 13 de Cloudflare. Y como hay menos allocations por inserción y mejor localidad de memoria, el insert throughput subió 43% y la lookup latency bajó 19%: la optimización de memoria no le restó nada a la velocidad.
Cómo empezar a aplicarlo
No hace falta operar un resolutor DNS a escala de Cloudflare para aplicar el mismo patrón. Si mantenés un caché en Rust (o cualquier lenguaje con tipos de colección redimensionables), el primer paso es preguntarte si esa colección se vuelve a modificar después de insertada. Si la respuesta es no, un tipo inmutable como Box<[T]> casi siempre gana.
Un caso realista, con el bitflag y el Box juntos:
use bitflags::bitflags;
bitflags! {
struct EntradaFlags: u8 {
const AUTENTICADO = 0b0000_0001;
const TIENE_ERRORES = 0b0000_0010;
const ES_NEGATIVA = 0b0000_0100;
}
}
struct CacheEntryOptimizada {
ttl: u32,
hits: u32,
flags: EntradaFlags,
respuestas: Box<[u8]>,
}
Para usar bitflags agregá la dependencia en tu Cargo.toml:
[dependencies]
bitflags = "2"
Para verificar el ahorro en tu propio proyecto no hace falta adivinar: std::mem::size_of::<T>() te dice el tamaño exacto del struct antes y después del cambio, y un profiler de heap como valgrind --tool=massif o el crate dhat-rs te muestra cuántas allocations y cuántos bytes reales consume tu caché bajo carga. Correlo antes y después de cada cambio, no solo al final: así identificás qué optimización aportó qué porcentaje, igual que hizo Cloudflare con sus cinco cambios sucesivos.
💡 Tip: Medí con std::mem::size_of::<T>() antes de tocar nada. Es gratis, no requiere benchmark y te dice de entrada si hay bytes desperdiciados en el layout del struct.
Si querés observar el comportamiento de ECS que menciona Cloudflare, podés simular una consulta con subred explícita contra 1.1.1.1:
# Linux / macOS / WSL
dig @1.1.1.1 example.com A +subnet=203.0.113.0/24
# Windows (PowerShell, con dig instalado via winget o BIND tools)
dig @1.1.1.1 example.com A +subnet=203.0.113.0/24
Cada subred distinta que uses puede generar una entrada de caché adicional del lado del resolutor, que es justamente el escenario que hace más pesados a los data centers con mucho tráfico ECS.
Impacto y análisis
El caso de Big Pineapple es un recordatorio de que optimizar memoria y optimizar velocidad no son objetivos en tensión cuando el problema es un layout de datos ineficiente. Menos bytes por entrada significa menos allocations, mejor localidad de caché de CPU y, en este caso, un caché DNS que responde más rápido con menos hardware. Para cualquier equipo que opere un caché de alta cardinalidad (un proxy de Redis, un edge cache de CDN, un gateway de API) el patrón es transferible: auditar structs inmutables, sacar capacidad sobrante, fusionar listas paralelas y empaquetar booleanos.
⚠️ Ojo: Box<[T]> no admite crecer después de creado. Si tu struct necesita un campo mutable, como el contador de hits que sí cambia en cada acierto de caché, ese campo tiene que quedar fuera del Box o usar un tipo con mutabilidad interior (Cell o AtomicU32), no podés simplemente envolver todo el struct.
El siguiente diagrama resume dónde entra la entrada optimizada en el flujo de una consulta DNS contra 1.1.1.1:
sequenceDiagram
participant C as Cliente
participant BP as Big Pineapple
participant A as Servidor autoritativo
C->>BP: consulta DNS (example.com A)
BP->>BP: busca CacheKey en el cache DNS
alt cache hit
BP-->>C: responde desde CacheEntry optimizada
else cache miss
BP->>A: reenvia la consulta
A-->>BP: respuesta con TTL
BP->>BP: inserta CacheEntry optimizada
BP-->>C: responde al cliente
end
La limitación honesta de este enfoque es que no todos los campos de un caché se prestan a la inmutabilidad. El contador de hits, por ejemplo, cambia en cada acierto, así que no puede vivir dentro de un Box<[T]> congelado: tiene que quedar en un campo aparte, mutable. La optimización agresiva de memoria solo funciona si primero separás con claridad qué datos son de solo lectura una vez insertados y cuáles siguen cambiando.
Qué sigue
Cloudflare aclara que las cifras del benchmark aproximan, pero no reproducen exactamente, el comportamiento en producción: la memoria de proceso también depende de la mezcla de tráfico, la ocupación del caché y el estado del allocator. Por eso midieron memoria resident en instancias reales durante el rollout, además del benchmark sintético. Es razonable esperar que este tipo de auditoría de layout de datos se repita en otras plataformas de Cloudflare que manejan volúmenes similares de entradas en memoria, no solo en el caché DNS.
📖 Resumen en Telegram: Ver resumen
Probalo vos: corré cargo run --release con los dos structs de ejemplo de este artículo y compará el resultado de size_of antes de decidir si tu propio caché tiene bytes para recuperar.
Preguntas frecuentes
¿Qué es Big Pineapple?
Es la plataforma interna de Cloudflare que sostiene 1.1.1.1, Gateway DNS, DNS Firewall, AS112 y otros servicios de DNS de la compañía. Mantiene el caché DNS que responde la mayoría de las consultas sin tener que reenviarlas a un servidor autoritativo.
¿Por qué Vec<T> ocupa más memoria que Box<[T]>?
Porque Vec<T> reserva un campo extra de capacidad para poder crecer sin reasignar memoria, y además suele reservar espacio de heap de más para futuras inserciones. Box<[T]> no puede crecer después de creado, así que no necesita ninguna de las dos cosas.
¿Estos cambios afectaron la velocidad, o solo la memoria?
Afectaron ambas cosas. Según Cloudflare, el insert throughput del caché subió 43% y la lookup latency bajó 19%, porque hay menos allocations por entrada y mejor localidad de memoria.
¿Qué es EDNS Client Subnet y por qué importa acá?
Es una extensión de DNS que le permite a un servidor autoritativo devolver respuestas distintas según la subred del cliente. Cuando está activo, Big Pineapple cachea varias versiones de la misma consulta, lo que aumenta tanto el número de entradas como la memoria por entrada.
¿Puedo aplicar esta misma optimización en mi propio caché en Rust?
Sí. El patrón (reemplazar Vec/String por Box cuando el dato ya no cambia, fusionar listas paralelas con offsets y empaquetar booleanos en un bitflag) es aplicable a cualquier struct inmutable de alta cardinalidad, no solo a un caché DNS.
¿Dónde está el post técnico original de Cloudflare?
En el blog oficial de Cloudflare, con el detalle completo de los cinco cambios y la metodología de benchmark usada para medir cada uno.
Referencias
- Cloudflare Blog: post técnico original sobre la optimización del caché DNS de 1.1.1.1.
- Rust std docs, Box: documentación oficial del tipo Box y sus garantías de layout.
- Rust std docs, Vec: documentación oficial de Vec, incluyendo el campo de capacidad.
- RFC 7871: especificación del mecanismo EDNS Client Subnet (ECS).
- docs.rs, bitflags: documentación del crate usado para empaquetar campos booleanos.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
0 Comentarios