⏱️ Lectura: 16 min
Cuando un nodo de etcd se cae a mitad de una escritura y Kubernetes sigue funcionando sin corromper su estado, hay un algoritmo con nombre propio operando por detrás: Raft. Sin él, el control plane de Kubernetes, CockroachDB, Consul y Vault no podrían garantizar que todos los nodos vean exactamente los mismos datos después de una caída.
📑 En este artículo
- TL;DR
- Qué es el algoritmo Raft y por qué importa
- Cómo funciona Raft por dentro
- Ejemplos prácticos: Raft en código
- Cómo empezar: montar un clúster etcd de 3 nodos
- Casos de uso reales
- Errores comunes y buenas prácticas
- Comparativa con alternativas
- Profundizando: detalles avanzados
- Preguntas frecuentes
- Referencias
El algoritmo Raft se publicó en 2014, cuando Diego Ongaro y John Ousterhout, de Stanford, presentaron un paper con un objetivo explícito: lograr las mismas garantías de tolerancia a fallos que Paxos, pero de forma que un ingeniero pudiera entenderlo sin un doctorado en sistemas distribuidos. Ese objetivo de comprensibilidad es la razón por la que hoy Raft, y no Paxos, es el consenso detrás de la mayoría de las bases de datos distribuidas nuevas.
TL;DR
- Vas a entender por qué un clúster necesita elegir un líder único antes de aceptar escrituras.
- Vas a poder distinguir los tres estados de un nodo Raft: follower, candidate y leader.
- Vas a aprender cómo se replica una entrada de log hasta que la mayoría la confirma (commit).
- Vas a poder montar un clúster etcd de 3 nodos y verificar cuál es el líder con etcdctl.
- Vas a poder programar un nodo Raft en Go con la librería hashicorp/raft.
- Vas a entender la diferencia entre Raft, Paxos, Zab y 2PC, y cuándo conviene cada uno.
- Vas a identificar los errores más comunes al desplegar clústeres Raft en producción.
Qué es el algoritmo Raft y por qué importa
Raft es un algoritmo de consenso distribuido: un protocolo que permite a un grupo de máquinas (un clúster) ponerse de acuerdo sobre una secuencia de valores, aunque algunas de esas máquinas fallen o la red se particione. Ponerse de acuerdo significa algo concreto: todos los nodos que sobreviven terminan con el mismo log de operaciones, en el mismo orden, incluso si el líder actual se cae en medio de una escritura.
Este problema aparece en cualquier sistema que necesite replicación con consistencia fuerte: una base de datos distribuida que no puede permitirse que dos nodos crean que escribieron valores distintos para la misma clave, o un almacén de configuración como etcd, donde Kubernetes guarda el estado de cada pod, deployment y secret del clúster. Si ese almacén se divide en dos versiones de la verdad (lo que se conoce como split brain), el clúster entero queda en un estado inconsistente.
Antes de Raft, el algoritmo de referencia era Paxos, descrito por Leslie Lamport en 1989. Paxos es correcto y está profundamente estudiado, pero su propia comunidad reconoce que es difícil de implementar sin errores sutiles: el paper original es célebre por ser casi ilegible, y la mayoría de las implementaciones en producción divergen del algoritmo puro para poder razonar sobre él. Raft resuelve el mismo problema descomponiéndolo en tres subproblemas independientes: elección de líder, replicación de logs y seguridad, cada uno con reglas simples de seguir.
Cómo funciona Raft por dentro
Los tres estados de un nodo
Cada nodo de un clúster Raft está siempre en uno de tres estados: follower (seguidor), candidate (candidato) o leader (líder). Al arrancar, todos los nodos empiezan como followers. Un follower es pasivo: solo responde a las peticiones que le manda el líder o un candidato, nunca inicia nada por su cuenta.
Si un follower no recibe noticias del líder durante un tiempo (el election timeout, elegido al azar para cada nodo), asume que el líder murió o quedó incomunicado y se convierte en candidato: incrementa su term (un contador de época que nunca retrocede) y pide el voto del resto del clúster.
stateDiagram-v2
[*] --> Follower
Follower --> Candidate: timeout de eleccion
Candidate --> Leader: mayoria de votos
Candidate --> Follower: descubre lider con term mayor
Candidate --> Candidate: split vote, nueva ronda
Leader --> Follower: detecta term mayor en respuesta
Un candidato gana la elección si obtiene el voto de la mayoría de los nodos del clúster, no la mayoría de los que responden: la mayoría del total configurado. En un clúster de 5 nodos hacen falta 3 votos; en uno de 3 nodos, 2. Esa exigencia de mayoría es la que hace que Raft tolere fallos: mientras la mayoría de los nodos esté viva y pueda comunicarse, el clúster puede elegir un líder y seguir aceptando escrituras.
El truco de la aleatoriedad en el timeout evita que dos followers se conviertan en candidatos exactamente al mismo tiempo y empaten el voto (split vote) una y otra vez. Si ocurre un empate, cada candidato espera un nuevo timeout aleatorio y reintenta.
Replicación de logs: cómo se confirma una escritura
Una vez que hay un líder, todas las escrituras del clúster pasan por él. El cliente envía la operación al líder, el líder la agrega a su log local como una entrada nueva (todavía no confirmada) y la envía en paralelo a todos los followers mediante mensajes AppendEntries.
sequenceDiagram
participant L as Lider
participant F1 as Seguidor 1
participant F2 as Seguidor 2
L->>F1: AppendEntries(log)
L->>F2: AppendEntries(log)
F1-->>L: ack
F2-->>L: ack
Note over L,F2: mayoria confirmo, entrada committed
Cuando la mayoría de los nodos (líder incluido) confirmó haber escrito esa entrada en su log, el líder la marca como committed y recién ahí aplica la operación a su máquina de estados y responde al cliente. Los followers se enteran de qué entradas están confirmadas en el siguiente AppendEntries (que también funciona como heartbeat cuando no hay escrituras nuevas) y aplican esa entrada a su propia máquina de estados.
Esta secuencia (proponer, replicar, esperar mayoría, confirmar, aplicar) garantiza que ningún dato se considera guardado hasta que sobrevive en más de la mitad del clúster. Si el líder se cae justo después de escribir la entrada pero antes de que la mayoría confirme, esa entrada puede perderse o puede sobrevivir, pero nunca queda en un estado ambiguo: el nuevo líder que se elija ya tiene, por construcción, todas las entradas confirmadas hasta ese momento.
Seguridad: por qué el nuevo líder nunca pierde datos confirmados
Raft garantiza esto con una regla simple en la elección: un nodo solo vota por un candidato si el log de ese candidato está tan actualizado o más que el propio. Un candidato con un log más corto o más viejo que el de la mayoría del clúster no puede ganar una elección. Eso significa que el líder que resulte electo siempre contiene, como mínimo, todas las entradas que ya fueron confirmadas antes de la elección.
Ejemplos prácticos: Raft en código
Para ver cómo se traduce esto a código, el ejemplo más simple es modelar el estado mínimo que necesita cualquier nodo Raft: en qué estado está, qué term tiene y a quién votó.
type NodeState int
const (
Follower NodeState = iota
Candidate
Leader
)
type LogEntry struct {
Term int
Command []byte
}
type RaftNode struct {
state NodeState
currentTerm int
votedFor string
log []LogEntry
commitIndex int
}
Este fragmento no implementa nada todavía: solo define la forma de los datos que cada nodo necesita persistir. currentTerm y votedFor deben grabarse en disco antes de responder a cualquier voto o AppendEntries, porque si el nodo se reinicia y los olvida puede votar dos veces en el mismo term y romper la garantía de mayoría única.
En un caso real casi nadie implementa Raft desde cero: se usa una librería probada. La más usada en Go es hashicorp/raft, la misma que corre adentro de Consul y Vault.
import (
"os"
"time"
"github.com/hashicorp/raft"
raftboltdb "github.com/hashicorp/raft-boltdb"
)
func setupRaft(nodeID, dataDir string, fsm raft.FSM) (*raft.Raft, error) {
config := raft.DefaultConfig()
config.LocalID = raft.ServerID(nodeID)
logStore, err := raftboltdb.NewBoltStore(dataDir + "/raft-log.db")
if err != nil {
return nil, err
}
snapshots, err := raft.NewFileSnapshotStore(dataDir, 2, os.Stderr)
if err != nil {
return nil, err
}
transport, err := raft.NewTCPTransport(
"127.0.0.1:8300", nil, 3, 10*time.Second, os.Stderr,
)
if err != nil {
return nil, err
}
return raft.NewRaft(config, fsm, logStore, logStore, snapshots, transport)
}
Este código arma las cuatro piezas que hashicorp/raft necesita para operar un nodo: un logStore para persistir el log en BoltDB, un snapshots store para compactar el log viejo, un transport TCP para hablar con el resto del clúster y un fsm (Finite State Machine) que el propio desarrollador implementa: el código que aplica cada entrada confirmada al estado real de la aplicación.
Cómo empezar: montar un clúster etcd de 3 nodos
La forma más rápida de ver Raft funcionando sin escribir una línea de Go es levantar un clúster de etcd, que expone su estado de líder directamente por API.
# descargar el binario oficial
ETCD_VER=v3.5.17
curl -L "https://github.com/etcd-io/etcd/releases/download/${ETCD_VER}/etcd-${ETCD_VER}-linux-amd64.tar.gz" -o etcd.tar.gz
tar xzf etcd.tar.gz && cd "etcd-${ETCD_VER}-linux-amd64"
# nodo 1 de 3, cluster estatico en localhost
./etcd --name node1 \
--initial-advertise-peer-urls http://127.0.0.1:2380 \
--listen-peer-urls http://127.0.0.1:2380 \
--listen-client-urls http://127.0.0.1:2379 \
--advertise-client-urls http://127.0.0.1:2379 \
--initial-cluster node1=http://127.0.0.1:2380,node2=http://127.0.0.1:2381,node3=http://127.0.0.1:2382 \
--initial-cluster-state new
Repetís el mismo comando para node2 y node3, cambiando los puertos de peer (2381, 2382) y de cliente. Cuando arrancan los tres, se ejecuta exactamente el proceso de elección descrito arriba: cada uno empieza como follower, uno de ellos gana el timeout aleatorio, pide votos y se convierte en líder.
⚠️ Ojo: un clúster Raft de número par de nodos (4, 6) no gana tolerancia a fallos extra frente al número impar inmediatamente anterior (3, 5): sigue necesitando la misma mayoría absoluta, pero paga más latencia de red por nodo replicado. La recomendación estándar es siempre usar un número impar.
flowchart TD
A["Cliente"] --> B["Nodo lider (etcd)"]
B --> C["Nodo seguidor 1"]
B --> D["Nodo seguidor 2"]
C --> E[("Log replicado")]
D --> F[("Log replicado")]
subgraph "Cluster Raft"
B
C
D
end
Verificar cuál es el líder
Con el clúster arriba, etcdctl permite confirmar en vivo qué nodo ganó la elección:
etcdctl --endpoints=http://127.0.0.1:2379,http://127.0.0.1:2379,http://127.0.0.1:2379 \
endpoint status --cluster --write-out=table
La tabla que devuelve incluye una columna IS LEADER: solo un endpoint la tiene en true. Si matás el proceso de ese nodo y volvés a correr el mismo comando contra los dos nodos restantes, vas a ver que en poco tiempo otro nodo pasa a true: esa es la elección de líder ocurriendo en tiempo real.
Casos de uso reales
Raft no es un ejercicio académico: sostiene el estado de sistemas que corren en producción a gran escala.
- etcd, el almacén clave-valor que usa Kubernetes como base de datos de su control plane: cada objeto (Pod, Deployment, Secret) que ves con
kubectl getvive en un log Raft replicado entre los nodos de etcd. - HashiCorp Consul y Vault, que usan
hashicorp/raftpara replicar el catálogo de servicios y los secretos entre nodos. - CockroachDB, que particiona sus datos en rangos y le asigna un grupo Raft independiente a cada rango, de forma que distintas partes de la base de datos puedan tener líderes distintos en paralelo.
- TiKV, la capa de almacenamiento de TiDB, con un diseño de rangos y grupos Raft muy similar al de CockroachDB.
Errores comunes y buenas prácticas
El error más frecuente al desplegar un clúster Raft en producción es subestimar el impacto de la latencia de disco en el election timeout. Cada voto y cada AppendEntries necesitan persistirse antes de responder (fsync), así que si el disco es lento, el heartbeat del líder puede tardar más que el timeout de los followers y disparar elecciones innecesarias todo el tiempo. La recomendación de etcd es correr sobre SSD y monitorear la latencia de fsync del log.
Otro error común es confundir mayoría de nodos configurados con mayoría de nodos que responden: Raft siempre calcula la mayoría sobre el tamaño total del clúster configurado, no sobre los nodos que están vivos en ese momento. Si un clúster de 5 nodos pierde 3, no importa que los 2 restantes se pongan de acuerdo entre sí: no forman mayoría y el clúster deja de aceptar escrituras hasta que vuelva a haber quórum. Es una decisión de diseño deliberada: preferir dejar de escribir antes que arriesgarse a una inconsistencia.
💡 Tip: las implementaciones modernas de Raft (incluida la de etcd) agregan una fase de pre-vote antes de la elección real: un nodo que perdió contacto con el líder consulta primero si ganaría la elección sin incrementar todavía su term, evitando que un nodo aislado de la red (pero vivo) fuerce elecciones innecesarias apenas se reconecta.
Cambiar la membresía de un clúster (agregar o quitar un nodo) en caliente es otro punto delicado: hacerlo de forma ingenua, reemplazando la configuración vieja por la nueva de una sola vez, puede generar una ventana donde existan dos mayorías simultáneas y dos líderes a la vez. Raft resuelve esto con un mecanismo de configuración conjunta (joint consensus), donde el clúster opera brevemente bajo la unión de la configuración vieja y la nueva antes de conmutar del todo.
Comparativa con alternativas
| Algoritmo | Usado en | Ventaja principal | Limitación |
|---|---|---|---|
| Raft | etcd, Consul, CockroachDB, TiKV | Diseñado para ser comprensible e implementable sin ambigüedad | Requiere mayoría viva; no funciona con clústeres particionados a la mitad |
| Paxos (multi-Paxos) | Google Chubby, variantes en Spanner | Décadas de estudio formal, muy flexible en variantes | El paper original es difícil de implementar sin errores sutiles |
| Zab (ZooKeeper Atomic Broadcast) | Apache ZooKeeper | Optimizado para el patrón líder único con broadcast ordenado | Acoplado al diseño interno de ZooKeeper, poco reutilizable fuera de él |
| Two-Phase Commit (2PC) | Transacciones distribuidas clásicas | Simple de entender para una sola transacción | Bloqueante: si el coordinador se cae, los participantes quedan colgados |
Profundizando: detalles avanzados
Un log Raft no puede crecer para siempre: cada nodo tomaría cada vez más tiempo en reiniciar y reproducir todo el historial. Por eso, cada nodo genera periódicamente un snapshot: una foto completa del estado de su máquina de estados en un punto dado, y descarta las entradas de log anteriores a ese punto. Un follower que se atrasó demasiado no recibe todo el log desde cero: el líder le manda directamente el snapshot más reciente y sigue desde ahí con AppendEntries.
Otro detalle que distingue una implementación de juguete de una lista para producción es el manejo de lecturas de solo lectura. Devolver una lectura directamente desde el estado local del líder sin ninguna verificación adicional es riesgoso: ese nodo podría haber dejado de ser el líder real (por ejemplo, por una partición de red) sin enterarse todavía. Las implementaciones serias resuelven esto con un intercambio de heartbeat con la mayoría antes de responder una lectura (read index) o con lease reads basados en tiempo, donde el líder solo confía en su propio estado durante una ventana corta después del último heartbeat confirmado.
📖 Resumen en Telegram: Ver resumen
Tu próximo paso: levantá los tres nodos de etcd del comando de arriba, matá el proceso que aparece como líder y confirmá con etcdctl endpoint status --cluster --write-out=table que otro nodo toma su lugar en menos de un segundo.
Preguntas frecuentes
¿Raft y Paxos son compatibles entre sí?
No directamente: son protocolos distintos con sus propios formatos de mensajes y garantías, aunque ambos resuelven el mismo problema de consenso y ambos toleran fallos de minoría. Un clúster no puede mezclar nodos que hablen Raft con nodos que hablen Paxos.
¿Cuántos nodos hacen falta como mínimo para tolerar un fallo?
Tres. Con 3 nodos, la mayoría es 2, así que el clúster tolera la pérdida de 1 nodo y sigue aceptando escrituras. Con 5 nodos tolera 2 fallos simultáneos, con el costo de replicar cada escritura a más réplicas.
¿Qué pasa si se cae el líder justo cuando está confirmando una escritura?
Depende de si esa entrada ya llegó a la mayoría antes de la caída. Si llegó, el nuevo líder (que por la regla de votación descrita arriba tiene esa entrada en su log) la mantiene y la aplica. Si no llegó, esa escritura se pierde, pero el cliente nunca recibió confirmación de que se guardó, así que puede reintentarla.
¿Raft sirve para bases de datos de una sola instancia?
No tiene sentido ahí: Raft existe para coordinar múltiples copias de los mismos datos. Si solo hay un nodo, no hay nada que replicar ni ningún fallo de red que tolerar.
¿Por qué etcd y no Raft puro para Kubernetes?
etcd es una implementación completa de Raft con una API clave-valor encima (watch, transacciones, TTLs). Kubernetes no implementa Raft: usa etcd como dependencia y confía en las garantías de consistencia que etcd ya resuelve internamente.
Referencias
- raft.github.io: sitio oficial del algoritmo con la visualización interactiva de elección de líder y replicación de logs.
- etcd.io/docs: documentación oficial de etcd, la implementación de Raft que usa Kubernetes.
- github.com/hashicorp/raft: implementación en Go usada por Consul y Vault, con ejemplos de FSM.
- Wikipedia: Raft (algorithm): resumen histórico y comparación con Paxos.
📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.
Imagen destacada: Foto de Kier in Sight Archives en Unsplash
0 Comentarios