Saltar a contenido

Swim Membership Protocol

Zeebe usa SWIM (Scalable Weakly-consistent Infection-style Membership) para detectar fallas y disseminar topología del cluster. Combina probes random (cada 1s, timeout 2s, 3 suspect probes), gossip epidémico (cada 250ms con fanout 2), y sync periódico (cada 10s). Failure detection toma ~5-10 segundos antes de declarar un node muerto, previniendo falsos positivos.

¿Qué es SWIM?

SWIM = Scalable Weakly-consistent Infection-style Membership protocol.

Origen: paper de Das, Gupta, Motivala (Cornell, 2002). Usado por HashiCorp Serf, Consul, Cassandra (early versions), y Atomix (fork de Camunda).

Tres sub-protocolos

  1. Probe: failure detection por ping random
  2. Gossip: dissemination epidémica de updates
  3. Sync: reconciliation periódica de full state

Parámetros oficiales (defaults)

Del código fuente SwimMembershipProtocolConfig.java:

Parámetro Default Función
gossipInterval 250 ms Frecuencia de gossip
gossipFanout 2 Peers por gossip
probeInterval 1 sec Frecuencia de probes
probeTimeout 2 sec Espera respuesta de probe
suspectProbes 3 Probes fallidos antes de suspect
failureTimeout 10 sec Confirma DEAD
syncInterval 10 sec Full state sync
broadcastUpdates false Updates van vía gossip, no broadcast
broadcastDisputes true Disputes SÍ van broadcast
notifySuspect false No avisar al suspect

Failure Detection — el flujo completo

flowchart TD
    Start[Member A quiere detectar falla de Member B]
    Start --> Ping[A --ping--> B<br/>probeInterval = 1 sec]
    Ping --> R{B responde?}
    R -->|sí| Alive1[ALIVE - done]
    R -->|no en probeTimeout 2 sec| Indirect[A pide probe indirecto<br/>a 2 random peers gossipFanout]
    Indirect --> R2{Algún peer contacta B?}
    R2 -->|sí| Alive2[ALIVE]
    R2 -->|no| Suspect[B marcado SUSPECT]
    Suspect --> More[suspectProbes 3 más intentos<br/>durante failureTimeout]
    More --> R3{Todos fallan + 10 sec?}
    R3 -->|sí| Dead[B confirmado DEAD]
    Dead --> Gossip[Gossip B is DEAD<br/>a todo el cluster]

Total de tiempo mínimo para declarar DEAD: ~5-10 segundos.

Esta latencia previene falsos positivos comunes: - GC pauses transitorias - Network blips - Saturación temporal de I/O

Indirect probing — el truco de SWIM

Cuando A no puede contactar B, ANTES de declarar suspect, A pide a peers random ayudar:

flowchart TD
    A[A: Can YOU contact B?] --> CD[C, D random peers]
    CD --> R{Alguno contacta B?}
    R -->|C reaches B| Alive[C reports B is ALIVE<br/>A actualiza estado de B]
    R -->|ninguno| Suspect[B --> SUSPECT]

Esto distingue entre: - B realmente muerto (nadie lo alcanza) - Problema de red entre A y B específicamente (C/D sí pueden)

Sin indirect probing, partitions de red causarían falsos positivos masivos.

Estados de un member

Estado Definición
ALIVE Probes recientes exitosos
SUSPECT Probes fallaron, en período de gracia
DEAD Confirmed muerto después de failureTimeout

Transiciones: - ALIVE → SUSPECT: fail probe + indirect probe - SUSPECT → ALIVE: cualquier evidencia de vida (gossip, probe exitoso) - SUSPECT → DEAD: failureTimeout sin recovery

Gossip — dissemination epidémica

Cada gossipInterval (250 ms), cada nodo elige gossipFanout (2) peers random y envía updates pendientes: - Member states cambiados recientemente - Properties metadata updates

Después de propagation, en promedio toda actualización alcanza el cluster en O(log N) rounds.

Para un cluster de 15 brokers (Intuit) con gossipInterval 250ms y fanout 2: - log2(15) ≈ 4 rounds - 4 × 250 ms = ~1 segundo para propagation total

Sync — full reconciliation

Cada syncInterval (10 sec), un nodo elige un peer random y intercambia full state. Esto: - Repara state divergence acumulada (eventually consistent) - Maneja membership changes que se perdieron en gossip - Backup de safety net

Disputes

Cuando dos nodes tienen información conflictiva sobre un peer (e.g., A dice "B is ALIVE", C dice "B is DEAD"), se genera un dispute.

broadcastDisputes = true (default) significa que disputes se envían a TODOS los peers, no solo gossip. La resolución es vía incarnation numbers (versions del state de cada member): - El state con mayor incarnation gana - El member en disputa puede incrementar su incarnation para "reclaim aliveness"

Threading

swimScheduler = Executors.newSingleThreadScheduledExecutor(...);
eventExecutor = Executors.newSingleThreadExecutor(...);

Single-threaded para SWIM operations Y para dispatch de events. Consistente con el design philosophy de Zeebe (ver concepts/stream-processing).

Discovery Providers

BootstrapDiscoveryProvider

Configuración estática en YAML:

zeebe:
  cluster:
    members: [0, 1, 2]
    initialContactPoints:
      - "broker-0:26502"
      - "broker-1:26502"
      - "broker-2:26502"

Funciona pero requiere actualización manual al cambiar tamaño del cluster.

DynamicDiscoveryProvider

Para Kubernetes y cloud: - DNS lookups - StatefulSet awareness - Auto-update on scale up/down

ClusterMembershipEvent

Eventos propagados a listeners:

public enum Type {
  MEMBER_ADDED,         // Nuevo node en el cluster
  MEMBER_REMOVED,       // Node confirmed DEAD o leaving
  METADATA_CHANGED,     // Properties del member cambiaron
  REACHABILITY_CHANGED  // Cambio en estado ALIVE/SUSPECT/DEAD
}

Componentes que escuchan: - Routing layer (gateway): actualizar backends - Raft partitions: rebalancear leadership - Job streaming: reconectar streams - Health endpoints: reportar estado del cluster

Implicaciones para el MVP

Single-node MVP: skip SWIM completo

Sin cluster, no hay membership. Health endpoint simple es suficiente.

Multi-node MVP: opciones

Opción LOC Pros Cons
Heartbeats simples + DB ~200 Trivial No failure detection robust
Consul/etcd externo ~500 Battle-tested Dependencia externa
HashiCorp memberlist (Go) ~100 (uso) Mismo SWIM, batería incluida Solo Go
scalecube-cluster (Java) ~100 (uso) SWIM en Java Menos mantenido
Implementar SWIM custom ~5000 Control total Mantenimiento alto

Recomendación: HashiCorp memberlist si el MVP es Go, scalecube si Java, Consul si quieres operar separado del runtime.

Parámetros conservadores

Si se implementa membership, los defaults de SWIM son razonables: - Probe 1s + timeout 2s + 3 suspect probes = 5-10 segundos detection - Gossip 250ms × fanout 2 = ~1s propagation para cluster pequeño

NO bajar el failure timeout — falsos positivos causan cascadas (mass failover, etc.).

Trade-off

Camunda invirtió en SWIM para multi-region deployments con failure modes complejos. Para un MVP single-region con < 10 nodos, heartbeats simples (cada 5s) + timeout 30s es suficiente y mucho más simple.

El SWIM ROI aparece cuando: - Cluster > 10 nodes (O(N²) heartbeats vs O(N log N) gossip) - Network particiones son comunes (indirect probing) - Falsos positivos son costosos (regions diferentes)

Cross-refs (paper original + brechas)