Saltar al contenido

Diseño de un generador de IDs únicos: Snowflake, UUIDv7 y ULID comparados

Cómo generar IDs únicos en un sistema distribuido: secuencias, UUIDv4, UUIDv7, ULID y Snowflake comparados, y después un generador estilo Snowflake diseñado paso a paso.

· Fundador y desarrollador

Publicado el

Diagrama de arquitectura. Los servicios Payment y Order llaman a NextId() en un servicio de IDs con tres instancias (worker IDs 0 a 2), que alquilan IDs a etcd y sincronizan la hora con NTP. El servicio Order inserta filas en Postgres. Una barra divide 64 bits en 1, 41, 10, 12.

Tu servicio de pedidos lleva años usando una clave BIGSERIAL de Postgres. Luego la tabla orders se reparte en cuatro shards, la secuencia de cada shard entrega el pedido 10452 y soporte encuentra a dos clientes detrás de un mismo número de pedido. O la app móvil necesita un ID antes de poder llegar siquiera al servidor.

Los IDs dejan de ser gratis cuando más de una máquina los crea. Las opciones intercambian tamaño, orden, coordinación por ID y lo bien que tratan a un índice B-tree. Este artículo compara secuencias, ticket servers, UUIDv4, UUIDv7, ULID e IDs estilo Snowflake, y luego diseña un generador estilo Snowflake: distribución de bits, leases de worker ID, saltos de reloj y cálculo de colisiones.

Requisitos para IDs distribuidos

  • Únicos, para siempre. Entre shards, regiones y reinicios.
  • Más o menos ordenados por tiempo, para que la paginación con WHERE id > ? y los índices ordenados por tiempo funcionen. El Snowflake original de Twitter prometía orden entre máquinas dentro de un segundo.
  • Compactos. 64 bits caben en un BIGINT; 128 bits duplican cada clave.
  • Sin coordinación por ID. El objetivo de Snowflake era al menos 10 000 IDs por segundo por proceso, cada uno en menos de 2 ms.
  • Seguros de exponer, sin filtrar más de lo que pretendes.

Seis formas de generar IDs

Enfoque Tamaño Orden Coordinación Inserciones en B-tree
Secuencia de base de datos 64 bits Creciente Cada ID desde una base de datos Borde derecho
Ticket server 64 bits Creciente por servidor Una llamada por ID o por rango Borde derecho
UUIDv4 128 bits Ninguno Ninguna Páginas aleatorias
UUIDv7 128 bits Por milisegundo Ninguna Cerca del borde derecho
ULID 128 bits Por milisegundo Ninguna Cerca del borde derecho
Estilo Snowflake 64 bits Por milisegundo, aproximado Un worker ID por generador Cerca del borde derecho

Una secuencia de base de datos es correcta mientras una sola base escribe cada fila y se rompe cuando varias bases comparten un espacio de IDs. Deja huecos, porque nextval nunca se revierte, y en PostgreSQL un CACHE mayor que 1 mantiene los valores distintos pero desordenados entre sesiones. Los números de pedido secuenciales además revelan tu volumen de pedidos.

Un ticket server centraliza la secuencia. Los ticket servers MySQL de Flickr eran dos máquinas, una emitía números impares y otra pares, y los clientes alternaban entre ellas. Alquilar bloques de IDs elimina la llamada por ID, como en el diseño de un acortador de URLs.

UUIDv7, ULID y Snowflake ponen una marca de tiempo al principio, de modo que los IDs creados juntos se ordenan juntos; solo Snowflake cabe en 64 bits, y lo paga con worker IDs y una dependencia del reloj.

La distribución de bits de Snowflake

Bits Campo Capacidad
1 Bit de signo, siempre 0 Los IDs siguen siendo positivos como enteros de 64 bits con signo
41 Milisegundos desde una época propia 2^41 ms, unos 69,7 años
10 Worker ID 1 024 generadores
12 Secuencia dentro del milisegundo 4 096 IDs por milisegundo y worker

