Pular para o conteúdo

Hashing consistente explicado: anel de hash, nós virtuais e rebalanceamento

Por que hash(key) % N quebra quando você adiciona um servidor, como um anel de hash com nós virtuais resolve isso e como rendezvous, jump e Maglev se comparam.

· Fundador e desenvolvedor

Publicado em

Um círculo representa o espaço de hash. Tokens coloridos pertencem aos servidores de cache A, B e C, e tokens tracejados ao novo servidor D. As chaves no círculo apontam no sentido horário para o seu token. Uma legenda mostra as cores e uma nota traz os números de movimentação.

Você espalha um cache por dez servidores com hash(key) % 10. O tráfego cresce, você adiciona um décimo primeiro servidor e a taxa de acertos despenca: quase toda chave agora cai em outro servidor, então o cache fica frio em todo lugar ao mesmo tempo e o banco de dados por trás dele recebe a carga inteira. O hashing consistente existe justamente para evitar isso.

Este post explica o anel de hash e os nós virtuais com um diagrama, mede quanto eles ajudam, mostra uma implementação compacta e compara as alternativas que você vai encontrar em sistemas reais: rendezvous hashing, jump hash, Maglev e slots de hash fixos.

Por que o hashing por módulo quebra

Com server = hash(key) % N, o mapeamento depende de N. Mude N, e uma chave só fica onde está se o seu hash der o mesmo resto para os dois valores, o que é raro. Em uma simulação rápida com um milhão de chaves:

Mudança Chaves que mudam com hashing por módulo Chaves que mudam com um anel de hash
De 10 para 11 servidores 91,0% 10,6%

O ideal é 1/11, cerca de 9,1%: o servidor novo deveria ficar com a sua parte justa e mais nada deveria mudar de lugar. O anel chega perto; a diferença que sobra vem do fato de os tokens do servidor novo caírem em arcos um pouco maiores que a média.

Para um cache, a diferença é entre uma breve queda na taxa de acertos e uma partida a frio. Para um banco de dados com sharding ou um conjunto de workers com estado, é a diferença entre mover um décimo dos dados e mover quase tudo.

O anel de hash

O hashing consistente, apresentado por Karger e colegas em 1997 para cache web, mapeia servidores e chaves no mesmo espaço de hash circular, por exemplo os inteiros de 0 a 2^64 - 1, com o maior valor voltando para 0.

  1. Aplique o hash ao nome de cada servidor para obter uma posição no anel.
  2. Aplique o hash a cada chave para obter uma posição no anel.
  3. Ande no sentido horário a partir da posição da chave. O primeiro servidor que você encontrar é o dono da chave.

Adicionar um servidor o coloca em uma posição, e ele assume só as chaves entre o seu antecessor e ele mesmo. Todas as outras chaves ficam onde estavam. Remover um servidor passa as chaves dele para o próximo servidor no sentido horário. Nada mais muda.

Nós virtuais: corrigindo o desequilíbrio

Com uma posição por servidor, os arcos entre servidores são aleatórios e desiguais, então alguns servidores ficam com uma parte do anel muito maior que outros. A mesma simulação com dez servidores mostra o tamanho do problema:

Posições por servidor Servidor mais carregado vs. média Menos carregado vs. média
1 2,30× 0,13×
10 1,45× 0,69×
100 1,07× 0,88×
1.000 1,03× 0,95×

A solução é dar a cada servidor muitas posições, chamadas nós virtuais ou tokens, aplicando o hash a nomes como cache-a#0, cache-a#1 e assim por diante. Os nós virtuais trazem ainda outras duas vantagens:

  • Pesos. Um servidor com o dobro de memória recebe o dobro de tokens.
  • Falhas mais suaves. Quando um servidor morre, os seus muitos arcos pequenos vão para muitos sucessores diferentes, então a carga dele se espalha pelo cluster em vez de dobrar a carga de um único vizinho.

O custo é uma tabela de lookup maior e mais metadados para mover quando os membros mudam. O Cassandra mostra esse trade-off na prática: ele usava 256 tokens por nó por padrão até a versão 4.0 baixar o padrão para 16, junto com um algoritmo de alocação de tokens que posiciona esses 16 de forma uniforme em vez de aleatória.

Uma implementação pequena

Um anel é uma lista ordenada de posições de tokens mais uma busca binária:

import bisect
import hashlib

