Consenso distribuido
"El consenso distribuido es el problema de hacer que un grupo de procesos que pueden fallar lleguen a un acuerdo sobre un único valor."
Qué vas a aprender en este capítulo
El consenso es el corazón de los sistemas distribuidos confiables: múltiples nodos deben acordar quién es el líder, cuál es el próximo log entry, o si una transacción debe commitearse. El resultado FLP demuestra que es imposible en un sistema completamente asíncrono — pero en la práctica funciona gracias a supuestos razonables de timing.
3.1 El problema del consenso y FLP
📐 Fundamento
El problema del consenso:
Dado un conjunto de nodos que proponen valores, deben acordar un único valor tal que:
- Terminación: Eventualmente, todos los nodos no fallados deciden.
- Validez: El valor decidido fue propuesto por algún nodo.
- Acuerdo: Todos los nodos no fallados deciden el mismo valor.
Teorema FLP (Fisher, Lynch, Paterson, 1985):
En un sistema distribuido completamente asíncrono (sin garantías de tiempo) con al menos un proceso que puede fallar (crash-stop), es imposible garantizar que el consenso termine.
Implicación práctica: Los algoritmos de consenso reales (Paxos, Raft) asumen modelos parcialmente síncronos — eventualmente los mensajes llegan en tiempo acotado. Esto los hace viables pero no garantizados en el peor caso.
Quórum:
Para tolerar f fallos en un sistema de n nodos, se necesita que el quórum q cumpla:
Con n=5: quórum = 3. Tolera 2 fallos. Si 3+ nodos están vivos y se comunican, el sistema avanza.
¿Por qué quórum mayoritario? Dos quórums cualesquiera siempre se solapan en al menos un nodo. Ese nodo en común garantiza que el segundo quórum "sabe" lo que acordó el primero.
3.2 Raft — el algoritmo de consenso moderno
💡 Intuición
Paxos fue el primer algoritmo de consenso ampliamente usado, pero es notoriamente difícil de entender e implementar correctamente. Diego Ongaro creó Raft en 2014 con el objetivo explícito de ser comprensible.
El insight de Raft: un sistema de consenso puede separarse en tres problemas relativamente independientes: elección de líder, replicación del log, y seguridad.
📐 Fundamento
Roles en Raft:
- Leader: Recibe escrituras de los clientes, replica al resto.
- Follower: Acepta el log del leader y responde a heartbeats.
- Candidate: Cuando no hay heartbeat, intenta convertirse en leader.
Elección de líder:
Estado inicial: todos son followers
Follower: si no recibo heartbeat en timeout aleatorio (150-300ms)...
→ me convierto en Candidate
→ incremento mi term (era 1, ahora 2)
→ me voto a mí mismo
→ envío RequestVote a todos
Otros nodos: si term > mi term y no voté en este term...
→ voto por el candidato
→ actualizo mi term
Si recibo mayoría de votos:
→ me convierto en Leader
→ envío heartbeats inmediatamente para evitar otra elección
Replicación de log:
Cliente → Leader: "registrar pedido #456"
Leader:
1. Agrega entry al log local: {term:2, index:7, comando: "pedido #456"}
2. Envía AppendEntries(entry) a todos los followers
Follower: recibe AppendEntries
→ verifica consistencia del log
→ agrega entry al log local
→ responde OK
Leader: cuando mayoría de followers confirmó:
→ entry está "commiteado"
→ aplica el comando a la máquina de estados
→ responde al cliente: "OK, pedido #456 registrado"
→ notifica a followers que pueden commitear también
Propiedades de seguridad de Raft:
- Log Matching: Si dos logs tienen un entry con el mismo índice y term, entonces todos los entries anteriores son idénticos.
- Leader Completeness: Si un entry está commiteado en un term, estará presente en todos los líderes futuros.
- State Machine Safety: Si un servidor aplica un entry en el índice i, ningún otro servidor aplica un entry diferente en ese índice.
Implementación simplificada:
from enum import Enum
import random, threading, time
class Rol(Enum):
FOLLOWER = 'follower'
CANDIDATE = 'candidate'
LEADER = 'leader'
class NodoRaft:
def __init__(self, nodo_id: int, peers: list):
self.id = nodo_id
self.peers = peers # lista de otros nodos
self.rol = Rol.FOLLOWER
# Estado persistente
self.current_term = 0
self.voted_for = None
self.log = [] # [{term, index, comando}]
# Estado volátil
self.commit_index = 0
self.last_applied = 0
self.leader_id = None
# Solo en leaders
self.next_index = {}
self.match_index = {}
# Timer
self.election_timeout = random.uniform(0.15, 0.30)
self.last_heartbeat = time.time()
def solicitar_voto(self, term, candidate_id, last_log_index, last_log_term):
"""Manejar RequestVote RPC."""
if term < self.current_term:
return {'term': self.current_term, 'vote_granted': False}
if term > self.current_term:
self.current_term = term
self.rol = Rol.FOLLOWER
self.voted_for = None
voto_disponible = (self.voted_for is None or self.voted_for == candidate_id)
log_actualizado = (last_log_term > self._ultimo_term() or
(last_log_term == self._ultimo_term() and
last_log_index >= len(self.log)))
if voto_disponible and log_actualizado:
self.voted_for = candidate_id
return {'term': self.current_term, 'vote_granted': True}
return {'term': self.current_term, 'vote_granted': False}
def _ultimo_term(self):
return self.log[-1]['term'] if self.log else 0
etcd — Raft en producción:
etcd es la base de datos de configuración de Kubernetes. Usa Raft para garantizar que todos los nodos del clúster de Kubernetes tengan la misma vista de la configuración del sistema.
# Ver el estado del clúster etcd (en un nodo de Kubernetes)
etcdctl endpoint status --cluster
# endpoint ID version db size is leader is learner raft term raft index
# etcd-0:2379 8e9e05c52164 3.5.0 25 MB false false 42 12567
# etcd-1:2379 f1b27fbe4749 3.5.0 25 MB true false 42 12567
# etcd-2:2379 9fffb4c23500 3.5.0 25 MB false false 42 12567
3.3 Paxos — conceptual
📐 Fundamento
Paxos (Leslie Lamport, 1989/1998) fue el primer algoritmo de consenso ampliamente estudiado. Hay dos fases:
Fase 1 (Prepare/Promise):
Proposer → todos: "Prepare(n=42)"
"¿Pueden prometerte no aceptar números menores que 42?"
Acceptors: si n > mi mayor prepare previo:
→ "Promise(n=42)" + cualquier valor ya aceptado previo
Fase 2 (Accept/Accepted):
Proposer: tiene quórum de promises →
→ "Accept(n=42, valor='pupusa')" o el valor aceptado más alto
Acceptors: si n >= mi mayor prepare previo:
→ "Accepted(n=42, valor='pupusa')"
Proposer: recibe quórum de Accepted → ¡CONSENSO! valor='pupusa'
¿Por qué Raft ganó popularidad sobre Paxos?
- Paxos original solo resuelve un valor — para un log replicado se necesita "Multi-Paxos", que Lamport describió vagamente.
- Las implementaciones reales de Paxos difieren significativamente del paper.
- Raft fue diseñado desde cero para el caso completo (leader election + log replication) y tiene una especificación completa.
Sistemas que usan Paxos: Google Chubby (coordinación en Spanner), Google Megastore. Sistemas que usan Raft: etcd, CockroachDB, TiKV, Consul.
3.4 Ejercicios
✏️ Ejercicio 3.1 — Quórum
Para cada configuración de clúster, calculá el quórum necesario y la tolerancia a fallos:
a. 3 nodos b. 5 nodos c. 7 nodos d. ¿Por qué se recomienda siempre un número impar de nodos?
Solución
a. 3 nodos: quórum = 2, tolera 1 fallo. b. 5 nodos: quórum = 3, tolera 2 fallos. c. 7 nodos: quórum = 4, tolera 3 fallos.
d. Número impar: Con 4 nodos, el quórum también sería 3 (tolera 1 fallo) — igual que con 3 nodos pero con un nodo más. El nodo extra no agrega tolerancia adicional a fallos. Con 6 nodos, el quórum sería 4 (tolera 2 fallos) — igual que con 5 nodos. El nodo par extra no aporta beneficio de tolerancia y sí aumenta la latencia (hay que esperar más respuestas). Por esto, siempre se usan configuraciones impares (3, 5, 7...).
✏️ Ejercicio 3.2 — Elección de líder en Raft
En un clúster de 5 nodos con Raft, el líder falla. Describí el proceso de elección paso a paso:
a. ¿Qué detecta que el líder falló? b. ¿Cómo se convierte un nodo en candidato? c. ¿Qué pasa si dos nodos se convierten en candidatos simultáneamente? d. ¿Qué garantiza que el nuevo líder tenga todos los entries commiteados?
Solución
a. Los followers tienen un election timeout aleatorio (150-300ms). Al no recibir heartbeat del líder durante ese tiempo, asumen que el líder falló.
b. El follower con el timeout más corto: (1) incrementa su term (ej: de 5 a 6), (2) se vota a sí mismo, (3) envía RequestVote a los otros 4 nodos.
c. Split vote: Si dos nodos se convierten en candidatos al mismo tiempo (timeouts similares), cada uno obtiene 2 votos (incluyendo el propio) — ninguno alcanza el quórum de 3. Se espera el timeout de la siguiente elección (que al ser aleatorio, es poco probable que vuelva a empatar) y se intenta de nuevo con term incrementado.
d. Restricción de completitud del log: Un candidato solo recibe voto de un nodo si su log está "al menos tan actualizado" como el del votante (comparan last_log_term y last_log_index). Si un entry fue commiteado, fue aceptado por quórum (≥3 nodos). Esos 3 nodos votarán solo por candidatos que tengan ese entry → el nuevo líder siempre lo tendrá.
3.5 Para profundizar
- Ongaro, "Consensus: Bridging Theory and Practice" (tesis doctoral de Raft, 2014) — gratis en raft.github.io.
- "The Raft Paper" — In Search of an Understandable Consensus Algorithm (USENIX 2014).
- Kleppmann, DDIA, cap. 9.
- Siguiente: Tolerancia a fallos.
Definiciones nuevas: consenso distribuido, FLP, terminación, validez, acuerdo, quórum, Raft, leader election, term, AppendEntries, commit, Log Matching Property, Paxos, Prepare/Promise/Accept, split vote, election timeout, heartbeat.