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.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video