def hash64(value: str) -> int:
    # MD5 for spread, not security; any well-mixed hash works
    return int.from_bytes(hashlib.md5(value.encode()).digest()[:8], "big")

class HashRing:
    def __init__(self, servers, vnodes=100):
        self.vnodes = vnodes
        self.points = []   # sorted token positions
        self.owners = {}   # position -> server
        for server in servers:
            self.add(server)

    def add(self, server):
        for i in range(self.vnodes):
            p = hash64(f"{server}#{i}")
            bisect.insort(self.points, p)
            self.owners[p] = server

    def remove(self, server):
        for i in range(self.vnodes):
            p = hash64(f"{server}#{i}")
            self.points.remove(p)
            del self.owners[p]

    def lookup(self, key: str) -> str:
        i = bisect.bisect_right(self.points, hash64(key)) % len(self.points)
        return self.owners[self.points[i]]

As buscas custam O(log T) para T tokens. Versões de produção também tratam colisões de tokens e mantêm idêntica a visão de membros de cada cliente, já que dois clientes com listas de servidores diferentes discordam sobre onde as chaves ficam.

Replicação no anel

Sistemas de armazenamento usam o anel para posicionamento, além de roteamento. O artigo do Dynamo, da Amazon, guarda cada chave nos primeiros N servidores distintos no sentido horário a partir da sua posição, a sua preference list, pulando tokens extras que pertencem a um servidor que já está na lista. Com N = 3, perder um servidor ainda deixa duas cópias de tudo o que ele guardava, em servidores que são seus vizinhos no anel.

A mesma ideia de “andar no sentido horário e pular duplicatas” se estende a racks e zonas: pule tokens até que as réplicas cubram domínios de falha diferentes.

Alternativas que você vai encontrar

Método Busca Memória Chaves movidas na mudança Observações
Módulo hash % N O(1) Nenhuma Quase todas Só serve se N nunca mudar
Anel com nós virtuais O(log T) T tokens Cerca de 1/N Pesos e replicação vêm naturalmente
Rendezvous (highest random weight) O(N) por chave Nenhuma Cerca de 1/N Pontue cada servidor e escolha o maior; simples e equilibrado
Jump consistent hash O(log N) Nenhuma Cerca de 1/N Servidores numerados de 0 a N-1; só adiciona ou remove no fim
Hashing Maglev Busca em tabela O(1) Uma tabela fixa Quase mínimo Criado pelo Google para balanceadores de carga em software
Slots de hash fixos O(1) Uma entrada por slot Só os slots que você move O Redis Cluster usa 16.384 slots e os move explicitamente

O jump consistent hash combina com armazenamento com sharding em que os shards são numerados e só crescem. O rendezvous hashing combina com conjuntos pequenos de servidores, em que O(N) por busca não importa. Os slots de hash trocam o posicionamento automático por movimentações explícitas, controladas pelo operador, algo que muitos times preferem para bancos de dados.

Hot keys e cargas limitadas

O hashing consistente equilibra chaves, não tráfego. Um usuário famoso ou um produto viral continua caindo em um único servidor. O hashing consistente com cargas limitadas (bounded loads) limita cada servidor a uma porcentagem definida acima da média e manda o excedente para o próximo servidor no sentido horário. O HAProxy implementa isso com a configuração hash-balance-factor. Para chaves realmente quentes, você ainda precisa replicar essa chave, agrupar requisições ou colocar um pequeno cache local na frente.

Armadilhas operacionais

  • Trocar a função de hash ou o esquema de tokens embaralha tudo, exatamente como o hashing por módulo. Trate isso como uma migração.
  • Os membros precisam ser consistentes. Distribua a lista de servidores a partir de uma única fonte, como um serviço de coordenação ou a configuração do balanceador de carga, e mude-a em um único passo.
  • Servidores instáveis causam churn. Um servidor que falha nos health checks a cada poucos minutos leva as suas chaves de um lado para o outro. Adicione histerese antes de removê-lo e adicioná-lo de novo.
  • Servidores novos começam frios. Um nó de cache novo recebe a sua parte das requisições na hora; aqueça-o ou aumente o peso dele aos poucos se o backend não aguentar os cache misses.

O diagrama do anel de hash

Um círculo representa o espaço de hash. Tokens coloridos pertencem aos servidores de cache A, B e C, e tokens tracejados ao novo servidor D. As chaves no círculo apontam no sentido horário para o seu token. Uma legenda mostra as cores e uma nota traz os números de movimentação.

20 componentes · 5 conexões

