Cloudflare Just Saved 100TB of Memory

BBetter Stack
Computing/SoftwareInternet Technology

Transcript

00:00:00¿Cuánto te puede costar realmente un solo byte?
00:00:02Bueno, a la escala de Cloudflare, desperdiciar un solo byte por entrada
00:00:04les cuesta más de 250 gigabytes de memoria
00:00:06en toda su infraestructura,
00:00:08y recientemente redujeron a la mitad esa memoria por entrada,
00:00:11liberando 100 terabytes de memoria.
00:00:13En esta economía, eso es muchísimo dinero.
00:00:15Lo lograron con cinco cambios bastante sencillos
00:00:17en su sistema de caché,
00:00:18y al mismo tiempo hicieron que el DNS fuera más rápido,
00:00:21así que veamos cómo lo hicieron.
00:00:27Tal vez ya hayas oído hablar de 1.1.1.1 antes.
00:00:30Es el solucionador DNS público de Cloudflare,
00:00:32y funciona tomando el dominio al que quieres ir,
00:00:34como betterstack.com,
00:00:35y averiguando cuál es la IP real de ese dominio.
00:00:39El programa que hace este trabajo
00:00:40se llama Big Pineapple y está escrito en Rust.
00:00:42Como te imaginarás,
00:00:43es un software extremadamente ocupado,
00:00:46que almacena más de 250 mil millones de entradas de caché DNS al mismo tiempo.
00:00:50Esto garantiza que si alguien quiere ir a betterstack.com
00:00:52dos segundos después de otra persona,
00:00:53no tenga que recorrer toda la jerarquía DNS de nuevo,
00:00:56sino que guarda la respuesta en memoria y la devuelve de inmediato.
00:00:59Pero como mencioné en la introducción,
00:01:01250 mil millones de entradas de caché significan que un byte desperdiciado por entrada
00:01:04cuesta unos 250 gigabytes de RAM,
00:01:07y por eso estos cinco cambios tuvieron un impacto tan grande.
00:01:10Primero, hablemos del costo de capacidad.
00:01:12Así es como solía verse una entrada de caché.
00:01:14Marca de tiempo, tiempo de vida, contadores de visitas
00:01:16y luego un montón de vectores.
00:01:17Un vector de registros de respuesta,
00:01:18un vector de registros de autoridad,
00:01:20un vector de registros adicionales,
00:01:21un vector de errores
00:01:22y mucho más.
00:01:23Para los que no estén familiarizados con Rust,
00:01:25un vector es simplemente el tipo de dato para una lista redimensionable,
00:01:27y también consta de tres cosas en memoria:
00:01:29un puntero, una longitud y una capacidad.
00:01:31Es cuánto espacio se reserva para el crecimiento,
00:01:32para no tener que reasignar memoria cada vez que se añaden elementos.
00:01:35Lo cual es muy útil si el elemento realmente crece.
00:01:38Pero analizaron las entradas reales de caché DNS
00:01:40y se dieron cuenta de que todo se escribía en la caché
00:01:42y nunca se volvía a tocar.
00:01:44Solo se leían,
00:01:45así que estos vectores nunca crecían.
00:01:47Eso significa que hay espacio reservado en el montón
00:01:49que se desperdicia por completo.
00:01:50Hay un vector con capacidad para ocho elementos,
00:01:52pero solo almacena cinco,
00:01:53dejando tres espacios sin usar,
00:01:55y el campo de capacidad en sí es un usize,
00:01:57lo que equivale a ocho bytes de memoria
00:01:58que para ellos son totalmente innecesarios.
00:02:00La solución para esto fue increíblemente sencilla.
00:02:02Basta con cambiar un vector por un box,
00:02:04ya que el tamaño de un box es fijo al crearse,
00:02:07por lo que no necesita un campo de capacidad
00:02:08y almacena exactamente lo que contiene.
00:02:10No necesita asignar espacio adicional para el futuro.
00:02:13También se dieron cuenta de que podían aplicar este mismo ahorro
00:02:15a los campos de texto,
00:02:16ya que esencialmente son solo un vector de U8,
00:02:18permitiendo que el texto crezca o se reduzca,
00:02:20pero si no es necesario,
00:02:21se puede usar un segmento de cadena en un box,
00:02:23es decir, que esta cadena no va a cambiar.
00:02:26En total, en una entrada de caché,
00:02:27había ocho campos de vectores y cadenas,
00:02:29así que reemplazarlos con un box ahorró ocho bytes por campo,
00:02:3164 bytes por entrada,
00:02:33eliminando además el espacio excedente en el montón
00:02:35que un vector reserva para crecimiento futuro,
00:02:36lo que significa que los ahorros combinados sumaron más de 15 terabytes
00:02:39al escalar a 250 mil millones de entradas de caché.
00:02:42Todo eso a partir de un simple cambio de tipo de dato.
00:02:45Para nuestro próximo cambio,
00:02:46examinaron algunas de esas listas
00:02:48y simplemente se preguntaron: ¿realmente las necesitamos?
00:02:50Una respuesta DNS contiene tres entradas:
00:02:52respuesta, autoridad y adicional,
00:02:54y como vimos antes,
00:02:55se guardaban en caché tal como llegaban,
00:02:57en tres listas separadas,
00:02:58y aunque eliminamos el campo de capacidad en el primer cambio,
00:03:01cada lista sigue teniendo un puntero y una longitud,
00:03:03así que ocho bytes más ocho bytes por tres,
00:03:05lo que da un total de 48 bytes
00:03:07y tres asignaciones de memoria independientes en el montón.
00:03:09Pero al observar cómo se utilizaban
00:03:10estas listas en realidad,
00:03:12se dieron cuenta de que siempre se leían juntas,
00:03:13siempre se escribían juntas
00:03:14y siempre mantenían exactamente el mismo orden.
00:03:16Así que, en lugar de tres listas,
00:03:17podían tratarlas como una sola lista con dos divisores.
00:03:20De este modo, las tres listas se convierten en una lista de registros,
00:03:22más dos pequeños desplazamientos que indican dónde terminan las respuestas,
00:03:25dónde termina la autoridad
00:03:26y, dado que una respuesta DNS nunca tendrá cuatro mil millones de registros,
00:03:29un entero sin signo de 16 bits es suficiente para los desplazamientos,
00:03:33lo que equivale a solo dos bytes cada uno.
00:03:34Esto significa que, en total,
00:03:35reemplazaron dos cabeceras completas de 16 bytes con dos números de 2 bytes,
00:03:39ahorrando 28 bytes por entrada,
00:03:41y esto también permite que Rust elimine espacios vacíos adicionales entre campos,
00:03:44haciendo que la estructura sea más pequeña que la suma de los campos eliminados.
00:03:47Incluso llevaron este concepto más allá
00:03:49y fusionaron varios campos booleanos en una sola bandera de bits.
00:03:52Pasando al cambio número tres:
00:03:53¿Qué tal si dejamos de repetir el mismo nombre dos veces?
00:03:55Cada registro DNS incluye un propietario,
00:03:57que es simplemente el nombre de dominio al que pertenece ese registro.
00:04:00Así que cuando consultas betterstack.com y recibes tus registros,
00:04:03cada uno de ellos tiene escrito betterstack.com.
00:04:05Pero esa información es un poco redundante.
00:04:08Tú fuiste quien hizo la pregunta,
00:04:09así que el sistema ya conoce el nombre de dominio.
00:04:11Y Cloudflare ya estaba almacenando ese dominio de consulta como la clave de caché,
00:04:14¿para qué necesitan guardarlo de nuevo como propietario?
00:04:17Pues no lo necesitan.
00:04:17Así que Cloudflare simplemente cambió este campo a un box de nombre opcional,
00:04:20de modo que si el propietario coincide con la consulta,
00:04:22se almacena un valor nulo,
00:04:23pero si es genuinamente diferente,
00:04:24lo cual puede ocurrir en algunos casos como las cadenas CNAME,
00:04:27se guarda el propietario como antes.
00:04:29Al hacer esto, en los casos normales donde coinciden,
00:04:32se ahorran toda una asignación de memoria en el montón por registro,
00:04:34así que este ahorro consistió simplemente en examinar qué datos tenían en su caché
00:04:37y darse cuenta de que había valores duplicados.
00:04:39Para el ahorro número cuatro, tenemos la dirección IP de 144 bytes.
00:04:43Los datos de sus registros eran una enumeración en Rust de registros A, AAAA, TXT,
00:04:47SVCB y NAPTR; un solo tipo que los abarcaba todos.
00:04:51Pero aquí está el problema con las enumeraciones:
00:04:52siempre son tan grandes como su variante más grande.
00:04:54Cada valor de ese tipo ocupa exactamente la misma cantidad de espacio,
00:04:57lo necesite o no.
00:04:58En ese caso, NAPTR es el valor más grande, ocupando 136 bytes,
00:05:03y cuando le añades la etiqueta y el relleno al enum, llega a los 144 bytes.
00:05:07Si luego comparas eso con lo que necesita un registro A, que es solo una simple dirección IPv4,
00:05:12eso solo necesitaría cuatro bytes.
00:05:13Esto significa que cada registro A en esa caché estaba metido en una caja de 144 bytes,
00:05:17usando solo cuatro de ellos,
00:05:19y los registros A y AAAA son la mayor parte del tráfico real.
00:05:22En la propia mezcla de pruebas de Cloudflare, son un 56% registros A, 25% AAAA, así que la mayoría de la caché era solo relleno.
00:05:29La solución para esto fue nuestra confiable caja.
00:05:32Encapsularon las variantes grandes y dejaron A y AAAA en línea, porque eso es pequeño y común,
00:05:36así que ahora text, SVCB y NAPTR están detrás de un puntero,
00:05:40por lo que el enum solo tiene que ser tan grande como el más grande que queda, que es esa dirección IPv6 de 16 bytes.
00:05:45En total entonces, para un registro A o AAAA, eso supone un ahorro de 120 bytes cada uno.
00:05:50Pero este cambio en realidad tiene un inconveniente.
00:05:52Cuando encapsulas una variante, sus datos pasan de estar dentro de una entrada de caché,
00:05:55a estar en su propia región del montón en otra parte totalmente distinta,
00:05:58y eso te acarrea dos nuevos costos.
00:06:00El primero es el asignador.
00:06:02Cloudflare usa Gemalock, y Gemalock no te entrega el número exacto de bytes que pides,
00:06:06agrupa las asignaciones en bloques de tamaño fijo y redondea hacia arriba hasta el más cercano.
00:06:10Así que un registro text que pide 32 bytes aterriza en un bloque de 32 y no desperdicia nada,
00:06:15pero un registro MX pide 40, se redondea a 48 bytes y silenciosamente pierdes 8 bytes.
00:06:21El segundo costo es la localidad.
00:06:22Antes de encapsular, todos los datos de registro de una entrada estaban en un bloque de memoria contiguo,
00:06:27y después de encapsular, cada uno vive en otra parte, y leerlo significa seguir un puntero,
00:06:31y si ese puntero cae lejos del resto de la entrada,
00:06:34tu CPU tiene que ir a buscar una línea de caché completamente nueva solo para leerlo.
00:06:37Así que aunque la encapsulación solucionó el problema del relleno, creó un problema propio,
00:06:41y fue entonces cuando Cloudflare pensó: ¿y si no los almacenamos como tipos de Rust en absoluto?
00:06:45Bueno, ese es el cambio número cinco.
00:06:46Cloudflare describió esto como un punto medio: mantener el resto de la entrada de caché como campos
00:06:50estructurados normales, pero tomar los datos del registro y almacenarlos como bytes crudos.
00:06:54Así que en lugar de un enum o una caja por registro, toda la entrada recibe una sola matriz U8 en caja,
00:07:00con cada registro escrito como un prefijo de longitud de 2 bytes, seguido de sus datos.
00:07:03Esto deshace ambos costos de los que acabamos de hablar.
00:07:06Todas esas asignaciones en caja separadas se colapsan en una sola asignación para todos los datos de registro,
00:07:10por lo que ya no hay redondeo por registro a un bloque de Gemalock,
00:07:13y todo vuelve a estar empaquetado de forma contigua, recuperando esa localidad de caché que la encapsulación quitó.
00:07:18Y como beneficio adicional, también hace que las búsquedas sean más rápidas.
00:07:21Anteriormente, en cada acierto de caché, tenías un registro analizado en la memoria,
00:07:25y tenías que serializarlo campo por campo de vuelta al formato de cable DNS antes de enviarlo a ninguna parte.
00:07:30Ahora, en cambio, ya está en formato de cable DNS, por lo que la mayoría de los tipos de registro se copian directamente del búfer
00:07:35al mensaje saliente.
00:07:37Los únicos que todavía necesitan análisis son los registros que contienen nombres de dominio,
00:07:40es decir, CNAME, NS, MX y SOA,
00:07:43y eso es solo porque la compresión de nombres DNS significa que tienes que reescribir esos nombres de todos modos.
00:07:47Así que este cambio final reduce la memoria y elimina un montón de trabajo de la ruta crítica.
00:07:51Así que ahí lo tienes, esos son 5 cambios de bajo nivel en la caché,
00:07:54que al combinarse reducen la huella por entrada de 953 bytes a 420,
00:08:00un 56% más pequeño, y la memoria realmente asignada por entrada pasó de 1.1 kilobytes
00:08:05a 461 bytes, lo que supone un recorte del 58%.
00:08:09Además de eso, su rendimiento de inserción también aumentó un 43%,
00:08:12pasando de 625,000 entradas por segundo a 893,000,
00:08:17y las búsquedas también fueron un 19% más rápidas, bajando de 828 nanosegundos a 670.
00:08:23Implementaron estos cambios en producción este año,
00:08:25y su memoria residente P99 pasó de 9.3 gigabytes a 5.3,
00:08:29lo que representa un recorte del 43% en tráfico real,
00:08:32lo que en toda la flota equivale aproximadamente a 100 terabytes de memoria liberada,
00:08:35o el equivalente a la RAM de 130 servidores de 13ª generación.
00:08:38Ahora planean usar ese espacio extra para tener una caché más grande,
00:08:41acelerando las cosas aún más.
00:08:43Me gusta mucho esta publicación de blog porque destaca una decisión que tendremos que tomar.
00:08:46¿Deberíamos habernos sentado antes de construir todo esto y resolver toda la optimización,
00:08:50o habría sido eso una optimización prematura?
00:08:52Y supongo que en aquel entonces, en 2018, cuando se construyó esto,
00:08:55el costo de la RAM para Cloudflare no era tan significativo como lo es hoy.
00:08:59La publicación completa es un gran artículo, así que dejaré el enlace abajo.
00:09:02Házme saber qué piensas sobre esto en los comentarios,
00:09:03suscríbete mientras estás ahí,
00:09:04y como siempre, nos vemos en el próximo.
00:09:05Nos vemos en el próximo.

