Seu serviço de pedidos usa há anos uma chave BIGSERIAL do Postgres. Então a tabela orders é dividida em quatro shards, a sequence de cada shard entrega o pedido 10452 e o suporte encontra dois clientes atrás do mesmo número de pedido. Ou o app mobile precisa de um ID antes mesmo de conseguir alcançar o servidor.
IDs deixam de ser de graça quando mais de uma máquina os cria. As opções trocam tamanho, ordenação, coordenação por ID e o quanto tratam bem um índice B-tree. Este post compara sequences, ticket servers, UUIDv4, UUIDv7, ULID e IDs estilo Snowflake, e depois projeta um gerador estilo Snowflake: layout de bits, leases de worker ID, saltos de relógio e a matemática de colisões.
Requisitos para IDs distribuídos
- Únicos, para sempre. Entre shards, regiões e reinícios.
- Mais ou menos ordenados por tempo, para que a paginação com
WHERE id > ?e os índices ordenados por tempo funcionem. O Snowflake original do Twitter prometia ordem entre máquinas dentro de um segundo. - Compactos. 64 bits cabem em um
BIGINT; 128 bits dobram cada chave. - Sem coordenação por ID. A meta do Snowflake era pelo menos 10.000 IDs por segundo por processo, cada um em até 2 ms.
- Seguros para expor, sem vazar mais do que você pretende.
Seis formas de gerar IDs
| Abordagem | Tamanho | Ordenação | Coordenação | Inserts no B-tree |
|---|---|---|---|---|
| Sequence de banco de dados | 64 bits | Crescente | Cada ID vem de um banco de dados | Borda direita |
| Ticket server | 64 bits | Crescente por servidor | Uma chamada por ID ou por faixa | Borda direita |
| UUIDv4 | 128 bits | Nenhuma | Nenhuma | Páginas aleatórias |
| UUIDv7 | 128 bits | Por milissegundo | Nenhuma | Perto da borda direita |
| ULID | 128 bits | Por milissegundo | Nenhuma | Perto da borda direita |
| Estilo Snowflake | 64 bits | Por milissegundo, aproximada | Um worker ID por gerador | Perto da borda direita |
Uma sequence de banco de dados serve enquanto um único banco escreve cada linha e quebra quando vários bancos dividem um espaço de IDs. Ela deixa lacunas, já que nextval nunca é revertido, e no PostgreSQL um CACHE acima de 1 mantém os valores distintos, mas fora de ordem entre sessões. Números de pedido sequenciais também revelam o seu volume de pedidos.
Um ticket server centraliza a sequence. Os ticket servers MySQL do Flickr eram duas máquinas, uma emitindo números ímpares e outra pares, com os clientes alternando entre elas. Alugar blocos de IDs elimina a chamada por ID, como no design de um encurtador de URLs.
UUIDv7, ULID e Snowflake colocam um timestamp primeiro, de modo que IDs criados juntos ordenam juntos; só o Snowflake cabe em 64 bits, pagando com worker IDs e uma dependência do relógio.
O layout de bits do Snowflake
| Bits | Campo | Capacidade |
|---|---|---|
| 1 | Bit de sinal, sempre 0 | IDs continuam positivos como inteiros de 64 bits com sinal |
| 41 | Milissegundos desde uma época própria | 2^41 ms, cerca de 69,7 anos |
| 10 | Worker ID | 1.024 geradores |
| 12 | Sequência dentro do milissegundo | 4.096 IDs por milissegundo por worker |
A versão do Twitter de 2010 usou essa divisão com uma época em novembro de 2010, então seus IDs acabam em julho de 2080. A partir da época Unix, 41 bits acabariam em setembro de 2039, por isso defina a época perto do seu lançamento. Cada instância guarda seu último milissegundo e um contador:
// Next is called with g.mu held.
func (g *Generator) Next() (int64, error) {
if time.Now().After(g.leaseDeadline) {
return 0, ErrLeaseLost // another instance may own this worker ID now
}
now := time.Now().UnixMilli()
if now < g.lastMs {
if g.lastMs-now > 5 {
return 0, ErrClockMovedBack
}
now = g.waitUntil(g.lastMs) // small step back: wait it out
}
if now == g.lastMs {
g.seq = (g.seq + 1) & 0xFFF // 12 bits
if g.seq == 0 { // 4,096 IDs used in this millisecond
now = g.waitUntil(g.lastMs + 1)
}
} else {
g.seq = 0
}
g.lastMs = now
return (now-epochMs)<<22 | g.workerID<<12 | g.seq, nil
}
Um worker emite até 4,1 milhões de IDs por segundo; o RPC ao redor é o limite real, então deixe os clientes buscarem IDs em lotes. Os IDs passam de 2^53 cerca de 25 dias depois da época, além do Number.MAX_SAFE_INTEGER do JavaScript, por isso envie-os como strings JSON, como fazem as APIs do X e do Discord.
A divisão é um orçamento. O Discord usa 42 bits para o tempo e divide os bits de nó em worker e processo. O Instagram gerava IDs dentro de cada shard do PostgreSQL com um ID de shard de 13 bits, o que ajuda quando você faz sharding de um banco de dados. O Sonyflake conta em unidades de 10 ms para durar 174 anos, a 256 IDs por 10 ms por instância.
Atribuindo worker IDs
O Snowflake não precisa de coordenação por ID, só por processo: dois geradores ativos com o mesmo worker ID podem emitir o mesmo ID.
- Configuração estática. Cada host recebe um
WORKER_ID. Serve para uma frota fixa; um arquivo de configuração copiado cria duplicatas. - Ordinais de StatefulSet. O Kubernetes numera os pods de um StatefulSet a partir de 0 e rotula cada um com
apps.kubernetes.io/pod-index(estável desde a v1.32), que a downward API repassa. Os ordinais são únicos apenas dentro de um StatefulSet em um cluster, e excluir um pod à força pode deixar dois pods com o mesmo ordinal. - Leases no etcd ou ZooKeeper. Na inicialização, a instância reivindica a menor chave livre, como
/idgen/workers/17, com uma transação que só tem sucesso se a chave não existir, em um lease de 10 segundos renovado a cada 3 segundos. Se a instância morrer, a chave expira. O Snowflake do Twitter de 2010 usava um nó efêmero do ZooKeeper e se recusava a iniciar se seu relógio estivesse a mais de 10 segundos da média de seus pares.
env:
- name: WORKER_ID
valueFrom:
fieldRef:
fieldPath: metadata.labels['apps.kubernetes.io/pod-index']
Leases precisam de duas regras. Pare de emitir IDs quando o último keepalive bem-sucedido for mais antigo que o TTL concedido menos uma margem, para que uma instância pausada ou particionada pare antes que outra possa reivindicar seu ID. E salve uma high-water mark: a cada poucos segundos, grave um timestamp um pouco à frente do relógio em uma chave separada, nunca emita IDs além dele e faça o próximo detentor esperar até que seu relógio o ultrapasse. O etcd mantém ambos consistentes com Raft.
Quando o relógio volta
Os daemons NTP normalmente ajustam o relógio com slew, fazendo-o correr um pouco mais rápido ou mais devagar, mas dão um salto em desvios grandes: o ntpd salta acima de 128 ms por padrão, e no modo padrão de segundo bissexto do chrony, o kernel recua o relógio um segundo para um segundo bissexto inserido. A documentação do chrony recomenda saltar apenas no boot:
makestep 1.0 3 # step (if off by over 1 s) only in the first three updates
leapsecmode slew # absorb a leap second by slewing, not by stepping back
Para um salto que passa, escolha uma regra:
| Estratégia | O que o gerador faz | Custo |
|---|---|---|
| Fail | Rejeita requisições até o relógio passar last_ms |
Erros enquanto o salto durar |
| Wait | Dorme até o relógio passar last_ms |
Bom para poucos ms; saltos longos travam todos os chamadores |
| Relógio lógico | Continua contando a partir de last_ms |
Nunca bloqueia; os timestamps ficam à frente do tempo real |
O original do Twitter falhava. Esperar alguns milissegundos e depois falhar com um alerta é um padrão sensato. Medir o tempo com CLOCK_MONOTONIC, que nunca volta, protege um processo em execução; a high-water mark cobre os reinícios.
UUIDv7 e índices B-tree
A RFC 9562, publicada em maio de 2024, substituiu a RFC 4122 e definiu o UUIDv7: um timestamp Unix de 48 bits em milissegundos, 4 bits de versão, 2 bits de variante e 74 bits para aleatoriedade, ou para um contador quando você precisa de ordem estrita dentro de um milissegundo. O timestamp dura até o ano 10889.
A RFC alerta que UUIDs sem ordem temporal têm má localidade de índice, com efeitos sobre os B-trees que podem ser dramáticos. No PostgreSQL, as novas chaves v7 vão para a página folha mais à direita, e os splits ali deixam as páginas 90% cheias, o fillfactor padrão do B-tree. Chaves v4 aleatórias atingem qualquer página: o índice inteiro vira o working set, páginas cheias sofrem split e, depois de cada checkpoint, a primeira alteração em cada página grava a página inteira no WAL. O InnoDB do MySQL ainda copia a chave primária para cada índice secundário, então uma chave de 16 bytes faz todos crescerem.
O PostgreSQL 18 adicionou uuidv7() em setembro de 2025. Armazene UUIDs como binário de 16 bytes, não como 36 caracteres de texto. E o v7 revela seu horário de criação; a RFC recomenda o v4 onde um ID tenha papel de segurança.
ULID
A especificação do ULID tem o mesmo formato: um timestamp de 48 bits em milissegundos e 80 bits aleatórios. Seu texto de 26 caracteres em base32 de Crockford, sem I, L, O e U, ordena corretamente como string e lê bem em URLs. O modo monotonic soma 1 dentro de um milissegundo, o que mantém a ordem, mas torna o próximo valor adivinhável. O ULID é uma especificação da comunidade; para chaves de banco de dados, o UUIDv7 oferece o mesmo com suporte nativo.
Qual a chance de uma colisão?
Para n IDs com b bits aleatórios, a chance de qualquer colisão é de cerca de n² / 2^(b+1). IDs com prefixo de tempo só podem colidir dentro de um mesmo milissegundo, então importa a taxa, não o total. A 1 milhão de IDs por segundo durante um ano, distribuídos uniformemente:
| Esquema | Bits aleatórios | Pode colidir com | Chance em um ano |
|---|---|---|---|
| UUIDv4 | 122 | Qualquer outro ID | Cerca de 1 em 10 bilhões |
| ULID | 80 | IDs do mesmo milissegundo | Cerca de 1 em 77 milhões |
| UUIDv7 | 74 | IDs do mesmo milissegundo | Cerca de 1 em 1,2 milhão |
| Snowflake | 0 | Mesmo worker ID e milissegundo | Zero, enquanto os worker IDs forem únicos e os relógios avançarem |
Esquemas aleatórios falham com uma probabilidade pequena e calculável. O Snowflake só falha por causa da operação, mas então com certeza. Mantenha uma unique constraint nos dois casos.
O diagrama do gerador de IDs estilo Snowflake
Conteúdo do diagrama
- ID service · 3 replicas
- id-gen-0 · worker ID 0
- id-gen-1 · worker ID 1
- id-gen-2 · worker ID 2
- Payment service
- Order service
- orders · id BIGINT PRIMARY KEY
- etcd cluster
- NTP servers
- 0
- 41 bits · milliseconds since the custom epoch 2^41 ms ≈ 69.7 years
- 10 bits · worker ID 1,024 instances
- 12 bits · sequence 4,096 IDs per ms
- NextId()
- INSERT row with the new id
- lease worker ID
- time sync
Como ler:
- Os clientes chamam
NextId()em qualquer uma das três instâncias, que respondem a partir da memória: o último milissegundo e um contador de 12 bits. - O etcd guarda cada worker ID em um lease de 10 segundos, além dos timestamps salvos.
- O NTP mantém os relógios próximos (aresta tracejada); o chrony só salta no boot.
- O serviço Order grava linhas no Postgres, onde IDs crescentes caem na página B-tree mais à direita.
- A barra mostra os 64 bits em escala; o bloco Markdown lista as configurações.
Desenhando a geração de IDs com clareza
- Desenhe o layout de bits em escala. Ele mostra o orçamento num relance.
- Mostre de onde vêm os worker IDs, com o TTL do lease. As duplicatas começam ali.
- Desenhe o relógio como uma dependência. O gerador só é tão correto quanto o seu relógio.
- Escreva as regras de falha no canvas: relógio recuou, lease perdido, reinício. O exemplo de diagrama de system design coloca números ao lado das caixas da mesma forma.
Monte no Archboard
- Entre com Google ou GitHub e crie um diagrama.
- Adicione uma Moldura do pack Básico para o serviço de IDs, com três nós Contêiner âmbar do pack C4 Model dentro: id-gen-0 a id-gen-2, um worker ID cada.
- Adicione dois nós Contêiner à esquerda para os serviços Payment e Order, e um nó Postgres do pack Dados abaixo deles.
- À direita, adicione um Contêiner violeta para o etcd, um Servidor azul do pack Rede para o NTP e um bloco Markdown do pack Básico com as configurações.
- Conecte-os: NextId() dos dois clientes até a moldura, INSERT para o Postgres, o lease para o etcd e uma aresta tracejada de sincronização de hora vinda do NTP.
- Abaixo, adicione quatro nós retangulares Elipse do pack Básico, com tamanho 1 : 41 : 10 : 12, para o layout de bits, e algumas notas Texto do Básico.
Um fluxograma Mermaid do design vira um primeiro rascunho editável com o conversor de Mermaid. O exemplo usa 14 nós de diagrama e fica dentro do plano Free.
Perguntas frequentes
O que é um Snowflake ID? Um ID de 64 bits construído a partir de um timestamp, um worker ID e uma sequência por milissegundo, introduzido pelo Twitter em 2010. Os geradores funcionam sem conversar entre si, e os IDs ordenam aproximadamente pelo horário de criação.
UUIDv7 é melhor que ULID?
Ambos carregam um timestamp de 48 bits em milissegundos e bits aleatórios. O UUIDv7 é um padrão da IETF com suporte em bancos de dados, como uuidv7() no PostgreSQL 18; o ULID tem uma forma de texto mais curta. Para chaves de banco de dados, prefira UUIDv7.
Devo usar UUIDv4 ou UUIDv7 como chave primária? UUIDv7 para a maioria das tabelas: valores novos ordenam depois dos antigos, então os inserts ficam perto da borda direita do índice. Use UUIDv4 onde um ID não deve revelar seu horário de criação.
Quantos IDs por segundo um gerador Snowflake consegue produzir? 4.096 por milissegundo por worker, cerca de 4,1 milhões por segundo, ou 4,2 bilhões entre 1.024 workers. A chamada de rede ao redor do gerador costuma ser o limite real.
O que acontece quando o relógio volta? O gerador não pode reutilizar um milissegundo que talvez já tenha usado. Ele pode falhar até o relógio alcançar, esperar ou manter um relógio lógico que corre à frente do tempo real; o original do Twitter falhava.