
La última vez que Cloudflare publicó que había liberado 100TB de RAM pensé que era ruido de marketing. Ahora volvieron a hacerlo, en otro servicio distinto, y esta vez contaron la receta completa: matemática de probabilidad, un struct de Rust mal empaquetado y paciencia para migrar sin tirar la caché global a la basura. Como administrador de VPS que pelea por cada giga, esta historia me parece de las más útiles que han publicado en meses.
El problema: 160 puntos por servidor heredados de NGINX
El servicio afectado es un balanceador interno basado en Pingora que usa hashing consistente para decidir a qué servidor mandar cada petición cacheable. Para que la carga reparta parejo, cada servidor no tiene un punto en el anillo de hashes, sino 160 (el valor que NGINX heredó de siempre y Pingora copió por defecto). Con cada combinación de features y restricciones de cumplimiento que necesitaba un anillo propio, la memoria se fue acumulando silenciosamente hasta ser notoria.
El primer hallazgo no fue glamoroso: un struct de dos campos, el hash de 32 bits y el índice del servidor, también de 32 bits. Pero nunca iban a coordinar más de 65.000 servidores a la vez, así que el índice cabía en 16 bits. Y acá viene el detalle que me gusta: cambiar el tipo a u16 no reduce nada, porque las reglas de alineación de Rust redondean el tamaño del struct a múltiplo del campo más grande. La solución fue empaquetar los 6 bytes en un array crudo y exponerlos con getters. Feo, menos legible, pero 25% menos memoria solo en esa estructura. A veces la ingeniería es exactamente eso: elegir entre elegante y barato.
La matemática que nadie quería derivar
El segundo golpe vino del lado estadístico. La fórmula del error del anillo solo estaba derivada para un hash por servidor; para k hashes la mayoría de las fuentes da aproximaciones. Uno de los ingenieros derivó la fórmula exacta y con eso se dieron cuenta de algo contraintuitivo: con hashes de 32 bits, las colisiones (el clásico problema del cumpleaños) hacían que generar más de 100.000 hashes por servidor no comprara precisión real. Entonces pudieron bajar el número de hashes por servidor en 90% sin error apreciable. No optimizaron el código: cuestionaron una constante que llevaba años puesta porque NGINX la puso.
La parte que más respeto: la migración
Cambiar el anillo cambia dónde vive cada objeto en caché. Un flip global habría invalidado casi toda la caché de la red y convertido una optimización de memoria en un diluvio de tráfico a los orígenes. Lo que hicieron fue correr los dos anillos en paralelo, decidir por petición cuál usar, y hacer la migración por capas de datacenters, controlando por separado cuánto tráfico migraba y desde dónde. Con contadores, trazas y la opción de rollback sin redeploy. Esa disciplina operacional es la diferencia entre un post de blog y un incidente.
Lo que me llevo yo
Mi lección de esta pega: las constantes heredadas son deuda técnica invisible. 160 hashes por servidor sonaba a detalle de implementación, pero multiplicado por miles de servidores y decenas de anillos eran 100 teras. En mi escala, que es una microscópica fracción de la de ellos, me pasa lo mismo con límites de memoria de PHP o workers de Caddy que copié de un tutorial hace años y nunca medí. No es que vaya a ahorrar 100TB, pero la pregunta es la misma: ¿esta constante la elegí yo o la heredé?
Y una nota para los que optimizan en Rust: la alineación no perdona. Si tu struct tiene un u32 y un u16, ocupa 8 bytes igual. Antes de recurrir a #[repr(packed)] con sus riesgos, un array de bytes con getters compila a lo mismo. La matemática es universal, dice el post original, y el empaquetado de memoria también.
Fuente de inspiración: Saving another 100TB of RAM with math (and Rust)