Abrir no Archboard
Conteúdo do diagrama
  • Hash space, clockwise key → next token
  • A1
  • B1
  • C1
  • D1
  • A2
  • B2
  • C2
  • D2
  • Server A · cache-a
  • Server B · cache-b
  • Server C · cache-c
  • Server D · joining
Um anel de hash com três servidores de cache, cada um com vários nós virtuais. Uma chave pertence ao primeiro token no sentido horário a partir do seu hash. Um servidor novo assume só os arcos à frente dos próprios tokens, então a maioria das chaves fica onde está.

Como ler:

  • O círculo é o espaço de hash. As posições crescem no sentido horário, e o topo é onde o maior valor volta a zero.
  • Os tokens coloridos são nós virtuais. Cada cor é um servidor, e cada servidor tem vários tokens espalhados pelo anel.
  • As chaves ficam nas posições dos seus hashes, e as setas apontam no sentido horário para o token que é dono delas.
  • Os tokens tracejados pertencem ao servidor D, que está sendo adicionado. Só as chaves logo antes dos tokens de D, no sentido anti-horário, mudam de dono.
  • A legenda e o bloco Markdown listam os servidores e os números de movimentação da simulação acima.

Desenhar esquemas de hashing com clareza

  • Desenhe o anel, não uma tabela. A regra do sentido horário é visual; uma tabela de lookup esconde por que só algumas chaves mudam de lugar.
  • Pinte os tokens por servidor e adicione uma legenda, em vez de puxar uma aresta de cada token até o seu servidor.
  • Mostre uma mudança de membros. Um anel estático explica o roteamento; um anel em que um servidor entra explica todo o sentido da técnica.
  • Coloque números reais ao lado: chaves movidas e distribuição da carga. Eles convencem mais que a figura.

O hashing consistente aparece em vários posts de design de sistemas deste blog: no diagrama de sharding de banco de dados, para posicionar dados; no diagrama de balanceador de carga, para roteamento persistente; e no diagrama de estratégias de cache, para caches distribuídos.

Monte no Archboard

  1. Entre com Google ou GitHub e crie um diagrama.
  2. Adicione uma forma Elipse grande do pack Básico como o anel.
  3. Adicione sobre o anel formas Elipse pequenas como tokens, pintadas por servidor: duas para cada um dos servidores A, B e C, e duas tracejadas para o novo servidor D.
  4. Adicione quatro nós Redis do pack Dados em uma coluna ao lado do anel como legenda, com as cores dos seus tokens.
  5. Adicione as chaves como rótulos Texto do pack Básico sobre o anel, com setas curtas até os tokens donos delas.
  6. Adicione um bloco Markdown do pack Básico com os números de movimentação de chaves e de carga.

O exemplo usa 14 nós de diagrama e fica dentro do plano Free.

Perguntas frequentes

Que problema o hashing consistente resolve? Ele mantém a maioria das chaves no mesmo servidor quando servidores são adicionados ou removidos. Com o hashing por módulo, mudar a quantidade de servidores remapeia quase todas as chaves; com o hashing consistente, só cerca de 1/N delas muda de lugar.

O que são nós virtuais no hashing consistente? Cada servidor físico é colocado muitas vezes no anel, com nomes diferentes. Mais posições deixam os arcos mais uniformes, permitem dar pesos aos servidores conforme a capacidade e espalham a carga de um servidor que falhou por muitos outros.

Quantos nós virtuais cada servidor deve ter? O suficiente para uma distribuição uniforme no tamanho do seu cluster: na nossa simulação, 100 por servidor mantiveram o servidor mais carregado a até 7% da média com dez servidores. Sistemas com alocação de tokens mais inteligente, como o Cassandra 4.0 em diante, usam bem menos.

O Redis Cluster usa hashing consistente? Não no sentido do anel. O Redis Cluster mapeia as chaves para 16.384 slots de hash fixos e atribui os slots aos nós; mover dados significa mover slots explicitamente.

Qual é a diferença entre hashing consistente e rendezvous hashing? Os dois movem cerca de 1/N das chaves quando os membros mudam. O rendezvous hashing pontua cada servidor para cada chave e escolhe o maior; não precisa de anel, mas custa O(N) por busca.

Comece grátis — um assento, sem cartão de crédito.

Artigos relacionados

← Voltar ao blog

Abra um canvas em um minuto

Entre com Google ou GitHub. Grátis para um assento. Faça upgrade quando a equipe entrar.