Saltar a contenido

Partitioning en Zeebe

Resumen: Zeebe particiona los datos en persistent streams (shards), cada uno con su propio log Raft y estado RocksDB. Las keys codifican el partition ID en los 13 bits superiores (maximo 8192 particiones) y un key local de 51 bits. La particion 1 es especial (DEPLOYMENT_PARTITION) y recibe todas las deployments. El routing de comandos usa round-robin, correlation keys, o partition fija segun el tipo. Cada particion tiene un leader y N-1 followers, con replication factor impar recomendado.


Modelo fundamental

Una particion en Zeebe es un persistent stream — un shard independiente con: - Su propio log de Raft (append-only, replicado). - Su propia base de datos RocksDB (estado materializado). - Su propio StreamProcessor (single-threaded, ver concepts/stream-processing). - Su propia instancia de Raft (leader election y replicacion, ver concepts/raft-consensus).

Las particiones son completamente independientes entre si: no comparten estado, no coordinan entre ellas (con la excepcion del deployment distribution). Esto permite escalabilidad horizontal lineal hasta cierto punto.


Key encoding

Zeebe codifica el partition ID directamente en cada key de recurso usando una estructura de bits fija:

|-- 13 bits --|---------- 51 bits ----------|
| partition   |        local key             |
|   ID        |     (monotonic counter)      |

Detalles

  • 13 bits para partition ID: permite un maximo de 8192 particiones (2^13 = 8192). En la practica, pocos deployments usan mas de 32-64.
  • 51 bits para local key: permite 2^51 (~2.25 quadrillion) keys por particion. El key local es un contador monotonicamente creciente, generado por la column family KEY en RocksDB.
  • Total: 64 bits (un long en Java).

Operaciones sobre keys

// Extraer partition ID
partitionId = key >> 51  // shift right 51 bits

// Extraer local key
localKey = key & ((1L << 51) - 1)  // mask lower 51 bits

// Construir key completo
key = ((long) partitionId << 51) | localKey

Implicaciones

  • Dado un key, se puede determinar inmediatamente a que particion pertenece sin consultar ninguna tabla de routing.
  • Las keys son globalmente unicas sin coordinacion entre particiones.
  • El limite de 8192 particiones es generoso para la mayoria de casos de uso.
  • Para una implementacion simplificada, este esquema de encoding es altamente recomendable: es simple, eficiente, y elimina la necesidad de un servicio de routing externo.

DEPLOYMENT_PARTITION

La particion 1 tiene un rol especial como DEPLOYMENT_PARTITION:

  1. Todas las deployments se envian a la particion 1, independientemente del routing normal.
  2. La particion 1 valida el deployment (parseo BPMN/DMN, validacion de schemas).
  3. Si es valido, la particion 1 distribuye el deployment a todas las demas particiones mediante un protocolo de distribucion interno.
  4. Cada particion escribe el deployment en su propio estado RocksDB.

Por que es necesario

  • La validacion de deployments es costosa y debe hacerse una sola vez.
  • Todas las particiones necesitan conocer todas las definiciones de proceso para poder crear instancias.
  • Sin una particion central, habria que coordinar la validacion entre multiples nodos (complejo y propenso a errores).

Riesgo

La particion 1 es un single point of bottleneck para deployments. Si esta sobrecargada, los deployments se retrasan. Sin embargo, los deployments son infrecuentes comparados con la ejecucion de instancias, asi que en la practica esto rara vez es un problema.


Routing de comandos

Diferentes tipos de comandos se routean a diferentes particiones:

Round-robin: CreateProcessInstance

Las solicitudes de creacion de instancias de proceso se distribuyen round-robin entre todas las particiones. Esto balancea la carga de forma equitativa.

El gateway mantiene un contador simple:

partitionId = (counter++ % numPartitions) + 1

Correlation key: Messages

Los mensajes se routean a una particion especifica basada en el hash de su correlation key:

partitionId = (hash(correlationKey) % numPartitions) + 1

Esto garantiza que un mensaje y la instancia de proceso que lo espera estan en la misma particion, permitiendo matching local sin coordinacion entre particiones.

Particion fija: Timers

