Transcript
00:00:00Quanto um único byte pode realmente custar?
00:00:02Bem, na escala da Cloudflare, desperdiçar um único byte por entrada
00:00:04custa mais de 250 gigabytes de memória
00:00:06em toda a sua frota,
00:00:08e recentemente eles cortaram essa memória por entrada pela metade,
00:00:11liberando 100 terabytes de memória.
00:00:13Nesta economia, isso é muito dinheiro.
00:00:15Eles fizeram isso com cinco mudanças bem simples
00:00:17no sistema de cache,
00:00:18e ainda tornaram o DNS mais rápido ao mesmo tempo,
00:00:21então vamos mergulhar e ver como eles fizeram isso.
00:00:27Bem, você talvez já tenha ouvido falar do 1.1.1.1 antes.
00:00:30É o resolvedor de DNS público da Cloudflare,
00:00:32e ele funciona pegando o domínio que você quer acessar,
00:00:34como betterstack.com,
00:00:35e descobrindo qual é o IP real para esse domínio.
00:00:39Ora, o componente que faz esse trabalho,
00:00:40chama-se Big Pineapple, e é escrito em Rust,
00:00:42e como você deve imaginar,
00:00:43é um pedaço de software incrivelmente ocupado,
00:00:46armazenando mais de 250 bilhões de entradas de cache DNS a qualquer momento.
00:00:50Isso garante que se alguém quiser acessar o betterstack.com
00:00:52dois segundos depois de outra pessoa,
00:00:53ele não precise percorrer toda a hierarquia de DNS novamente,
00:00:56apenas mantendo a resposta na memória e servindo-a de volta imediatamente.
00:00:59Mas como mencionei na introdução,
00:01:01250 bilhões de entradas de cache significam que um byte desperdiçado por entrada
00:01:04custa cerca de 250 gigabytes de RAM,
00:01:07e é por isso que essas cinco mudanças tiveram um impacto tão grande.
00:01:10Primeiro, temos o custo de capacidade.
00:01:12Eis como era uma entrada de cache antigamente.
00:01:14Carimbo de data/hora, tempo de vida, contadores de hits,
00:01:16e um monte de vetores.
00:01:17Um vetor de registros de resposta,
00:01:18um vetor de registros de autoridade,
00:01:20um vetor de registros adicionais,
00:01:21um vetor de erros,
00:01:22e muito mais.
00:01:23Para aqueles que não estão familiarizados com Rust,
00:01:25um vetor é apenas o tipo de dados padrão para uma lista redimensionável,
00:01:27e ele também consiste em três coisas na memória.
00:01:29Um ponteiro, um comprimento e uma capacidade.
00:01:31É quanto espaço está reservado para crescimento,
00:01:32para que ele não precise realocar toda vez que você adiciona um item.
00:01:35O que é muito útil se o conteúdo realmente crescer.
00:01:38Mas eles analisaram entradas reais de cache DNS,
00:01:40e perceberam que tudo era apenas gravado no cache,
00:01:42e nunca mais tocado.
00:01:44Ele era apenas lido,
00:01:45então esses vetores nunca realmente cresciam.
00:01:47Isso significa que há espaço de heap sobre-alocado,
00:01:49parado ali e desperdiçado.
00:01:50Existe um vetor com capacidade para oito itens,
00:01:52mas com apenas cinco armazenados,
00:01:53deixando três espaços não utilizados,
00:01:55e o próprio campo de capacidade é um usize,
00:01:57o que representa oito bytes de memória,
00:01:58que para eles simplesmente não são necessários.
00:02:00A solução para isso foi incrivelmente simples.
00:02:02Basta trocar um vetor por um box,
00:02:04já que o tamanho de um box é fixo no tamanho com que foi criado,
00:02:07então ele não precisa de um campo de capacidade,
00:02:08e também armazena exatamente o que está lá.
00:02:10Ele não precisa alocar capacidade extra para o futuro.
00:02:13Eles também perceberam que podiam aplicar exatamente essa mesma economia
00:02:15aos campos de string também,
00:02:16já que elas são essencialmente apenas um vetor de U8s,
00:02:18permitindo que o texto cresça ou diminua,
00:02:20mas se você não precisa disso,
00:02:21você pode simplesmente usar um box de string slice,
00:02:23ou seja, esta string não vai mudar.
00:02:26No total, em uma entrada de cache,
00:02:27havia oito campos de vetores e strings,
00:02:29então substituí-los por um box poupou oito bytes por campo,
00:02:31e 64 bytes por entrada,
00:02:33eliminando também o espaço de heap excedente
00:02:35que um vetor reserva para crescimento futuro,
00:02:36o que significa que a economia combinada somou mais de 15 terabytes
00:02:39quando dimensionada para 250 bilhões de entradas de cache.
00:02:42Tudo isso a partir de uma simples mudança de tipo de dados.
00:02:45Para a nossa próxima mudança, no entanto,
00:02:46eles olharam para algumas dessas listas
00:02:48e simplesmente se perguntaram: nós sequer precisamos delas?
00:02:50Uma resposta de DNS contém três entradas,
00:02:52resposta, autoridade e adicional,
00:02:54e como vimos antes,
00:02:55elas eram armazenadas em cache conforme chegavam,
00:02:57em três listas separadas,
00:02:58e mesmo tendo removido o campo de capacidade na mudança um,
00:03:01cada lista ainda possui um ponteiro e um comprimento,
00:03:03então oito bytes mais oito bytes vezes três,
00:03:05o que dá 48 bytes no total,
00:03:07e três alocações de heap separadas.
00:03:09Mas quando analisaram como essas listas
00:03:10eram realmente usadas,
00:03:12perceberam que elas eram sempre lidas juntas,
00:03:13sempre gravadas juntas,
00:03:14e estão sempre na mesma ordem exata.
00:03:16Então, em vez de três listas,
00:03:17eles podiam tratá-la como uma única lista com dois divisores dentro.
00:03:20Assim, as três listas se tornam uma única lista de registros,
00:03:22mais dois pequenos offsets que dizem que as respostas terminam aqui,
00:03:25e a autoridade termina aqui,
00:03:26e como a resposta de DNS nunca terá quatro bilhões de registros,
00:03:29um inteiro sem sinal de 16 bits será suficiente para os offsets,
00:03:33o que representa apenas dois bytes cada.
00:03:34Isso significa que, no total,
00:03:35eles substituíram dois cabeçalhos completos de 16 bytes por dois números de 2 bytes,
00:03:39poupando 28 bytes por entrada,
00:03:41e isso também permite que o Rust remova algum espaçamento extra entre os campos,
00:03:44tornando a struct menor do que apenas os campos removidos por si só.
00:03:47Eles até levaram esse conceito adiante,
00:03:49e fundiram vários campos booleanos em um único bit flag.
00:03:52Passando para a mudança número três,
00:03:53e se pararmos de dizer o mesmo nome duas vezes?
00:03:55Cada registro de DNS carrega um proprietário,
00:03:57que é justamente o nome de domínio ao qual o registro pertence.
00:04:00Portanto, quando você consulta o betterstack.com e recebe seus registros de volta,
00:04:03cada um deles traz escrito betterstack.com.
00:04:05Mas essa informação é meio redundante.
00:04:08É você quem fez a pergunta,
00:04:09então o sistema já sabe o nome de domínio.
00:04:11E a Cloudflare já estava armazenando esse domínio consultado como chave de cache,
00:04:14então por que eles precisam armazená-lo novamente como proprietário?
00:04:17Bem, eles não precisam.
00:04:17Então a Cloudflare simplesmente mudou esse campo para um box name opcional,
00:04:20onde se o proprietário corresponder à consulta,
00:04:22você armazena none,
00:04:23mas se ele for genuinamente diferente,
00:04:24o que pode acontecer em alguns casos como cadeias de CNAME,
00:04:27você armazena o proprietário como antes.
00:04:29Ao fazer isso, para os casos normais em que eles coincidem,
00:04:32eles economizam uma alocação inteira de heap por registro,
00:04:34de modo que essa economia foi simplesmente uma questão de analisar os dados presentes no cache
00:04:37e perceber que havia valores duplicados.
00:04:39Para a economia número quatro, temos o endereço IP de 144 bytes.
00:04:43Os dados de seus registros eram um enum em Rust de registros A, AAAA, TXT,
00:04:47SVCB e NAPTR, um único tipo englobando todos eles.
00:04:51Mas eis o problema com os enums.
00:04:52Eles são sempre tão grandes quanto sua maior variante.
00:04:54Cada valor desse tipo ocupa a mesma quantidade de espaço,
00:04:57quer precise disso ou não.
00:04:58Nesse caso, o NAPTR é o maior valor, ocupando 136 bytes,
00:05:03e quando você adiciona a tag e o preenchimento ao enum, ele chega a 144 bytes.
00:05:07Se você comparar isso com o que um registro A precisa, que é apenas um endereço IPv4 simples,
00:05:12isso precisaria de apenas quatro bytes.
00:05:13Isso significa que cada registro A nesse cache estava dentro de uma caixa de 144 bytes,
00:05:17usando apenas quatro deles,
00:05:19e os registros A e AAAA formam a maior parte do tráfego real.
00:05:22Na mistura de benchmark da própria Cloudflare, são 56% de registros A, 25% AAAA, então a maior parte do cache era apenas preenchimento.
00:05:29A solução para isso foi a nossa confiável caixa.
00:05:32Eles colocaram as variantes grandes em caixas e deixaram A e AAAA embutidos, porque são pequenos e comuns,
00:05:36então agora TXT, SVCB e NAPTR estão atrás de um ponteiro,
00:05:40de modo que o enum só precisa ser tão grande quanto o maior restante, que é o endereço IPv6 de 16 bytes.
00:05:45No total, então, para um registro A ou AAAA, são 120 bytes economizados em cada um.
00:05:50Mas essa mudança realmente traz uma desvantagem.
00:05:52Quando você coloca uma variante em uma caixa, os dados dela deixam de ficar dentro de uma entrada de cache
00:05:55e passam a residir em sua própria região do heap em outro lugar completamente diferente,
00:05:58e isso traz dois novos custos.
00:06:00O primeiro é o alocador.
00:06:02A Cloudflare usa o Jemalloc, e o Jemalloc não te entrega o número exato de bytes que você pede,
00:06:06ele agrupa alocações em blocos de tamanho fixo e arredonda para cima até o mais próximo.
00:06:10Portanto, um registro TXT que pede 32 bytes vai para um bloco de 32 e não desperdiça nada,
00:06:15mas um registro MX pede 40, é arredondado para 48 bytes e perde silenciosamente 8 bytes.
00:06:21O segundo custo é a localidade.
00:06:22Antes do boxing, todos os dados de registro de uma entrada ficavam em um único bloco contíguo de memória,
00:06:27e depois do boxing, cada um vive em outro lugar, e lê-los significa seguir um ponteiro,
00:06:31e se esse ponteiro estiver longe do resto da entrada,
00:06:34sua CPU precisa buscar uma linha de cache inteiramente nova só para lê-lo.
00:06:37Portanto, embora o boxing tenha resolvido o problema de preenchimento, ele criou um problema próprio,
00:06:41e foi aí que a Cloudflare pensou: e se não os armazenarmos como tipos Rust?
00:06:45Bem, essa é a mudança número cinco.
00:06:46A Cloudflare descreveu isso como um meio-termo: manter o restante da entrada de cache como campos
00:06:50estruturados normais, mas pegar os dados do registro em si e armazená-los como bytes brutos.
00:06:54Assim, em vez de um enum ou uma caixa por registro, a entrada inteira recebe uma única matriz u8 encaixotada,
00:07:00com cada registro escrito como um prefixo de comprimento de 2 bytes, seguido por seus dados.
00:07:03Isso desfaz ambos os custos de que acabamos de falar.
00:07:06Todas aquelas alocações encaixotadas separadas se fundem em uma única alocação para todos os dados de registro,
00:07:10então não há mais arredondamento por registro para um bloco do Jemalloc,
00:07:13e tudo fica empacotado de forma contígua novamente, recuperando a localidade de cache que o boxing havia tirado.
00:07:18E, como bônus adicional, isso também torna as buscas mais rápidas.
00:07:21Anteriormente, em cada hit de cache, você tinha um registro analisado na memória,
00:07:25e precisava serializá-lo campo por campo de volta para o formato de fio DNS antes de enviá-lo a qualquer lugar.
00:07:30Agora, no entanto, ele já está no formato de fio DNS, então a maioria dos tipos de registro é copiada diretamente do buffer
00:07:35para a mensagem de saída.
00:07:37Os únicos que ainda precisam de análise são os registros que contêm nomes de domínio,
00:07:40ou seja, CNAME, NS, MX e SOA,
00:07:43e isso apenas porque a compressão de nomes DNS significa que você precisa reescrever esses nomes de qualquer maneira.
00:07:47Portanto, esta mudança final reduz a memória e elimina uma carga de trabalho do caminho crítico.
00:07:51Aí está, essas são 5 alterações de baixo nível no cache,
00:07:54que, quando combinadas, reduzem a pegada por entrada de 953 bytes para 420,
00:08:00sendo 56% menor, e a memória efetivamente alocada por entrada passou de 1,1 quilobyte
00:08:05para 461 bytes, uma redução de 58%.
00:08:09Além disso, a taxa de inserção aumentou em 43%,
00:08:12de 625.000 entradas por segundo para 893.000,
00:08:17e as buscas também ficaram 19% mais rápidas, caindo de 828 nanossegundos para 670.
00:08:23Eles implementaram essas mudanças em produção este ano,
00:08:25e a memória residente P99 caiu de 9,3 gigabytes para 5,3,
00:08:29uma redução de 43% no tráfego real,
00:08:32o que em toda a frota representa cerca de 100 terabytes de memória liberada,
00:08:35ou o equivalente a RAM de 130 servidores da Geração 13.
00:08:38Agora eles planejam usar esse espaço extra para ter um cache maior,
00:08:41tornando as coisas ainda mais rápidas.
00:08:43Eu realmente gosto deste post de blog porque ele destaca uma decisão que teremos que tomar.
00:08:46Deveríamos ter nos sentado antes de construir tudo isso e planejado todas as otimizações,
00:08:50ou isso teria sido uma otimização prematura?
00:08:52E imagino que lá em 2018, quando isso foi construído,
00:08:55o custo de RAM para a Cloudflare não era tão significativo quanto é hoje.
00:08:59O post completo é um ótimo artigo, então vou deixar o link abaixo.
00:09:02Deixe-me saber o que você acha disso nos comentários,
00:09:03aproveite e se inscreva,
00:09:04e como sempre, vejo você no próximo.
00:09:05Vejo você no próximo.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video