Cluster
Wippy se ejecuta como un nodo único por defecto. Habilitar el cluster convierte un conjunto de nodos en un sistema coordinado que comparte membresía, nombres de proceso a nivel de cluster, bloqueos distribuidos y mensajería de grupos de proceso sobre un núcleo de consenso Raft acotado.
El clustering está desactivado hasta que se establece cluster.enabled: true. Todo lo que se describe a continuación es inerte en un nodo único.
Qué ofrece el clustering
- Membresía — cada nodo conoce el conjunto activo de peers a través de gossip, con detección rápida de fallos.
- Nombres de proceso a nivel de cluster — registrar un proceso bajo un nombre que se resuelve desde cualquier nodo, con una elección de garantías de consistencia (ver Naming).
- Bloqueos distribuidos —
system.lockproporciona exclusión mutua a nivel de cluster con liberación automática cuando el titular falla (ver Bloqueos distribuidos). - Grupos de proceso — publicar a todos los miembros de un grupo con nombre en todos los nodos (ver Grupos de proceso).
- Un núcleo de consenso — un cluster Raft pequeño y acotado proporciona el soporte linealizable sobre el que se construyen las primitivas de naming y bloqueo.
Arquitectura: Raft acotado
Hacer que cada nodo sea un peer de Raft escala mal: el líder replica cada entrada de log a cada peer, por lo que el coste del líder inactivo crece con el tamaño del cluster. Wippy limita Raft a un núcleo de tamaño fijo y deja que el resto del cluster viaje por gossip. Cada nodo ocupa uno de tres roles en la configuración Raft:
| Rol | Cantidad (por defecto) | En configuración Raft | Recibe replicación de log | Vota |
|---|---|---|---|---|
| Voter | hasta 5 (max_voters, impar) |
sí | sí | sí |
| Standby | hasta 4 (max_standbys) |
sí | sí | no |
| Client | ilimitado | no | no | no |
- Los voters forman el quórum. Las escrituras se confirman una vez que la mayoría de los voters las reconoce. El número de voters es siempre impar para que la mayoría esté bien definida.
- Los standbys son miembros no votantes mantenidos completamente replicados y en espera. Cuando un voter se va, el líder promueve el standby de mayor rango al slot abierto, de modo que el quórum se recupera sin esperar a que un nodo nuevo se ponga al día.
- Los clients son todos los nodos más allá de
voters + standbys. No están en la configuración Raft en absoluto, por lo que el líder nunca les envía entradas de log. Participan en gossip y enrutan escrituras a un miembro Raft. Esto mantiene la CPU del líder inactivo plana (O(1)) sin importar cuánto crezca el cluster.
Dado que los standbys y los clients pueden absorber el resto de la flota, un cluster de cientos de nodos sigue teniendo un núcleo de consenso de 5 voters. Los límites max_voters/max_standbys son lo que hace "acotado" al diseño.
Selección de voters
El líder ejecuta un reconciliador que, en cada cambio de membresía (con debounce de raft.reconcile_debounce, por defecto 2s), recalcula qué nodos deben ser voters y aplica el conjunto mínimo de operaciones de promoción/degradación. La selección es determinista — cada nodo deriva el mismo orden a partir de la misma vista de gossip — y se guía por tres sugerencias anunciadas en gossip:
raft.eligible— un nodo coneligible: falsenunca es elegido como voter (usar para nodos que se desea que permanezcan como clients o standbys).raft.priority— un valor menor es preferido al llenar slots de voter; los empates se rompen por ID de nodo.failure_domain— los voters se distribuyen primero entre dominios distintos (zonas/racks), de modo que perder un dominio no pueda eliminar la mayoría de voters.
Las operaciones se aplican en un orden que preserva el quórum: primero adiciones y promociones, luego degradaciones, luego eliminaciones.
Membresía y gossip
La membresía usa gossip SWIM (HashiCorp memberlist). Cada nodo enlaza un puerto de gossip (por defecto 7946) e intercambia continuamente pequeños mensajes con los peers para detectar fallos y diseminar metadatos.
Un nodo se une apuntando a uno o más nodos existentes:
cluster:
enabled: true
name: node-2
membership:
join_addrs: "node-1:7946"
El primer nodo no necesita join_addrs — arranca como semilla. Las uniones se reintentan con backoff, y un nodo que se encuentra aislado intenta periódicamente reintegrarse, por lo que un nodo reiniciado con una nueva IP (común en Kubernetes) reconverge rápidamente.
El gossip puede cifrarse con una clave compartida, proporcionada en línea o desde un archivo:
cluster:
membership:
secret_file: /etc/wippy/cluster.key
Los cambios de membresía (NodeJoined, NodeLeft, NodeUpdated) son los eventos que impulsan el bootstrap de Raft, la reconciliación de voters, la sincronización de grupos de proceso y la limpieza automática de nombres pertenecientes a un nodo que se fue.
Bootstrap
El cluster inicial se forma por gossip, no por una lista de peers estática. Esto sigue el patrón bootstrap_expect de Consul/Nomad: se indica a cada nodo inicial cuántos nodos se esperan, y esperan hasta que puedan verse todos entre sí antes de formar quórum juntos.
bootstrap_expect |
Comportamiento |
|---|---|
0 |
Nunca se auto-bootstrapea; solo se une a un cluster que ya existe |
1 |
Nodo único; se bootstrapea inmediatamente con sí mismo como único voter |
N |
Espera hasta que N peers elegibles estén establemente visibles en gossip, luego todos derivan la misma lista de voters y forman quórum |
Para un bootstrap de N nodos, establecer el mismo bootstrap_expect: N en cada nodo inicial. Cada uno anuncia un estado "pre-bootstrap" en gossip; una vez que exactamente N peers de este tipo son visibles durante una breve ventana de estabilidad, cada nodo computa independientemente el conjunto de voters ordenado idéntico y forma el cluster. La ventana de estabilidad previene que una vista parcial y breve active el bootstrap prematuramente.
Los nodos que arrancan más tarde ven un cluster ya formado y omiten el bootstrap por completo — el reconciliador del líder los añade como voters o standbys.
Núcleo de consenso Raft
El núcleo de consenso es sin disco: los logs y snapshots de Raft residen solo en memoria, por lo que no hay directorio de datos que aprovisionar. Al reiniciar, un nodo se reintegra al gossip y repite el estado desde sus peers. Esto elimina deliberadamente los modos de fallo de persistencia-versus-quórum que introduce el Raft en disco, y coincide con el modelo de sistemas de coordinación en memoria (Erlang global, Akka distributed data). El compromiso: la durabilidad del cluster proviene de tener un quórum activo, no del disco — ver Recuperación.
Raft no abre su propio puerto de escucha. Viaja por la malla internodo — las mismas conexiones TCP usadas para el tráfico de relay entre nodos — multiplexado con yamux. El puerto internodo se selecciona automáticamente al arrancar (rango 7950-7959, luego efímero), se fija y se anuncia en gossip para que los peers puedan alcanzarlo. El único puerto que normalmente se expone es el puerto de gossip.
El FSM de Raft contiene el registro de nombres global: bindings activos nombre -> PID más reservas strong en vuelo. Eso es lo que las primitivas de naming que se describen a continuación leen y escriben.
Naming y ámbitos de nombre
Un proceso puede registrarse bajo un nombre y ser alcanzado por ese nombre en lugar de su PID raw. La decisión clave es el ámbito, que selecciona la garantía de consistencia. Hay cuatro ámbitos disponibles, de más barato/débil a más fuerte:
| Ámbito | Respaldado por | Visibilidad | Garantía |
|---|---|---|---|
| Local | mapa por nodo | solo este nodo | Instantáneo, local al nodo; sin coordinación |
| Eventual | gossip CRDT | en todo el cluster | Eventualmente consistente; converge tras rondas de gossip |
| Consistent | Raft | en todo el cluster | Escrituras linealizables; singleton único en el cluster |
| Strong | Raft + ack de todos los nodos | en todo el cluster | Consistente, más reconocimiento de cada nodo activo antes de que el nombre esté activo |
Cómo elegir:
- Local — nombres significativos solo en un nodo (un helper por nodo). Se libera en el momento en que el proceso sale. Coste cero.
- Eventual — nombres de servicio, grupo y presencia en todo el cluster donde una breve ventana de datos obsoletos es aceptable. El conjunto de vínculos se replica por completo en cada nodo, por lo que conviene a un espacio de nombres acotado, no a un nombre por entidad de alta cardinalidad como un proceso por sesión (direccione esos directamente por PID). Cuando dos orígenes registran el mismo nombre, la resolución de conflictos elige un ganador y el proceso perdedor recibe un evento de cancelación (
process.event.CANCEL) con el motivoname revoked: <name>; continúa ejecutándose y puede volver a registrarse. Los nombres se liberan cuando el nodo propietario se va. - Consistent — la elección estándar para singletons con nombre a nivel de cluster. Primero en escribir gana: un segundo registro del mismo nombre a un PID diferente falla con "already exists" y devuelve el propietario actual. Las escrituras necesitan quórum, por lo que se detienen en una partición de minoría. Las lecturas provienen de la réplica Raft local y pueden retrasarse una escritura por unos pocos milisegundos.
- Strong — el pequeño conjunto de singletons del plano de control donde incluso una lectura momentáneamente obsoleta es peligrosa. Además de la garantía Consistent, el registro abre una reserva que cada nodo activo debe reconocer antes de que el nombre sea autoritativo; cualquier nodo que ya tenga un binding conflictivo lo rechaza inmediatamente. Si el plazo vence antes de que todos los nodos confirmen, el registro expira e informa qué nodos faltaban. Esto es la base de los bloqueos distribuidos.
Los nombres se liberan automáticamente: Local al salir el proceso; Consistent y Strong al salir el proceso (mediante monitoreo de topología) y al irse el nodo; Eventual al irse el nodo. La resolución para mensajería (process.send, process.terminate y similares) consulta los planos en orden — Consistent, luego Eventual, luego Local — de modo que un nombre Consistent sombrea uno Eventual con la misma cadena.
La superficie Lua para naming reside en process.registry (register/lookup/unregister con un ámbito) — ver la referencia de Process.
Grupos de proceso
Los grupos de proceso son una facilidad de publish/subscribe y membresía consciente del cluster modelada en pg de Erlang. Un proceso se une a un grupo con nombre; una difusión se reparte por la malla internode a los miembros del grupo en todos los nodos, entregada en modo best-effort. Los grupos son eventualmente consistentes e independientes de Raft — usan la vista de membresía de gossip para elegir los destinatarios — por lo que siguen funcionando incluso mientras el núcleo de consenso está convergiendo.
Operaciones típicas: unirse/abandonar un grupo, difundir a todos los miembros (o solo miembros locales), listar miembros y monitorear un grupo para eventos de unión/salida. Cuando un nuevo nodo se une, los grupos reconcilian su membresía a través de un handshake de sincronización directa, y un bucle de anti-entropía en segundo plano repara cualquier divergencia con el tiempo.
Ver Grupos de Proceso para la API Lua y el tipo de entrada pg.scope para la configuración.
Bloqueos distribuidos
system.lock es exclusión mutua a nivel de cluster construida directamente sobre el ámbito de nombre Strong. Adquirir un bloqueo registra su nombre bajo el ámbito Strong propiedad del proceso que llama; liberarlo lo desregistra. Dado que Strong requiere que cada nodo activo reconozca, puede existir como máximo un titular en todo el cluster.
local ok, err = system.lock.acquire("orders.migration")
if ok then
-- sección crítica: solo un titular en todo el cluster
system.lock.release("orders.migration")
end
Acquire es fail-fast (no bloqueante): si el bloqueo está tomado, devuelve inmediatamente, por lo que los callers añaden su propio retry/backoff. El bloqueo se libera automáticamente si el proceso titular sale o su nodo se va, por lo que la limpieza es automática. Ver la referencia de System para las firmas exactas.
Configuración
La referencia completa clave por clave está en Configuración. Las formas mínimas:
Nodo único (desarrollo):
cluster:
enabled: true
name: dev
raft:
bootstrap_expect: 1
Cluster de votación de tres nodos:
cluster:
enabled: true
name: node-1
failure_domain: us-east-1a
membership:
join_addrs: "node-2:7946,node-3:7946"
secret_file: /etc/wippy/cluster.key
raft:
bootstrap_expect: 3
Cliente solo-gossip (se une para naming/mensajería, nunca ejecuta Raft):
cluster:
enabled: true
name: edge-7
membership:
join_addrs: "node-1:7946,node-2:7946"
raft:
role: client
Puertos
| Propósito | Puerto | Protocolo | Clave de configuración |
|---|---|---|---|
| Gossip (membresía) | 7946 | TCP + UDP | cluster.membership.bind_port |
| Malla internodo (relay + Raft) | automático | TCP | cluster.internode.bind_port |
No hay un puerto Raft separado — Raft se multiplexa sobre la malla internodo. El puerto internodo se asigna automáticamente y se anuncia a través de gossip, por lo que solo el puerto de gossip necesita exposición predecible.
Observabilidad
La salud del cluster se expone a través del endpoint estándar de Prometheus y mediante verificaciones de liveness.
Métricas clave a monitorear:
| Métrica | Significado |
|---|---|
raft_state |
0 = follower, 1 = candidate, 2 = leader |
raft_term |
Término Raft actual; incrementos rápidos señalan rotación de elecciones |
raft_voters / raft_non_voters |
Voters y standbys activos en la configuración |
raft_leader_changes_total |
Transiciones de líder; debería ser casi plano en un cluster saludable |
raft_voter_churn_burst_total |
Ráfagas de operaciones de añadir/eliminar voters; la rotación sostenida es una señal de alerta |
gossip_members{state} |
Recuentos por estado (alive/suspect/dead/left) |
gossip_convergence_seconds |
Tiempo entre eventos de gossip |
Verificaciones de liveness incorporadas (conectadas al endpoint de liveness):
- gossip — saludable mientras la puntuación de salud de gossip del nodo se mantiene baja, con una ventana de gracia al arrancar para que un nodo que se reintegra no sea eliminado prematuramente.
- raft last-contact — un follower votante falla si no ha escuchado de un líder recientemente; un standby tolera un intervalo mucho mayor; los líderes siempre pasan.
- process-group broadcast — falla si un grupo no ve tráfico de difusión durante un período prolongado, detectando un bucle de eventos bloqueado o una partición persistente.
Recuperación y modos de fallo
Dado que el núcleo de consenso no tiene disco, la durabilidad proviene de un quórum activo en lugar del disco. Las reglas prácticas:
- Mantener una mayoría de voters activos. Con 5 voters se toleran 2 fallos simultáneos de voters; los standbys se promueven para llenar los slots abiertos. Caer por debajo de una mayoría detiene las escrituras (nuevos registros Consistent/Strong y adquisiciones de bloqueos) hasta que el quórum regresa. Los nombres existentes y las búsquedas continúan sirviéndose desde réplicas locales.
- El líder desaloja proactivamente un voter que está silente en heartbeat y muerto en gossip, de modo que un voter muerto no bloquee permanentemente el quórum mientras se promueve un standby.
- Para recuperar un cluster que ha perdido quórum, reiniciar los nodos fallidos. Se reintegran al gossip y los miembros supervivientes los incorporan de nuevo. Distribuir voters entre
failure_domains es lo que previene que un fallo en una sola zona cause pérdida de quórum.
Ver también
- Configuración — cada clave de configuración del cluster
- Process — registrar y resolver procesos por nombre
- System —
system.cluster,system.raft,system.node,system.lock - Observabilidad — métricas y endpoints de salud
- Modelo de Procesos — actores, PIDs y mensajería