🧠FFVAcademy
🗳️

Consensus e Raft: como nós discordam e chegam a acordo

18 min de leitura·+90 XP
Pré-requisitos (0/1)0%

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.

💡
As 3 propriedades de consenso:
  • 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:

🚨
Teorema FLP: em um sistema assíncrono (mensagens podem demorar arbitrariamente, processos podem ser arbitrariamente lentos), nenhum protocolo determinístico pode resolver consenso se pelo menos 1 processo pode falhar (crash fault).

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:

EscapeIdeiaQuem usa
Modelo parcialmente síncronoAssume que mensagens eventualmente chegam em tempo limitado. Na prática: timeouts.Raft, Paxos, PBFT
Protocolos randomizadosUsa aleatoriedade pra quebrar simetria — termina com probabilidade 1 (não determinístico).Ben-Or, Avalanche
Failure detectorsOracle 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 decidido

A 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.

⚠️
Por que Paxos sobreviveu? Performance e flexibilidade. Spanner (Google), Chubby, BigTable e YugabyteDB usam Paxos por otimizações avançadas (Fast Paxos, EPaxos, Flexible Paxos). Para a maioria dos casos, Raft resolve 99% do problema com 1% da dor.

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:

SubproblemaO que resolve
Leader ElectionComo escolher um único leader por term (período de tempo)
Log ReplicationComo o leader propaga entries para os followers em ordem
SafetyInvariantes 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:

  1. Vira candidate
  2. Incrementa seu currentTerm
  3. Vota em si mesmo
  4. Envia RequestVote(term, candidateId, lastLogIndex, lastLogTerm) pra todos
  5. Se recebe maioria → vira leader, começa a mandar heartbeats
  6. Se recebe heartbeat de leader legítimo (term >= seu) → volta a follower
  7. Se timeout sem decisão → novo term, nova eleição
💡
Regras de voto (garantia de safety): um nó só vota em um candidato se:
  • 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):

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)
⚠️
Por que randomizar o electionTimeout? Se dois followers timed out ao mesmo tempo, os dois viram candidate e split votam. Com timeout aleatório (ex: 150-300ms uniforme), a probabilidade de colisão repetida cai exponencialmente. É por isso que Raft quase sempreelege um leader em 1-2 rodadas.

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 leader
  • prevLogIndex, prevLogTerm: o índice/term da entry imediatamente antes das novas — usado pra log matching
  • entries[]: lista de entries novas
  • leaderCommit: o commitIndex atual do leader
💡
Log Matching Property: se duas entries em logs diferentes têm o mesmo (term, index), elas contêm o mesmo comando E todos os prefixos também coincidem. O follower rejeita AppendEntries se o prevLog não bate, e o leader decrementa nextIndex daquele follower e tenta de novo com entry mais antiga. Eventualmente encontram um ponto comum.

Mini-implementação do AppendEntries:

python
# 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: Leader Completeness. Raft tem uma regra sutil que evita corrupção: um leader só pode commit entries do seu próprio term diretamente. Entries de terms anteriores são committed indiretamente quando uma entry do term atual é committed em cima delas. Esse detalhe (descrito como "Figure 8" no paper) é o ponto mais confuso do Raft — mas é o que garante que safety não quebra durante falhas.

Safety: as 5 propriedades que Raft garante

PropriedadeO que garante
Election SafetyNo máximo 1 leader por term (garantido pela regra de voto único)
Leader Append-OnlyLeader nunca sobrescreve ou deleta entries do próprio log — só adiciona
Log MatchingSe dois logs têm uma entry com mesmo (term, index), eles são idênticos até esse ponto
Leader CompletenessSe uma entry foi committed em term T, ela aparece no log de todos os leaders futuros
State Machine SafetySe 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

SistemaUso do RaftObservações
etcdBackend 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 electionRaft por datacenter; gossip entre DCs.
CockroachDBRaft por range (cada shard tem seu próprio cluster Raft)Milhares de grupos Raft por cluster — escala horizontal real.
TiKV / TiDBRaft por region (shard)Mesmo padrão do CockroachDB.
MongoDB (4.0+)Replica set election baseada em RaftAntes era protocolo próprio; migraram pra Raft por simplicidade.
RedpandaKafka-compatible usando Raft (não ZooKeeper/KRaft)Uma partição Kafka = um grupo Raft.
RabbitMQQuorum queues (substituem mirror queues antigas)Raft garante no-data-loss mesmo com crash do leader.
Kafka (KRaft)Substituto do ZooKeeper desde 3.3Usa 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?

5 nós em produção séria

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/stagingTolera 1 falha, economia real.

Alt: 7+ nós: nãoCada 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, quase sempre

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 ReplicationOrigem intelectual do Raft. Academicamente interessante, pouco usado em produção.

📋 Aplicação web precisa de 'eleição de líder' simples (ex: cron singleton)

Use Redis Redlock ou PostgreSQL advisory lock

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 RedisCom TTL + renovação. Ver papers do Kleppmann sobre limites.

Alt: etcd leaseSe você já tem etcd (K8s), use lease com TTL.

Alt: Postgres pg_try_advisory_lockSe você tem PG, literalmente 1 linha.

Operação: o que dói no dia a dia

ProblemaSintomaComo diagnosticar
Split brain (nunca acontece em Raft real)Dois nós se dizem leaderVerifique terms. Leader novo tem term maior — followers rejeitarão o velho. Se acontece, há bug na implementação.
Cluster sem quórumEscritas timed out, nenhum nó respondendoetcdctl endpoint status: veja quantos nós estão vivos. Se < ⌊N/2⌋+1, pare escritas até recuperar.
Leader flappingEleições constantes, muitos term changesRede instável (jitter alto), GC pauses longos (&gt;electionTimeout), heartbeat perdido. Aumente electionTimeout ou investigue infra.
Log compaction paradaDisco enche, escritas lentasRaft precisa de snapshots periódicos pra compactar log. Config snapshot-count (etcd) ou equivalente.
Cluster impossível de recuperarQuórum perdido permanentemente (3/3 corrompidos)etcd tem etcdctl snapshot restore — reconstrói cluster de 1 snapshot. Procedimento de último recurso.
⚠️
Regra de ouro operacional: nunca faça maintenance em mais de ⌊N/2⌋ nós simultaneamente. Num cluster de 5, reboote 1 por vez, espere voltar como follower saudável antes do próximo. É o erro operacional #1 que mata clusters etcd.

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.

Take-aways:
  • 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

Continue lendo