La versión de Twitter de 2010 usó esta división con una época en noviembre de 2010, así que sus IDs se agotan en julio de 2080. Desde la época Unix, 41 bits se agotarían en septiembre de 2039, así que fija la época cerca de tu lanzamiento. Cada instancia guarda su último milisegundo y un 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
}

Un worker emite hasta 4,1 millones de IDs por segundo; el RPC que lo rodea es el límite real, así que deja que los clientes pidan IDs por lotes. Los IDs superan 2^53 unos 25 días después de la época, más allá del Number.MAX_SAFE_INTEGER de JavaScript, así que envíalos como strings JSON, como hacen las APIs de X y Discord.

La división es un presupuesto. Discord usa 42 bits para el tiempo y divide los bits de nodo en worker y proceso. Instagram generaba IDs dentro de cada shard de PostgreSQL con un ID de shard de 13 bits, lo que ayuda cuando haces sharding de una base de datos. Sonyflake cuenta en unidades de 10 ms para durar 174 años, a 256 IDs por 10 ms y por instancia.

Asignar worker IDs

Snowflake no necesita coordinación por ID, solo por proceso: dos generadores activos con el mismo worker ID pueden emitir el mismo ID.

  1. Configuración estática. Cada host recibe un WORKER_ID. Va bien para una flota fija; un archivo de configuración copiado crea duplicados.
  2. Ordinales de StatefulSet. Kubernetes numera los pods de un StatefulSet desde 0 y etiqueta cada uno con apps.kubernetes.io/pod-index (estable desde v1.32), que la downward API transmite. Los ordinales son únicos solo dentro de un StatefulSet en un clúster, y borrar un pod a la fuerza puede dejar dos pods con el mismo ordinal.
  3. Leases en etcd o ZooKeeper. Al arrancar, la instancia reclama la clave libre más baja, como /idgen/workers/17, con una transacción que solo tiene éxito si la clave no existe, sobre un lease de 10 segundos renovado cada 3 segundos. Si la instancia muere, la clave caduca. El Snowflake de Twitter de 2010 usaba un nodo efímero de ZooKeeper y se negaba a arrancar si su reloj se desviaba más de 10 segundos de la media de sus pares.
env:
  - name: WORKER_ID
    valueFrom:
      fieldRef:
        fieldPath: metadata.labels['apps.kubernetes.io/pod-index']

Los leases necesitan dos reglas. Deja de emitir IDs cuando el último keepalive exitoso sea más antiguo que el TTL concedido menos un margen, para que una instancia pausada o aislada se detenga antes de que otra pueda reclamar su ID. Y guarda una high-water mark: cada pocos segundos, almacena una marca de tiempo algo por delante del reloj en una clave aparte, nunca emitas IDs más allá de ella y haz que el siguiente titular espere hasta que su reloj la pase. etcd mantiene ambas consistentes con Raft.

Cuando el reloj retrocede

Los demonios NTP suelen ajustar el reloj con slew, haciéndolo correr algo más rápido o más lento, pero lo saltan ante desfases grandes: ntpd salta por encima de 128 ms por defecto, y con el modo de segundo intercalar por defecto de chrony, el kernel retrocede el reloj un segundo por un segundo intercalar insertado. La documentación de chrony aconseja saltar solo en el arranque:

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 un salto que se cuela, elige una regla:

Estrategia Qué hace el generador Coste
Fail Rechaza peticiones hasta que el reloj pase last_ms Errores mientras dure el salto
Wait Duerme hasta que el reloj pase last_ms Bien para unos pocos ms; los saltos largos bloquean a todos los llamadores
Reloj lógico Sigue contando desde last_ms Nunca bloquea; las marcas de tiempo van por delante del tiempo real

