⏱️ 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
  1. TL;DR
  2. Qué es el algoritmo Raft y por qué importa
  3. Cómo funciona Raft por dentro
    1. Los tres estados de un nodo
    2. Replicación de logs: cómo se confirma una escritura
    3. Seguridad: por qué el nuevo líder nunca pierde datos confirmados
  4. Ejemplos prácticos: Raft en código
  5. Cómo empezar: montar un clúster etcd de 3 nodos
    1. Verificar cuál es el líder
  6. Casos de uso reales
  7. Errores comunes y buenas prácticas
  8. Comparativa con alternativas
  9. Profundizando: detalles avanzados
  10. Preguntas frecuentes
    1. ¿Raft y Paxos son compatibles entre sí?
    2. ¿Cuántos nodos hacen falta como mínimo para tolerar un fallo?
    3. ¿Qué pasa si se cae el líder justo cuando está confirmando una escritura?
    4. ¿Raft sirve para bases de datos de una sola instancia?
    5. ¿Por qué etcd y no Raft puro para Kubernetes?
  11. 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.

Diagrama de los tres estados de un nodo Raft: follower, candidate y leader
Solo puede existir un líder por term; el resto queda en follower. Foto de Vince Fleming en Unsplash

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 get vive en un log Raft replicado entre los nodos de etcd.
  • HashiCorp Consul y Vault, que usan hashicorp/raft para 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.
Clúster etcd de tres nodos replicando su log
etcd usa Raft para que Kubernetes guarde su estado sin perder consistencia. Foto de Outward Bound Costa Rica en Unsplash

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

AlgoritmoUsado enVentaja principalLimitación
Raftetcd, Consul, CockroachDB, TiKVDiseñado para ser comprensible e implementable sin ambigüedadRequiere mayoría viva; no funciona con clústeres particionados a la mitad
Paxos (multi-Paxos)Google Chubby, variantes en SpannerDécadas de estudio formal, muy flexible en variantesEl paper original es difícil de implementar sin errores sutiles
Zab (ZooKeeper Atomic Broadcast)Apache ZooKeeperOptimizado para el patrón líder único con broadcast ordenadoAcoplado al diseño interno de ZooKeeper, poco reutilizable fuera de él
Two-Phase Commit (2PC)Transacciones distribuidas clásicasSimple de entender para una sola transacciónBloqueante: 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

📱 ¿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


Andrés Morales

Desarrollador e investigador en inteligencia artificial. Escribe sobre modelos de lenguaje, frameworks, herramientas para devs y lanzamientos open source. Cubre papers de ML, ecosistema de startups tech y tendencias de programación.

0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.