Consensus e Raft: como nós discordam e chegam a acordo
- ⬜🔄 Modelos de Consistência: strong, eventual, causal, read-your-writes(Sistemas Distribuídos)
Recomendamos completar os pré-requisitos antes de seguir, mas nada te impede de continuar.
Consensus é o problema mais fundamental dos sistemas distribuídos: como N máquinas que podem falhar, mentir sobre o tempo e perder mensagens conseguem concordar em um único valor? A resposta dessa pergunta decide se o seu Kubernetes sobrevive a um reboot, se o seu CockroachDB não perde uma linha durante um failover, e se o seu sistema bancário pode aceitar débitos duplicados. Raft é o algoritmo mais influente dos últimos 15 anos — mais simples que Paxos, provado correto, usado em produção por etcd, Consul, Kubernetes, CockroachDB, TiKV, MongoDB, Redpanda, RabbitMQ (quorum queues) e dezenas de outros.
Este módulo vai do FLP impossibility (o teorema que diz que o impossível é possível se você for esperto) até uma implementação didática em Python. No fim, você vai saber por que 3 ou 5 é um número mágico, por que split-brain não acontece no Raft, e como debugar um cluster etcd quando ele trava.
O problema do consenso
Consenso: um conjunto de processos precisa concordar em um único valor, dado que cada processo propõe um valor e alguns podem falhar. Sons simples? Em teoria, é. Na prática — com rede não confiável, timeouts, máquinas que congelam por GC, e relógios que divergem — é o pesadelo que levou 30 anos de pesquisa pra domar.
- Agreement: todos os nós decidem o mesmo valor
- Validity: o valor decidido foi proposto por algum processo
- Termination: todos os processos corretos eventualmente decidem
Parecem óbvias. Mas FLP prova que as três juntas são impossíveis num modelo assíncrono com falhas. Algo tem que ceder.
FLP Impossibility (1985): o teorema que mudou tudo
Fischer, Lynch e Paterson provaram em 1985 o resultado mais importante de sistemas distribuídos:
A intuição: sem limite superior no tempo de mensagens, você não consegue distinguir um processo morto de um processo lento. Se você espera, pode esperar pra sempre. Se não espera, pode decidir sem contar com ele e quebrar agreement.
Como a prática contornou FLP? Três escapes principais:
| Escape | Ideia | Quem usa |
|---|---|---|
| Modelo parcialmente síncrono | Assume que mensagens eventualmente chegam em tempo limitado. Na prática: timeouts. | Raft, Paxos, PBFT |
| Protocolos randomizados | Usa aleatoriedade pra quebrar simetria — termina com probabilidade 1 (não determinístico). | Ben-Or, Avalanche |
| Failure detectors | Oracle externo que "diz" quem falhou. Chandra-Toueg (1996) mostrou que ◇S é suficiente. | Paxos (implicitamente) |
Raft e Paxos pagam o preço assumindo timeouts. Isso significa: em uma partição de rede muito longa, o cluster pode ficar indisponível (escolhe consistência — CP no CAP). É uma escolha deliberada.
Paxos: o pai (difícil) de todos
Paxos foi proposto por Leslie Lamport em 1989 e publicado formalmente em 1998 ("The Part-Time Parliament"). Ganhou Turing Award, inspirou uma geração de sistemas — e todo mundo acha horrível de entender. Lamport escreveu o paper como uma alegoria sobre um parlamento na Grécia antiga. Foi rejeitado. Reescreveu simples ("Paxos Made Simple", 2001) e ainda assim é difícil.
Intuição em 3 fases:
Cliente ──► PROPONER ──► ACCEPTORS (2F+1 nós, tolera F falhas)
│ │
│ 1. PREPARE(n) ──► todos os acceptors
│ ◄── PROMISE(n, maior valor já aceito)
│
│ 2. ACCEPT(n, v) (v = valor escolhido) ──► todos
│ ◄── ACCEPTED(n, v) de quórum
│
│ 3. LEARN(v) ──► LEARNERS aplicam v
▼
Valor decididoA complexidade real do Paxos vem de Multi-Paxos (sequência de decisões), reconfigurações dinâmicas e otimizações. Ninguém implementa Paxos do paper — todas as implementações usam variantes (Zab, Raft, Viewstamped Replication). Isso levou Ongaro e Ousterhout a criarem o Raft em 2014 com um objetivo explícito: ser entendível.
Raft: Paxos que um humano normal consegue entender
Raft foi projetado em Stanford (2014) com o objetivo explícito de ser didático. Ele decompõe o problema em 3 subproblemas independentes que você pode estudar separadamente:
| Subproblema | O que resolve |
|---|---|
| Leader Election | Como escolher um único leader por term (período de tempo) |
| Log Replication | Como o leader propaga entries para os followers em ordem |
| Safety | Invariantes que garantem que duas máquinas nunca aplicam decisões diferentes |
Estados de um nó Raft:
timeout, start election
┌──────────────────────────────────────────┐
▼ │
┌─────────┐ discovers leader/ ┌──────────────┐
│FOLLOWER │──── higher term ──────────►│ CANDIDATE │
│ │◄───────────────────────────│ │
└─────────┘ └──────────────┘
▲ │
│ │ wins election
│ discovers higher term │ (maioria de votos)
│ ▼
│ ┌──────────────┐
└─────────────────────────────────│ LEADER │
└──────────────┘A vida inteira do cluster é dividida em terms (períodos numerados monotonicamente). Cada term tem no máximo 1 leader. Se um term não eleger leader (split vote), um novo term começa. O número do term é carimbado em cada RPC e entry — e é a chave da safety do Raft.
Leader Election: como escolher o chefe
Quando um follower não recebe heartbeat do leader em electionTimeout(tipicamente 150-300ms, randomizado pra evitar split votes), ele:
- Vira candidate
- Incrementa seu
currentTerm - Vota em si mesmo
- Envia
RequestVote(term, candidateId, lastLogIndex, lastLogTerm)pra todos - Se recebe maioria → vira leader, começa a mandar heartbeats
- Se recebe heartbeat de leader legítimo (term >= seu) → volta a follower
- Se timeout sem decisão → novo term, nova eleição
- Ainda não votou nesse term (ou já votou no mesmo candidato)
- O log do candidato é pelo menos tão atualizadoquanto o seu (lastLogTerm maior, ou lastLogTerm igual e lastLogIndex >= o seu)
Isso garante a Leader Completeness Property: qualquer leader eleito tem todas as entries committed dos terms anteriores. Sem isso, Raft não funciona.
Implementação didática de RequestVote (Python):
# request_vote.py — mostra as regras de voto do Raft
from dataclasses import dataclass
@dataclass
class RaftNode:
id: str
current_term: int = 0
voted_for: str | None = None
log: list = None # lista de (term, command)
def last_log_index(self) -> int:
return len(self.log or []) - 1
def last_log_term(self) -> int:
return self.log[-1][0] if self.log else 0
def handle_request_vote(
self,
candidate_id: str,
candidate_term: int,
candidate_last_idx: int,
candidate_last_term: int,
) -> tuple[int, bool]:
# Rule 1: term antigo — recusa
if candidate_term < self.current_term:
return (self.current_term, False)
# Rule 2: term maior — atualiza e volta a follower
if candidate_term > self.current_term:
self.current_term = candidate_term
self.voted_for = None
# Rule 3: já votou em outro candidato neste term — recusa
if self.voted_for not in (None, candidate_id):
return (self.current_term, False)
# Rule 4: log do candidato é atualizado o suficiente?
# "up-to-date": maior term vence; mesmo term → maior índice vence
my_last_term = self.last_log_term()
my_last_idx = self.last_log_index()
log_ok = (
candidate_last_term > my_last_term
or (candidate_last_term == my_last_term and candidate_last_idx >= my_last_idx)
)
if not log_ok:
return (self.current_term, False)
# Concede voto
self.voted_for = candidate_id
return (self.current_term, True)Log Replication: como o leader replica entries
Uma vez eleito, o leader começa a atender clientes. Cada comando vira uma log entry (term + índice + comando). O protocolo é:
CLIENTE LEADER FOLLOWERS (2+ em cluster de 5) │ │ │ ├─ command ─────────►│ │ │ │ 1. Append ao seu log │ │ │ (term=T, idx=I) │ │ │ │ │ ├─ AppendEntries(T,I,cmd) ►│ (em paralelo pra todos) │ │ ├─ Valida log consistency │ │ ├─ Append ao próprio log │ │◄─── ACK ─────────────────┤ │ │ │ │ │ 2. Recebe ACK de maioria (quórum) │ │ │ │ │ 3. commitIndex = I │ │ │ aplica na state machine │ │ │ │◄─ result ──────────┤ │ │ │ │ │ ├─ heartbeat (com commitIdx)►│ │ │ ├─ followers aplicam entries │ │ │ committed na state machine
A RPC AppendEntries carrega (entre outros):
term: term do leaderprevLogIndex,prevLogTerm: o índice/term da entry imediatamente antes das novas — usado pra log matchingentries[]: lista de entries novasleaderCommit: ocommitIndexatual do leader
nextIndex daquele follower e tenta de novo com entry mais antiga. Eventualmente encontram um ponto comum.Mini-implementação do AppendEntries:
# append_entries.py — log replication do lado do follower
def handle_append_entries(
node: RaftNode,
leader_term: int,
prev_log_idx: int,
prev_log_term: int,
entries: list, # [(term, cmd), ...]
leader_commit: int,
) -> tuple[int, bool]:
# 1. Term antigo — rejeita
if leader_term < node.current_term:
return (node.current_term, False)
# 2. Atualiza term se o leader tem term maior
if leader_term > node.current_term:
node.current_term = leader_term
node.voted_for = None
# 3. Reset election timer (recebeu heartbeat válido)
node.reset_election_timer()
# 4. Log matching check: prevLog precisa existir e bater
log = node.log or []
if prev_log_idx >= 0:
if prev_log_idx >= len(log):
return (node.current_term, False) # log do follower é curto demais
if log[prev_log_idx][0] != prev_log_term:
return (node.current_term, False) # term não bate — conflito
# 5. Append entries (truncando conflitos, se houver)
for i, (term, cmd) in enumerate(entries):
idx = prev_log_idx + 1 + i
if idx < len(log) and log[idx][0] != term:
# conflito: trunca tudo a partir daqui
log = log[:idx]
if idx >= len(log):
log.append((term, cmd))
node.log = log
# 6. Avança commitIndex até o min(leaderCommit, último índice local)
if leader_commit > node.commit_index:
node.commit_index = min(leader_commit, len(log) - 1)
node.apply_committed_entries()
return (node.current_term, True)Safety: as 5 propriedades que Raft garante
| Propriedade | O que garante |
|---|---|
| Election Safety | No máximo 1 leader por term (garantido pela regra de voto único) |
| Leader Append-Only | Leader nunca sobrescreve ou deleta entries do próprio log — só adiciona |
| Log Matching | Se dois logs têm uma entry com mesmo (term, index), eles são idênticos até esse ponto |
| Leader Completeness | Se uma entry foi committed em term T, ela aparece no log de todos os leaders futuros |
| State Machine Safety | Se um nó aplicou uma entry em índice I na state machine, nenhum outro nó aplicará uma entry diferente em I |
Essas 5 propriedades são provadas formalmente no paper original e verificadas em TLA+. Elas valem mesmo em cenários patológicos: crashes em rajada, partições assimétricas, reordenação de mensagens, máquinas congelando por minutos.
Quem usa Raft em produção
| Sistema | Uso do Raft | Observações |
|---|---|---|
| etcd | Backend de metadados do Kubernetes (API server state) | Cluster 3 ou 5 nós típico. Se etcd cai, o K8s control plane para. |
| Consul (HashiCorp) | Service discovery, KV, leader election | Raft por datacenter; gossip entre DCs. |
| CockroachDB | Raft por range (cada shard tem seu próprio cluster Raft) | Milhares de grupos Raft por cluster — escala horizontal real. |
| TiKV / TiDB | Raft por region (shard) | Mesmo padrão do CockroachDB. |
| MongoDB (4.0+) | Replica set election baseada em Raft | Antes era protocolo próprio; migraram pra Raft por simplicidade. |
| Redpanda | Kafka-compatible usando Raft (não ZooKeeper/KRaft) | Uma partição Kafka = um grupo Raft. |
| RabbitMQ | Quorum queues (substituem mirror queues antigas) | Raft garante no-data-loss mesmo com crash do leader. |
| Kafka (KRaft) | Substituto do ZooKeeper desde 3.3 | Usa Kafka Raft Metadata Mode em vez de ZK. |
Se você usa Kubernetes, você já depende de Raft. Quando o etcd fica lento, seu API server responde 500. Quando o quórum do etcd quebra (2 de 3 nós mortos), o K8s vira read-only e depois morre. Entender Raft = entender o chão onde seu cluster anda.
Decisões reais
📋 Cluster etcd do Kubernetes: 3 ou 5 nós?
3 nós tolera 1 falha — se você perde 1 num reboot planejado e outro falha inesperado, o cluster congela. 5 nós tolera 2 falhas simultâneas e permite rolling upgrade sem janela de risco. O custo adicional (2 VMs) é desprezível contra a dor de recuperar um etcd morto.
Alt: 3 nós para dev/staging — Tolera 1 falha, economia real.
Alt: 7+ nós: não — Cada AppendEntries vai pra mais nós → latência de escrita aumenta, throughput cai. 5 é o sweet spot.
📋 Escolher Raft ou Paxos para um novo sistema
Raft é mais simples de implementar, debugar e explicar. Bibliotecas maduras existem em Go (hashicorp/raft, etcd-io/raft), Rust (openraft, tikv/raft-rs), Java (atomix, copycat). Paxos faz sentido só quando você precisa de otimizações específicas (Fast Paxos, EPaxos pra latência wan, Flexible Paxos pra quóruns assimétricos) e tem time de pesquisa pra manter.
Alt: Paxos (Multi-Paxos / EPaxos) — Quando latência WAN importa muito (Spanner).
Alt: Zab (ZooKeeper) — Se você já tem ZK infra madura.
Alt: Viewstamped Replication — Origem intelectual do Raft. Academicamente interessante, pouco usado em produção.
📋 Aplicação web precisa de 'eleição de líder' simples (ex: cron singleton)
Você não precisa de Raft pra eleger quem roda uma cron todo minuto. Raft é pesado — full state machine + log durável + eleição. Pra leader election leve (cron singleton, rate limiter coordinator), advisory lock no PG ou lease com TTL no Redis resolve com 10 linhas de código.
Alt: Redlock no Redis — Com TTL + renovação. Ver papers do Kleppmann sobre limites.
Alt: etcd lease — Se você já tem etcd (K8s), use lease com TTL.
Alt: Postgres pg_try_advisory_lock — Se você tem PG, literalmente 1 linha.
Operação: o que dói no dia a dia
| Problema | Sintoma | Como diagnosticar |
|---|---|---|
| Split brain (nunca acontece em Raft real) | Dois nós se dizem leader | Verifique terms. Leader novo tem term maior — followers rejeitarão o velho. Se acontece, há bug na implementação. |
| Cluster sem quórum | Escritas timed out, nenhum nó respondendo | etcdctl endpoint status: veja quantos nós estão vivos. Se < ⌊N/2⌋+1, pare escritas até recuperar. |
| Leader flapping | Eleições constantes, muitos term changes | Rede instável (jitter alto), GC pauses longos (>electionTimeout), heartbeat perdido. Aumente electionTimeout ou investigue infra. |
| Log compaction parada | Disco enche, escritas lentas | Raft precisa de snapshots periódicos pra compactar log. Config snapshot-count (etcd) ou equivalente. |
| Cluster impossível de recuperar | Quórum perdido permanentemente (3/3 corrompidos) | etcd tem etcdctl snapshot restore — reconstrói cluster de 1 snapshot. Procedimento de último recurso. |
Perguntas típicas (Q&A)
Por que sempre número ímpar de nós?
Um cluster de 4 nós tolera 1 falha (quórum = 3). Um cluster de 5 nós tolera 2 falhas (quórum = 3). Mesmo quórum, mesmo custo de escrita, mas o de 5 tolera mais falhas. Números pares desperdiçam 1 nó. 3, 5, 7 são os padrões.
Raft tolera bizantinos (nós mentirosos)?
Não. Raft assume modelo de crash fault: nós caem, mas não mentem. Se um nó é comprometido (malicious), Raft pode aceitar entries inválidas. Pra BFT, use PBFT, HotStuff, Tendermint (usado por blockchains). Custo: quórum de 2F+1 em crash fault vs 3F+1 em byzantine fault.
Por que o Raft performs pior em WAN?
Cada escrita precisa de round-trip do leader pro quórum. Se o leader está em us-east-1 e tem follower em eu-west-1, você paga 80-100ms por escrita. Por isso Spanner usa Paxos com Commit Wait + TrueTime, e CockroachDB usa geo-partitioning (dados "locais" ficam em quórum local). Raft vanilla não é ótimo pra multi-region writes.
O que é "Pre-Vote"?
Extensão do Raft pra evitar "disruptive servers": um nó isolado volta ao cluster com term muito alto e força reeleição desnecessária. Pre-Vote faz o candidate perguntar "eu ganharia?" antes de incrementar o term. etcd e hashicorp/raft usam.
Quando Raft fica indisponível mesmo sem falha?
Em partições de rede que separam o leader da maioria. O leader antigo vira follower (perde quórum de heartbeat); a maioria elege novo leader. Durante a transição (100-500ms típico), o cluster rejeita escritas. É a manifestação do CP-no-CAP.
- Consenso é impossível em modelo assíncrono puro (FLP). Raft contorna com timeouts.
- Raft = Leader Election + Log Replication + Safety, com terms monotônicos como chave.
- Quórum de maioria ⌊N/2⌋+1: 3 tolera 1 falha, 5 tolera 2. Use ímpar.
- Entries committed quando replicadas em quórum. Leader só commit entries do próprio term diretamente.
- Em produção: etcd/Consul/CockroachDB/K8s dependem de Raft. Cuidar do cluster = cuidar do ingresso.
- Raft é CP no CAP: partições podem causar indisponibilidade, mas nunca dados inconsistentes.
- Não use Raft pra leader election trivial — advisory lock resolve.
Próximo módulo: agora que sabemos como o cluster concorda, vamos ver como o clientesobrevive a rede quebrada — idempotência e retries.
Quiz rápido
4 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito