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¶
- Probe: failure detection por ping random
- Gossip: dissemination epidémica de updates
- 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)¶
- failure-detector-formal-properties — propiedades Chandra-Toueg + FLP que SWIM materializa.
- infection-style-dissemination — modelo Bailey 1975 detrás del gossip.
- incarnation-numbers — Suspect/Alive/Confirm preference rules con versionado.
- ../analysis/swim-paper-vs-camunda-defaults — validación de defaults Camunda contra paper, brechas detectadas (round-robin no expuesto, suspicion timeout fijo).
- ../sources/swim-paper-das-gupta-motivala-2002 — paper canónico.