Key Takeaway

La reestructuración de los tipos de datos en la caché DNS de Cloudflare redujo la huella de memoria por entrada en un 58% y liberó 100 terabytes en toda su infraestructura.

Highlights

  • Cloudflare redujo su memoria de caché DNS en 100 terabytes mediante cinco optimizaciones de bajo nivel en el software Big Pineapple escrito en Rust.

  • El cambio de vectores a estructuras Box eliminó el espacio reservado para crecimiento futuro en el montón, ahorrando más de 15 terabytes de RAM.

  • Fusionar tres listas separadas de registros DNS en una sola lista con dos desplazamientos numéricos redujo la huella en 28 bytes por entrada.

  • El rendimiento de inserción de caché aumentó un 43% y las búsquedas se aceleraron un 19% tras implementar estos cambios en producción.

  • Almacenar los datos de los registros como bytes crudos en lugar de enums de Rust evitó el redondeo excesivo de memoria del asignador Jemalock.

Timeline

Optimización del costo de capacidad en vectores

  • El software Big Pineapple almacena más de 250 mil millones de entradas de caché DNS simultáneamente.
  • Los vectores en Rust reservan capacidad adicional para crecimiento futuro que las entradas de caché nunca utilizaban.
  • Sustituir vectores por tipos Box de tamaño fijo eliminó el campo de capacidad innecesario.