Los timers se evaluan en la particion donde se creo la instancia del proceso. No se redistribuyen. Esto simplifica la logica de timer evaluation: cada particion solo necesita evaluar sus propios timers.

Particion 1: Deployments

Como se describio arriba, todas las deployments van a la particion 1.

Implicaciones para implementacion

El routing es stateless en el gateway: solo necesita saber cuantas particiones hay y un contador para round-robin. No mantiene ninguna tabla de routing compleja.


Leader-Follower por particion

Cada particion tiene su propio grupo de Raft con: - Un leader: procesa todos los commands, replica al log. - N-1 followers: replican el log, pueden servir para read queries (no implementado por defecto en Zeebe).

Distribucion de leaders

Los leaders se distribuyen round-robin entre los brokers disponibles para balancear la carga:

Ejemplo con 3 brokers y 6 particiones:

Broker 0: leader(P1), leader(P4), follower(P2), follower(P5)
Broker 1: leader(P2), leader(P5), follower(P3), follower(P6)
Broker 2: leader(P3), leader(P6), follower(P1), follower(P4)

Esta distribucion se recalcula automaticamente cuando un broker se une o abandona el cluster.


Replication factor

El replication factor determina cuantas copias de cada particion existen:

  • Replication factor 1: sin replicacion. Si el broker muere, se pierde la particion. Solo para desarrollo.
  • Replication factor 3: tolerancia a 1 fallo. Recomendado para produccion.
  • Replication factor 5: tolerancia a 2 fallos. Para workloads criticos.

Por que impar

El replication factor debe ser impar porque el quorum de Raft requiere (N/2) + 1 nodos: - RF=3: quorum=2, tolera 1 fallo. - RF=4: quorum=3, tolera 1 fallo (igual que RF=3, pero con un nodo extra desperdiciado). - RF=5: quorum=3, tolera 2 fallos.

Un replication factor par desperdicia un nodo sin mejorar la tolerancia a fallos.


Recursos por particion

Cada particion consume aproximadamente:

  • ~2 threads: uno para el StreamProcessor/Raft y otro para I/O asincrono.
  • 1 instancia de RocksDB: con todas sus column families (142+, ver concepts/rocksdb-state-store).
  • Memoria para el log buffer: records pendientes de procesamiento.
  • Disco para el log: segmentos de log de Raft.
  • Disco para snapshots: checkpoints periodicos de RocksDB.

Sizing

La formula rough para capacity planning:

threads_totales = particiones * 2 + overhead_sistema
memoria_por_particion ≈ 256MB - 1GB (dependiendo del workload)

Para un cluster de 3 brokers con 12 particiones y RF=3: - Cada broker: ~8 particiones (4 como leader, 4 como follower) × 2 threads = ~16 threads.


Gossip protocol para membership

Los brokers descubren la topologia del cluster (que brokers existen, que particiones tiene cada uno, quien es leader) mediante un gossip protocol:

  • Cada broker mantiene una tabla de membership local.
  • Periodicamente intercambia informacion con otros brokers.
  • Convergencia eventual: todos los brokers terminan con la misma vista del cluster.
  • No se usa un servicio de descubrimiento externo (como ZooKeeper o etcd para metadata).

El gossip protocol es solo para discovery. La consistencia del log y la eleccion de leaders se maneja via Raft, no via gossip.

Para implementacion simplificada: el gossip protocol puede reemplazarse por un registro estatico de nodos (archivo de configuracion) si el cluster no cambia frecuentemente. Solo implementar gossip si se necesita descubrimiento dinamico.


Implicaciones para implementacion simplificada

  1. Key encoding de 64 bits: adoptar tal cual. Es elegante, eficiente, y probado.
  2. DEPLOYMENT_PARTITION: simplifica enormemente la logica de deployment. Mantener este patron.
  3. Round-robin para instancias: la estrategia mas simple y efectiva para balancear carga.
  4. Correlation key routing: esencial para message correlation correcta. Sin esto, los mensajes no encontrarian sus destinatarios.
  5. RF=3 como default: suficiente para la mayoria de casos. No implementar RF > 3 hasta que haya demanda.
  6. Gossip es opcional: para un MVP, una lista estatica de nodos es suficiente.
  7. Empezar con pocas particiones (3-6) y escalar horizontalmente segun necesidad.