Cloudflare recupera mais 100TB de RAM otimizando hashing consistente em Rust
Ajustando a matemática por trás do algoritmo ketama e reestruturando um struct em Rust, a equipe de performance da Cloudflare cortou memória do Pingora Backend Router sem trocar hardware.
A Cloudflare publicou os detalhes de uma otimização que economizou mais de 100TB de RAM em toda a sua rede global, feita em um único serviço interno: o Pingora Backend↳Back-end49 conteúdosIntegração front-end com backend: 7 decisões que evitam caos entre APIs, BFF e GraphQLDev (Back & Front) · abr 2026Como criar uma FAKE API REST para testes — JSONPlaceholderDev (Back & Front) · set 2025Construindo um aplicativo de bate-papo de IA simples com Spring AI e AngularDev (Back & Front) · jul 2025Ver tudo em Dev (Back & Front) → Router (PBR), responsável por balancear requisições cacheáveis entre servidores. O ganho veio de duas frentes que raramente aparecem juntas em um post técnico: uma derivação matemática exata de uma fórmula estatística e um ajuste de baixo nível em um struct Rust. Nenhuma das duas, isoladamente, resolveria o problema. O relato completo está no blog da Cloudflare.
O ticket que começou tudo
A história nasce de um ticket interno aberto por Ivan Babrou: uso excessivo de memória na biblioteca pingora-ketama, usada pelo PBR para hashing consistente. Em alguns casos, o consumo chegava a 6GB só com essa estrutura. Para entender por que isso aconteceu, a equipe (Kevin Guthrie, Mariia Iurchenko e Zaidoon Abd Al Hadi, além do próprio Ivan) teve que revisitar como o algoritmo de hashing consistente é implementado na prática, não só na teoria de livro.
Por que hashing consistente, e por que ele engorda
Hashing consistente permite mapear tarefas (no caso, chaves de cache por URL) para servidores de forma estável: quando um servidor entra ou sai, só uma fração pequena das chaves precisa ser remapeada. Na prática, cada hash de servidor e de tarefa cai em um número inteiro (32, 64 ou 128 bits) e a tarefa vai para o primeiro servidor à direita desse número numa reta (ou anel) de hashes.
O problema é que, com um único hash por servidor, a distribuição de carga é péssima. Cloudflare calculou a expectativa e o desvio padrão da fração de carga que cada servidor recebe em uma distribuição com N servidores: o valor esperado é 1/N, mas o desvio padrão relativo (o chamado coeficiente de variação, CV) é aproximadamente igual à raiz de (N-1)/(N+1). Com 100 servidores, isso dá um CV de cerca de 99%, o que significa, na prática, que alguns servidores podem receber o dobro de tráfego que deveriam enquanto outros ficam praticamente ociosos.
A solução clássica: multiplicar hashes, com peso
A saída conhecida é dar a cada servidor múltiplos pontos no anel, não só um. Quanto mais pontos, mais a lei dos grandes números equilibra a distribuição. O NGINX usa 160 pontos por servidor como padrão, e o Pingora seguiu o mesmo número. Com 160 hashes por servidor, o CV cai de 99% para cerca de 8% no exemplo de 100 servidores, uma melhora enorme.
A Cloudflare ainda usa uma variação chamada ketama para dar peso diferente a cada servidor: se um servidor deve receber w vezes mais tráfego que outro, ele recebe w vezes mais hashes no anel. No caso do PBR, o peso é proporcional ao espaço em disco de cada nó, já que o serviço lida com cache. É esse esquema de pesos, multiplicado pela necessidade de anéis separados para cada combinação de recursos (compliance, features de cache habilitadas), que gera dezenas de anéis distintos guardados na memória simultaneamente, e explica os 6GB que o ticket de Ivan denunciou.
O truque de Rust que sozinho não bastava
Cada ponto no anel é representado por um struct simples:
struct Point {
hash: u32,
index: u32,
}Oito bytes: quatro para o hash, quatro para o índice que aponta para o servidor num array separado. Zaidoon notou que o índice nunca precisaria de mais de 16 bits (o PBR não coordena mais de 65 mil servidores ao mesmo tempo), então bastaria um u16. Só que trocar o tipo, sozinho, não muda nada:
struct PointV2 {
hash: u32,
index: u16,
}As regras de alinhamento de memória do Rust exigem que o tamanho de um struct seja múltiplo do tamanho do seu maior campo. Como o hash de 32 bits é o maior campo, o compilador ainda reserva 8 bytes, mesmo com o índice menor. A saída não foi usar #[repr(packed)] (que a equipe evita por razões conhecidas de segurança e portabilidade), e sim guardar os bytes crus num array e acessá-los por getters:
struct Point([u8; 6]);
impl Point {
fn hash(&self) -> u32 {
u32::from_ne_bytes(self.0[0..4].try_into().unwrap())
}
fn index(&self) -> u16 {
u16::from_ne_bytes(self.0[4..6].try_into().unwrap())
}
}A mudança, sozinha, reduziu em 25% a memória usada para hashing consistente. Mas o ganho maior ainda estava por vir, e dependia de matemática, não de código.
Por que 90% dos hashes eram desperdício
A fórmula de coeficiente de variação citada acima vale só para um hash por servidor. Para k hashes por servidor, a maioria das fontes só oferece aproximações. A equipe da Cloudflare derivou a fórmula exata (a derivação completa está num post complementar linkado no artigo original) e chegou a uma conclusão prática: cada redução de erro exige, aproximadamente, um aumento de uma ordem de grandeza no número de hashes. Com o peso médio de 625 usado internamente, o PBR estava gerando 160 × 625 = 100 mil hashes por servidor, e os últimos 90 mil desses hashes reduziam o erro em apenas 0,7%.
Pior: como os hashes de 32 bits têm um espaço finito, colisões passam a acontecer com mais frequência conforme o número de hashes cresce (o clássico paradoxo do aniversário). Simulações mostraram que, para data centers com 2048 servidores, o erro real chega a aumentar entre 10 mil e 100 mil hashes por servidor, justamente a faixa em que o PBR estava operando. Com os dados na mão, a equipe cortou o número de hashes gerados por servidor em 90%, sem perda perceptível de precisão no balanceamento.
Como migrar sem derrubar a origem
Trocar o anel de hashing muda para onde cada chave de cache aponta. Uma virada global de uma vez invalidaria quase todo o cache simultaneamente e jogaria uma avalanche de tráfego direto para os servidores de origem dos clientes. A solução foi manter as duas versões do anel (a antiga e a nova, compacta) em memória ao mesmo tempo durante a transição, com o framework de migração interno decidindo, por hash da requisição, qual anel usar. Isso deu duas vantagens: a decisão era estável por requisição e havia um caminho de rollback limpo, sem precisar reimplantar o PBR.
O rollout foi feito por camadas: primeiro locais de validação pequenos, depois grupos progressivamente maiores de data centers, controlando separadamente duas variáveis: quanto tráfego usava o novo anel e em quais data centers isso acontecia. Um rollout por porcentagem global teria espalhado o churn de cache por toda a rede de uma vez; o escopo por data center manteve o raio de impacto pequeno. A equipe monitorou traces de seleção de backend, contadores de versão de anel, erros de conexão do PBR, memória de processo, tempo de startup, comportamento de cache e tráfego de origem durante todo o processo. Ao chegar a 100%, o caminho antigo foi removido, resultando na queda de 100TB de memória usada globalmente, soma que se junta a outros 100TB que o time de DNS da Cloudflare já havia liberado no mês anterior.
O que muda para quem programa
As mudanças estão disponíveis agora na crate pingora-ketama, atrás de uma feature de cargo ainda não anunciada oficialmente: o anel v2 traz o formato de armazenamento compactado, um método de ordenação mais rápido e a possibilidade de escalar o número base de hashes por nó. Como a Pingora é open source, qualquer time que já use o framework para balanceamento de carga (ou que esteja avaliando construir um proxy próprio em Rust) pode testar o ganho direto no próprio projeto, sem esperar a Cloudflare anunciar nada em bombo.
Mas o ponto que vale além do código é o método: as regras de alinhamento de memória do Rust não são detalhe de compilador, são a diferença entre um struct de 6 bytes reais e um de 8 bytes desperdiçados, e isso importa em qualquer sistema que guarde milhões dessas estruturas em memória, seja um proxy de borda ou um jogo. E a estratégia de migração (duas versões coexistindo, decisão determinística por requisição, rollout com raio de impacto controlado por localidade) é um padrão reaproveitável para qualquer troca de algoritmo de roteamento ou particionamento em produção, dentro ou fora do contexto de CDN.
Fonte: Hacker News
Este artigo foi escrito por Redação iMasters. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.