Cada entrada de caché contenía múltiples vectores y cadenas de texto que asignaban espacio en el montón para elementos adicionales que nunca crecían porque las entradas solo se leían. Cambiar estos campos por estructuras Box eliminó el campo de capacidad de ocho bytes y el espacio excedente en el montón. Esta modificación aportó por sí sola más de 15 terabytes de ahorro al escalar a 250 mil millones de entradas.

Fusión de listas y eliminación de redundancias

  • Las respuestas DNS se guardaban previamente en tres listas separadas que consumían 48 bytes y tres asignaciones en el montón.
  • Unificar las listas en una sola estructura con desplazamientos de 16 bits redujo 28 bytes por entrada.
  • Eliminar el almacenamiento duplicado del nombre de dominio en cada registro ahorró una asignación completa en el montón.

Las listas de respuesta, autoridad y adicional siempre se leían y escribían juntas en el mismo orden, por lo que almacenar tres cabeceras completas resultaba ineficiente. Al tratarlas como una sola lista con dos pequeños desplazamientos, la estructura ocupó menos espacio y permitió que el compilador eliminara los huecos vacíos. Asimismo, omitir el nombre de propietario cuando coincidía con la clave de consulta evitó duplicaciones innecesarias.

Resolución del relleno en enumeraciones y bytes crudos

  • Las enumeraciones de Rust ocupaban 144 bytes para ajustarse al tipo más grande, desperdiciando espacio en registros IPv4 pequeños.
  • La encapsulación de variantes redujo el relleno pero introdujo penalizaciones por asignación de Jemalock y pérdida de localidad de caché.
  • Almacenar los registros como una matriz de bytes crudos recuperó la localidad de caché y aumentó la velocidad de búsqueda un 19%.

El uso de enumeraciones obligaba a que los registros comunes de cuatro bytes ocuparan 144 bytes debido al tipo NAPTR. Aunque encapsular las variantes grandes solucionó el problema del tamaño, fragmentó la memoria en el montón debido al redondeo del asignador Jemalock y afectó la localidad de caché de la CPU. Cloudflare solucionó esto último almacenando los datos como una sola matriz de bytes crudos con un prefijo de longitud, lo que además permitió copiar la información directamente al formato de cable DNS sin necesidad de análisis previo.

Community Posts

No posts yet. Be the first to write about this video!

Write about this video