El original de Twitter fallaba. Esperar unos milisegundos y luego fallar con una alerta es un valor por defecto sensato. Medir con CLOCK_MONOTONIC, que nunca retrocede, protege a un proceso en ejecución; la high-water mark cubre los reinicios.

UUIDv7 e índices B-tree

La RFC 9562, publicada en mayo de 2024, reemplazó a la RFC 4122 y definió UUIDv7: una marca de tiempo Unix de 48 bits en milisegundos, 4 bits de versión, 2 bits de variante y 74 bits para aleatoriedad, o para un contador cuando necesitas orden estricto dentro de un milisegundo. La marca de tiempo dura hasta el año 10889.

La RFC advierte que los UUID sin orden temporal tienen mala localidad de índice, con efectos sobre los B-trees que pueden ser dramáticos. En PostgreSQL, las nuevas claves v7 van a la página hoja más a la derecha, y los splits allí dejan las páginas llenas al 90 %, el fillfactor por defecto de los B-tree. Las claves v4 aleatorias golpean cualquier página: todo el índice se convierte en el working set, las páginas llenas se dividen y, tras cada checkpoint, el primer cambio en cada página escribe la página completa en el WAL. InnoDB de MySQL además copia la clave primaria en cada índice secundario, así que una clave de 16 bytes los hace crecer a todos.

PostgreSQL 18 añadió uuidv7() en septiembre de 2025. Guarda los UUID como binario de 16 bytes, no como 36 caracteres de texto. Y v7 revela su hora de creación; la RFC recomienda v4 donde un ID tenga un papel de seguridad.

ULID

La especificación ULID tiene la misma forma: una marca de tiempo de 48 bits en milisegundos y 80 bits aleatorios. Su texto de 26 caracteres en base32 de Crockford, sin I, L, O ni U, se ordena correctamente como string y se lee bien en URLs. El modo monotonic suma 1 dentro de un milisegundo, lo que mantiene el orden pero hace adivinable el siguiente valor. ULID es una especificación comunitaria; para claves de base de datos, UUIDv7 ofrece lo mismo con soporte nativo.

¿Qué probabilidad hay de una colisión?

Para n IDs con b bits aleatorios, la probabilidad de alguna colisión es aproximadamente n² / 2^(b+1). Los IDs con prefijo de tiempo solo pueden colisionar dentro de un mismo milisegundo, así que importa la tasa, no el total. A 1 millón de IDs por segundo durante un año, repartidos uniformemente:

Esquema Bits aleatorios Puede colisionar con Probabilidad en un año
UUIDv4 122 Cualquier otro ID Aproximadamente 1 entre 10 mil millones
ULID 80 IDs del mismo milisegundo Aproximadamente 1 entre 77 millones
UUIDv7 74 IDs del mismo milisegundo Aproximadamente 1 entre 1,2 millones
Snowflake 0 Mismo worker ID y milisegundo Cero, mientras los worker IDs sean únicos y los relojes avancen

Los esquemas aleatorios fallan con una probabilidad pequeña y calculable. Snowflake solo falla por la operación, pero entonces con certeza. Mantén una restricción unique en ambos casos.

El diagrama del generador de IDs estilo Snowflake

Diagrama de arquitectura. Los servicios Payment y Order llaman a NextId() en un servicio de IDs con tres instancias (worker IDs 0 a 2), que alquilan IDs a etcd y sincronizan la hora con NTP. El servicio Order inserta filas en Postgres. Una barra divide 64 bits en 1, 41, 10, 12.

18 componentes · 5 conexiones

Abrir en Archboard
Contenido del 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
Un servicio de IDs estilo Snowflake: tres instancias alquilan worker IDs a etcd, sincronizan la hora con NTP y responden NextId() desde memoria. Worker IDs únicos y un reloj que nunca retrocede mantienen único cada ID de 64 bits; la barra muestra la distribución de bits.

