Saltar al contenido

Entendiendo los algoritmos de consenso en sistemas distribuidos

Guía práctica de algoritmos de consenso: Raft, Paxos y elección de líder, con ejemplos reales de cuándo y por qué los sistemas distribuidos los necesitan.

5 min de lectura
Clúster de nodos de servidor alcanzando consenso mediante elección de líder y rondas de replicación de log

Cuando varios servidores necesitan ponerse de acuerdo sobre un único valor — qué nodo es el líder, cuál es el estado actual de un log replicado, si una transacción debe confirmarse — necesitan un algoritmo de consenso. Sin consenso, los sistemas distribuidos se dividen en particiones que discrepan sobre la realidad, y tus usuarios ven datos obsoletos, operaciones duplicadas o fallos directos.

El consenso es la base de toda base de datos replicada, toda cola distribuida y todo servicio de coordinación. Entenderlo cambia tu forma de razonar sobre los sistemas que construyes encima de ellos.

Por qué el consenso es difícil

En un sistema de un solo servidor hay una única fuente de verdad. En un sistema distribuido, esa garantía desaparece. Los servidores se caen, las redes se particionan, los mensajes llegan desordenados o no llegan. El resultado de imposibilidad FLP demuestra que ningún algoritmo determinista puede garantizar el consenso en un sistema asíncrono donde incluso un solo proceso pueda fallar.

Los algoritmos de consenso prácticos sortean esto usando timeouts y aleatorización — sacrifican garantías teóricas por fiabilidad en el mundo real.

tstypescript
// The fundamental problem consensus solves
interface ConsensusScenario {
  nodes: string[];
  proposedValues: Map<string, string>;
  outcome: 'agree' | 'disagree' | 'stuck';
}
 
// ❌ Without consensus — each node believes something different
const withoutConsensus: ConsensusScenario = {
  nodes: ['A', 'B', 'C'],
  proposedValues: new Map([
    ['A', 'commit'],    // A thinks the transaction committed
    ['B', 'abort'],     // B thinks it aborted
    ['C', 'commit'],    // C agrees with A but B disagrees
  ]),
  outcome: 'disagree',  // Split brain — data corruption
};
 
// ✅ With consensus — all nodes converge on one value
const withConsensus: ConsensusScenario = {
  nodes: ['A', 'B', 'C'],
  proposedValues: new Map([
    ['A', 'commit'],
    ['B', 'commit'],    // B accepted the majority decision
    ['C', 'commit'],
  ]),
  outcome: 'agree',     // All nodes agree — consistency preserved
};

Raft: consenso hecho comprensible

Raft fue diseñado específicamente para ser comprensible. Descompone el consenso en tres subproblemas: elección de líder, replicación de log y seguridad. Cada parte puede razonarse de forma independiente.

tstypescript
// Simplified Raft node state
type NodeRole = 'follower' | 'candidate' | 'leader';
 
interface RaftNode {
  id: string;
  role: NodeRole;
  currentTerm: number;
  votedFor: string | null;
  log: LogEntry[];
  commitIndex: number;
  lastApplied: number;
}
 
interface LogEntry {
  term: number;
  index: number;
  command: string;
}
 
// Leader election state machine
function handleElectionTimeout(node: RaftNode): RaftNode {
  // Follower hasn't heard from leader — become candidate
  return {
    ...node,
    role: 'candidate',
    currentTerm: node.currentTerm + 1,
    votedFor: node.id,  // Vote for self
  };
}
 
function handleVoteRequest(
  node: RaftNode,
  candidateId: string,
  candidateTerm: number,
  candidateLogLength: number
): { granted: boolean; updatedNode: RaftNode } {
  // Reject if candidate's term is stale
  if (candidateTerm < node.currentTerm) {
    return { granted: false, updatedNode: node };
  }
 
  // Grant vote if we haven't voted yet and candidate's log
  // is at least as up-to-date as ours
  const canVote = node.votedFor === null || node.votedFor === candidateId;
  const logUpToDate = candidateLogLength >= node.log.length;
 
  if (canVote && logUpToDate) {
    return {
      granted: true,
      updatedNode: {
        ...node,
        currentTerm: candidateTerm,
        votedFor: candidateId,
        role: 'follower',
      },
    };
  }
 
  return { granted: false, updatedNode: node };
}

El proceso de elección funciona en rondas llamadas términos (terms). Cuando un follower no recibe un heartbeat del líder dentro de su timeout de elección, incrementa su término y solicita votos. Un candidato que recibe votos de una mayoría se convierte en líder. Como solo un líder puede ganar por término, el clúster siempre tiene como máximo un líder.

Replicación de log en la práctica

Una vez elegido un líder, acepta peticiones de clientes y las replica a los followers. Una entrada del log se confirma (commit) solo después de que una mayoría de nodos la haya escrito en sus logs.

tstypescript
// Leader replicating entries to followers
interface AppendEntriesRequest {
  term: number;
  leaderId: string;
  prevLogIndex: number;
  prevLogTerm: number;
  entries: LogEntry[];
  leaderCommit: number;
}
 
interface AppendEntriesResponse {
  term: number;
  success: boolean;
  matchIndex: number;
}
 