Cómo leerlo:

  • Los clientes llaman a NextId() en cualquiera de las tres instancias, que responden desde memoria: el último milisegundo y un contador de 12 bits.
  • etcd guarda cada worker ID en un lease de 10 segundos, además de las marcas de tiempo guardadas.
  • NTP mantiene los relojes cerca (arista discontinua); chrony solo salta en el arranque.
  • El servicio Order escribe filas en Postgres, donde los IDs crecientes caen en la página B-tree más a la derecha.
  • La barra muestra los 64 bits a escala; el bloque Markdown lista los ajustes.

Dibujar con claridad la generación de IDs

  • Dibuja la distribución de bits a escala. Muestra el presupuesto de un vistazo.
  • Muestra de dónde vienen los worker IDs, con el TTL del lease. Los duplicados empiezan ahí.
  • Dibuja el reloj como una dependencia. El generador solo es tan correcto como su reloj.
  • Escribe las reglas de fallo en el lienzo: reloj retrocedido, lease perdido, reinicio. El ejemplo de diagrama de diseño de sistemas pone números junto a las cajas de la misma manera.

Constrúyelo en Archboard

  1. Inicia sesión con Google o GitHub y crea un diagrama.
  2. Añade un Marco del pack Básico para el servicio de IDs, con tres nodos Contenedor ámbar del pack C4 Model dentro: id-gen-0 a id-gen-2, un worker ID cada uno.
  3. Añade dos nodos Contenedor a la izquierda para los servicios Payment y Order, y un nodo Postgres del pack Datos debajo.
  4. A la derecha, añade un Contenedor violeta para etcd, un Servidor azul del pack Red para NTP y un bloque Markdown del pack Básico con los ajustes.
  5. Conéctalos: NextId() desde ambos clientes al marco, INSERT a Postgres, el lease a etcd y una arista discontinua de sincronización de hora desde NTP.
  6. Debajo, añade cuatro nodos rectangulares Elipse del pack Básico, con tamaño 1 : 41 : 10 : 12, para la distribución de bits, y algunas notas Texto de Básico.

Un diagrama de flujo Mermaid del diseño se convierte en un primer borrador editable con el conversor de Mermaid. El ejemplo usa 14 nodos de diagrama, dentro del plan Free.

Preguntas frecuentes

¿Qué es un Snowflake ID? Un ID de 64 bits construido a partir de una marca de tiempo, un worker ID y una secuencia por milisegundo, introducido por Twitter en 2010. Los generadores funcionan sin hablar entre sí, y los IDs se ordenan aproximadamente por hora de creación.

¿Es UUIDv7 mejor que ULID? Ambos llevan una marca de tiempo de 48 bits en milisegundos y bits aleatorios. UUIDv7 es un estándar del IETF con soporte en bases de datos, como uuidv7() en PostgreSQL 18; ULID tiene una forma de texto más corta. Para claves de base de datos, prefiere UUIDv7.

¿Debo usar UUIDv4 o UUIDv7 como clave primaria? UUIDv7 para la mayoría de las tablas: los valores nuevos se ordenan después de los viejos, así que las inserciones se quedan cerca del borde derecho del índice. Usa UUIDv4 donde un ID no deba revelar su hora de creación.

¿Cuántos IDs por segundo puede producir un generador Snowflake? 4 096 por milisegundo y worker, unos 4,1 millones por segundo, o 4 200 millones entre 1 024 workers. La llamada de red alrededor del generador suele ser el límite real.

¿Qué pasa cuando el reloj retrocede? El generador no debe reutilizar un milisegundo que pudo haber usado. Puede fallar hasta que el reloj se ponga al día, esperar o mantener un reloj lógico que vaya por delante del tiempo real; el original de Twitter fallaba.

Empieza gratis — un puesto, sin tarjeta de crédito.

Artículos relacionados

← Volver al blog

Abre un lienzo en un minuto

Inicia sesión con Google o GitHub. Gratis para un asiento. Mejora el plan cuando se una el equipo.