function leaderReplicateEntry(
  leader: RaftNode,
  command: string,
  followers: string[]
): void {
  const entry: LogEntry = {
    term: leader.currentTerm,
    index: leader.log.length,
    command,
  };
 
  // Append to leader's own log immediately
  leader.log.push(entry);
 
  // Send to all followers in parallel
  const ackCount = 1; // Leader counts as one acknowledgment
  const majority = Math.floor((followers.length + 1) / 2) + 1;
 
  // When majority acknowledges:
  // leader.commitIndex = entry.index;
  // Apply command to state machine
  // Respond to client with success
 
  console.log(
    `Entry ${entry.index} needs ${majority} acks ` +
    `(have ${ackCount}, waiting for ${majority - ackCount} more)`
  );
}
tstypescript
// ❌ Committing before majority acknowledgment
function unsafeCommit(leader: RaftNode, entry: LogEntry): void {
  leader.commitIndex = entry.index; // Committed with only leader's copy
  applyToStateMachine(entry);       // Could lose data if leader crashes
}
 
// ✅ Committing only after majority acknowledgment
function safeCommit(
  leader: RaftNode,
  entry: LogEntry,
  ackCount: number,
  clusterSize: number
): boolean {
  const majority = Math.floor(clusterSize / 2) + 1;
  if (ackCount >= majority) {
    leader.commitIndex = entry.index;
    applyToStateMachine(entry);
    return true; // Safe to respond to client
  }
  return false;  // Not yet committed — keep waiting
}
 
function applyToStateMachine(entry: LogEntry): void {
  console.log(`Applying: ${entry.command}`);
}

El requisito de mayoría es lo que hace a Raft tolerante a fallos. En un clúster de 5 nodos, 2 nodos pueden fallar y los 3 restantes siguen formando una mayoría. Las entradas confirmadas están garantizadas a sobrevivir porque al menos un nodo de cualquier mayoría futura las tendrá.

Temporización de la elección de líder

Los timeouts de elección deben aleatorizarse para evitar votos divididos — cuando dos candidatos inician elecciones simultáneamente y ninguno obtiene la mayoría.

tstypescript
// Election timeout configuration
interface ElectionConfig {
  minTimeoutMs: number;
  maxTimeoutMs: number;
  heartbeatIntervalMs: number;
}
 
// Rule: heartbeat << election timeout
// This ensures followers hear from the leader
// well before they start unnecessary elections
const config: ElectionConfig = {
  minTimeoutMs: 150,     // Minimum election timeout
  maxTimeoutMs: 300,     // Maximum election timeout
  heartbeatIntervalMs: 50, // Leader sends heartbeats at this interval
};
 
function randomElectionTimeout(config: ElectionConfig): number {
  const range = config.maxTimeoutMs - config.minTimeoutMs;
  return config.minTimeoutMs + Math.floor(Math.random() * range);
}
 
// In production systems like etcd:
// - Heartbeat interval: 100ms
// - Election timeout: 1000-2000ms
// - The 10x ratio gives plenty of margin for network delays

La aleatorización rompe la simetría. Si todos los nodos usaran el mismo timeout, todos se convertirían en candidatos a la vez, dividirían el voto, agotarían el timeout de nuevo y repetirían — un livelock. Los timeouts aleatorizados garantizan que normalmente un nodo agota su timeout primero y gana la elección antes de que otros empiecen.

Cuándo usar consenso y cuándo evitarlo

El consenso es caro — requiere idas y vueltas de red y quórums de mayoría. No todo sistema lo necesita.

ymlyaml
# When consensus IS needed:
leader_election:
  examples: ["database primary selection", "distributed lock service"]
  why: "Only one node should act as leader at a time"
 
replicated_state_machine:
  examples: ["etcd", "ZooKeeper", "CockroachDB"]
  why: "All replicas must apply the same operations in the same order"
 
distributed_transactions:
  examples: ["two-phase commit coordinator", "saga orchestrator"]
  why: "All participants must agree on commit or abort"
 
# When consensus is OVERKILL:
caching:
  alternative: "Eventual consistency — stale cache entries are acceptable"
 
analytics:
  alternative: "Approximate counts — exact agreement not required"
 
event_streaming:
  alternative: "At-least-once delivery — consumers handle duplicates"
 
dns_propagation:
  alternative: "Eventual consistency — TTLs handle staleness"

Si tu sistema puede tolerar desacuerdos temporales entre nodos, no necesitas consenso. La consistencia eventual con resolución de conflictos es más simple, más rápida y más disponible. Reserva el consenso para casos donde el desacuerdo causa violaciones de corrección — como dos nodos que creen ser el primario de la base de datos a la vez.

Conclusiones clave

  1. El consenso resuelve el problema del acuerdo — múltiples nodos convergiendo en un único valor a pesar de caídas y fallos de red
  2. Raft descompone el consenso en elección de líder, replicación de log y seguridad — cada parte es comprensible de forma independiente
  3. Los quórums de mayoría proporcionan tolerancia a fallos — un clúster de 5 nodos sobrevive a 2 fallos manteniendo la consistencia
  4. Los timeouts de elección aleatorizados evitan votos divididos — romper la simetría es esencial para la vivacidad (liveness)
  5. El consenso es caro — resérvalo para casos donde el desacuerdo causa problemas de corrección, no de rendimiento
  6. Los sistemas de producción (etcd, ZooKeeper, CockroachDB) manejan la complejidad — entiende los principios para configurarlos correctamente
Wilfredo Rujel

Wilfredo Rujel

Ingeniero de Software Full Stack

Compartir esta publicaciónX