Sistemas Distribuídos
Fallacies of Distributed Computing
Peter Deutsch (Sun Microsystems, 1994) documentou 8 suposições falsas que programadores novatos em sistemas distribuídos fazem. Cada uma dessas falácias causa bugs, outages e problemas de design que só aparecem em produção, sob carga real.
AS 8 FALÁCIAS DE COMPUTAÇÃO DISTRIBUÍDA:
1. A rede é confiável
→ Pacotes se perdem, conexões caem, switches falham
→ Consequência: TODA chamada de rede pode falhar
→ Mitigação: retries, timeouts, circuit breakers, idempotência
2. Latência é zero
→ Chamada local: ~1ns. Chamada de rede no datacenter: ~0.5ms
→ Chamada cross-region: ~50-150ms
→ Consequência: chatty protocols (muitas chamadas pequenas) são lentos
→ Mitigação: batching, caching, protocolos binários (gRPC, protobuf)
3. Largura de banda é infinita
→ Rede interna: ~10 Gbps. Internet: variável e compartilhada
→ Consequência: serialização ineficiente consome bandwidth
→ Mitigação: compressão, paginação, streaming, protocolos eficientes
4. A rede é segura
→ Qualquer pacote pode ser interceptado, modificado ou forjado
→ Consequência: ataques man-in-the-middle, data exfiltration
→ Mitigação: TLS/mTLS, zero-trust networking, service mesh
5. A topologia não muda
→ Servidores são adicionados/removidos, IPs mudam, rotas mudam
→ Consequência: hard-coded endpoints quebram
→ Mitigação: service discovery, DNS, load balancers, service mesh
6. Existe um administrador
→ Em sistemas distribuídos, não há controle centralizado
→ Múltimos times, múltiplas organizações, múltiplas jurisdições
→ Consequência: mudanças unilaterais quebram dependentes
→ Mitigação: contratos de API, versionamento, backward compatibility
7. Custo de transporte é zero
→ Serialização, desserialização, marshalling, TLS handshake
→ Consequência: overhead escondido em cada chamada
→ Mitigação: connection pooling, keep-alive, session resumption
8. A rede é homogênea
→ Diferentes hardware, OS, protocolos, versões coexistem
→ Consequência: bugs sutis de compatibilidade
→ Mitigação: protocolos padrão (HTTP, gRPC), testes de contrato
IMPLICAÇÕES PRÁTICAS DAS FALÁCIAS:
┌────────────────────────┬───────────────────────────────────────┐
│ Design monolítico │ Design distribuído │
├────────────────────────┼───────────────────────────────────────┤
│ Chamada de função │ Chamada de rede (pode falhar) │
│ Transação ACID local │ Consistência eventual ou saga │
│ Stack trace completo │ Trace distribuído (correlação IDs) │
│ Debug com breakpoint │ Debug com logs + métricas + traces │
│ Deploy atômico │ Rolling deploy com backward compat │
│ Latência previsível │ Latência variável (tail latencies) │
│ Falha total ou nada │ Falhas parciais (partial failures) │
│ Relógio único │ Sem relógio global (clock skew) │
└────────────────────────┴───────────────────────────────────────┘
Regra de ouro: Se você pode resolver com um monólito, resolva.
Sistemas distribuídos adicionam complexidade que só se justifica
quando os benefícios (escala, isolamento, deploy independente)
superam significativamente os custos.
CAP Theorem
Definição Formal
O Teorema CAP (Eric Brewer, 2000; prova formal por Gilbert & Lynch, 2002) afirma que em um sistema de dados distribuído, durante uma partição de rede, é impossível simultaneamente garantir Consistência e Disponibilidade.
DEFINIÇÕES FORMAIS:
Consistência (C — Linearizability):
→ Toda leitura retorna o valor da escrita mais recente ou um erro
→ Equivalente a ter uma única cópia dos dados
→ Todas as réplicas concordam sobre o estado atual
Disponibilidade (A — Availability):
→ Todo request recebido por um nó não-falhado recebe uma resposta
→ A resposta pode não ser o dado mais recente
→ Sem timeouts ou erros — sempre retorna algo
Tolerância a Partição (P — Partition Tolerance):
→ O sistema continua operando apesar de perda/atraso de mensagens
entre nós da rede
→ Em qualquer sistema distribuído real, partições ACONTECEM
→ Portanto, P não é opcional — a escolha real é entre C e A
┌─────────────────────────────────────────────────┐
│ │
│ Durante uma partição de rede: │
│ │
│ CP (Consistência + Partição): │
│ → Nós que não conseguem se comunicar │
│ REJEITAM requests (retornam erro) │
│ → Dados são sempre consistentes │
│ → Indisponível durante partição │
│ → Ex: etcd, ZooKeeper, PostgreSQL (sync rep) │
│ │
│ AP (Disponibilidade + Partição): │
│ → Nós respondem mesmo sem comunicação │
│ → Dados podem divergir (conflitos) │
│ → Sempre disponível, mas possivelmente stale │
│ → Ex: DynamoDB, Cassandra, DNS │
│ │
│ CA (Consistência + Disponibilidade): │
│ → Só possível sem partições de rede │
│ → Single-node databases (PostgreSQL stand-alone)│
│ → Na prática, partições sempre são possíveis │
│ │
└─────────────────────────────────────────────────┘
PACELC Extension
PACELC (Daniel Abadi, 2012):
Extensão do CAP que captura o trade-off em operação NORMAL (sem partição).
Se há Partição (P):
→ Escolher entre Availability (A) e Consistency (C)
Else (E — operação normal):
→ Escolher entre Latency (L) e Consistency (C)
Exemplos práticos:
┌──────────────┬──────────────────────────────────────────────────┐
│ Sistema │ PACELC │
├──────────────┼──────────────────────────────────────────────────┤
│ PostgreSQL │ PA/EC — partição: perde A; normal: prioriza C │
│ (sync rep) │ Reads sempre consistentes, escrita síncrona │
├──────────────┼──────────────────────────────────────────────────┤
│ DynamoDB │ PA/EL — partição: mantém A; normal: prioriza L │
│ │ Leituras rápidas, eventual consistency padrão │
├──────────────┼──────────────────────────────────────────────────┤
│ Cassandra │ PA/EL — partição: mantém A; normal: prioriza L │
│ │ Tunable consistency (ONE, QUORUM, ALL) │
├──────────────┼──────────────────────────────────────────────────┤
│ CockroachDB │ PC/EC — partição: prioriza C; normal: prioriza C│
│ │ Sempre consistente, aceita maior latência │
├──────────────┼──────────────────────────────────────────────────┤
│ MongoDB │ PA/EC — partição: perde A; normal: prioriza C │
│ (default) │ Strong reads do primary, eventual de secondary │
└──────────────┴──────────────────────────────────────────────────┘
Por que PACELC importa mais que CAP:
→ Partições de rede são RARAS no mesmo datacenter
→ O trade-off do dia a dia é Latência vs Consistência
→ DynamoDB é rápido PORQUE aceita eventual consistency
→ CockroachDB é consistente mas adiciona 1 RTT de latência
Consistency Models
Modelos de consistência definem as garantias que um sistema distribuído oferece sobre a ordem e visibilidade de operações. São um espectro, não uma escolha binária.
ESPECTRO DE CONSISTÊNCIA (mais forte → mais fraco):
┌──────────────────────────────────────────────────────────────┐
│ │
│ Linearizability (strongest) │
│ │ → Como se houvesse uma única cópia dos dados │
│ │ → Toda leitura retorna a escrita mais recente │
│ │ → Custo: alta latência (coordenação síncrona) │
│ │ → Uso: locks distribuídos, leader election │
│ │ → Ex: ZooKeeper, etcd │
│ │ │
│ Sequential Consistency │
│ │ → Todas as operações são vistas na MESMA ORDEM │
│ │ → Mas a ordem pode não refletir tempo real │
│ │ → Custo: coordenação para ordernar │
│ │ → Uso: total order broadcast │
│ │ │
│ Causal Consistency │
│ │ → Operações causalmente relacionadas são ordenadas │
│ │ → Operações concorrentes (sem relação causal) podem │
│ │ ser vistas em qualquer ordem │
│ │ → Custo: tracking de dependências causais │
│ │ → Uso: chat (mensagens de uma conversa são ordenadas) │
│ │ → Ex: MongoDB (causal consistency sessions) │
│ │ │
│ Read-your-writes │
│ │ → Após escrever, o MESMO cliente sempre vê sua escrita │
│ │ → Outros clientes podem ver dados stale │
│ │ → Implementação: sticky sessions ou read from primary │
│ │ → Uso: perfil de usuário (editar e ver atualizado) │
│ │ │
│ Eventual Consistency (weakest practical) │
│ → Se nenhuma nova escrita ocorrer, eventualmente │
│ todas as réplicas convergem para o mesmo valor │
│ → Sem garantia de QUANDO convergem │
│ → Custo: resolução de conflitos necessária │
│ → Uso: DNS, CDN, social feeds, analytics │
│ → Ex: DynamoDB (default), Cassandra (ONE), S3 │
│ │
└──────────────────────────────────────────────────────────────┘
CONFLITOS EM EVENTUAL CONSISTENCY:
Quando duas réplicas aceitam escritas concorrentes para o mesmo dado,
precisamos de uma estratégia de resolução de conflitos:
1. Last-Writer-Wins (LWW):
→ Usa timestamp para decidir qual escrita "ganha"
→ Simples mas PERDE dados silenciosamente
→ Problema: clock skew entre nós pode escolher errado
→ Ex: Cassandra (padrão), DynamoDB (padrão)
2. Multi-Value (siblings):
→ Armazena AMBOS os valores conflitantes
→ Retorna todos ao cliente para resolução
→ Ex: Riak (com vector clocks)
3. CRDTs (Conflict-free Replicated Data Types):
→ Estruturas de dados que CONVERGEM automaticamente
→ Propriedades matemáticas: comutatividade, associatividade, idempotência
→ Sem coordenação central necessária
→ Tipos:
- G-Counter: cada nó incrementa seu slot; total = soma dos slots
- PN-Counter: par de G-Counters (positivo - negativo)
- LWW-Register: último write (por timestamp) vence
- OR-Set (Observed-Remove Set): add/remove sem conflito
- LWW-Element-Set: set com LWW per element
→ State-based (merge via join) vs Op-based (broadcast ops)
→ Ex: Redis CRDT, Riak Data Types, Automerge, Yjs (collaborative editing)
→ Para implementações detalhadas, veja "Fundamentos de Sistemas Distribuídos"
4. Application-level resolution:
→ Lógica de merge definida pela aplicação
→ Ex: Git merge (manual), Google Docs (OT/CRDT), carrinho
de compras (union de items)
Data Partitioning
Particionamento (sharding) divide dados entre múltiplos nós para escalar horizontalmente. A estratégia de particionamento determina como os dados são distribuídos e como queries são roteadas.
ESTRATÉGIAS DE PARTICIONAMENTO:
1. RANGE PARTITIONING:
→ Divide dados por ranges de valores
→ Ex: users A-M → shard 1, N-Z → shard 2
→ Ex: timestamps Jan-Mar → partition 1, Apr-Jun → partition 2
Prós:
→ Range queries eficientes (buscar dados de um mês)
→ Implementação simples
Contras:
→ HOTSPOTS: distribuição desigual (mais nomes com "S" que "X")
→ Rebalanceamento complexo quando ranges mudam
Quando usar:
→ Time-series data (partitionar por mês/dia)
→ Dados com range queries frequentes
2. HASH PARTITIONING:
→ partition = hash(key) % num_partitions
→ Distribui dados uniformemente (se hash é bom)
Prós:
→ Distribuição uniforme de carga
→ Sem hotspots (na maioria dos casos)
Contras:
→ Range queries impossíveis (scatter-gather)
→ Adicionar/remover nós redistribui TUDO
Quando usar:
→ Point lookups (buscar por ID)
→ Quando distribuição uniforme é prioridade
3. CONSISTENT HASHING:
→ Nós e chaves são mapeados em um anel (ring) de hash
→ Cada nó é responsável pelo range entre ele e o nó anterior
→ Adicionar/remover nó redistribui apenas 1/N dos dados
┌──────────────────────────────────┐
│ Hash Ring │
│ │
│ Node A ←───── ← Node D │
│ ↗ ↘ │
│ │ │ │
│ │ Keys mapeadas │ │
│ │ no ring │ │
│ ↖ ↙ │
│ Node B ─────→ → Node C │
│ │
└──────────────────────────────────┘
VIRTUAL NODES (vnodes):
→ Cada nó físico tem múltiplos pontos virtuais no ring
→ Melhora distribuição quando há poucos nós físicos
→ Ex: Cassandra usa 256 vnodes por padrão
→ Nó com mais recursos pode ter mais vnodes
Prós:
→ Redistribuição mínima ao escalar
→ Com vnodes, distribuição uniforme mesmo com poucos nós
Contras:
→ Mais complexo de implementar e debugar
→ Hotspots em vnodes que cobrem ranges populares
Usado por: DynamoDB, Cassandra, Riak, Memcached (client-side)
4. DIRECTORY-BASED:
→ Tabela de lookup que mapeia key → partition
→ Flexibilidade total de roteamento
Prós:
→ Pode rebalancear arbitrariamente
→ Suporta qualquer estratégia de distribuição
Contras:
→ Lookup table é single point of failure
→ Lookup adiciona latência em toda operação
Quando usar:
→ Multi-tenant (tenant_id → partition dedicada)
→ Dados com requisitos regulatórios (LGPD: dados BR no Brasil)
SECONDARY INDEXES EM SISTEMAS PARTICIONADOS:
Problema: como manter índices secundários quando dados estão particionados?
1. Local Index (document-partitioned):
→ Cada partition mantém seu próprio índice dos dados locais
→ Escrita: rápida (atualiza apenas índice local)
→ Leitura: scatter-gather (query vai para TODAS as partitions)
→ Usado por: MongoDB, Cassandra, Elasticsearch
2. Global Index (term-partitioned):
→ Índice é particionado independente dos dados
→ Escrita: lenta (atualiza índice em outra partition)
→ Leitura: rápida (query vai para 1 partition do índice)
→ Usado por: DynamoDB (GSI), CockroachDB
Trade-off: reads frequentes → global index
writes frequentes → local index
Replication
Replicação mantém cópias dos dados em múltiplos nós para durabilidade, disponibilidade e performance de leitura.
MODELOS DE REPLICAÇÃO:
1. SINGLE-LEADER (Primary-Replica):
→ 1 nó aceita escritas (leader/primary)
→ N nós recebem cópias (followers/replicas)
→ Replicas servem leituras
┌──────────────────────────────────────────────┐
│ │
│ Client ──write──→ [Leader] ──sync/async──→ │
│ │ │
│ Client ──read──→ [Replica 1] │
│ Client ──read──→ [Replica 2] │
│ Client ──read──→ [Replica 3] │
│ │
└──────────────────────────────────────────────┘
Replicação síncrona:
→ Leader espera confirmação da replica antes de confirmar ao client
→ Garante que replica está atualizada
→ Aumenta latência de escrita
→ Se replica falhar, escrita bloqueia
Replicação assíncrona:
→ Leader confirma ao client sem esperar replicas
→ Menor latência de escrita
→ Risco: dados perdidos se leader falhar antes de replicar
→ Replication lag causa leituras stale
Usado por: PostgreSQL, MySQL, MongoDB (default), Redis Sentinel
2. MULTI-LEADER (Active-Active):
→ Múltiplos nós aceitam escritas
→ Cada leader replica para os outros
→ Conflitos são inevitáveis
┌──────────────────────────────────────────────┐
│ │
│ [Leader US] ←──replication──→ [Leader EU] │
│ ↑ ↑ │
│ writes writes │
│ from US from EU │
│ │
└──────────────────────────────────────────────┘
Quando usar:
→ Multi-datacenter (cada DC tem seu leader)
→ Offline-first apps (cada dispositivo é um "leader")
→ Collaborative editing (Google Docs)
Desafio principal: resolução de conflitos
→ Write conflicts quando dois leaders escrevem o mesmo dado
→ Soluções: LWW, merge functions, CRDTs
Usado por: CouchDB, Galera Cluster (MySQL), Active Directory
3. LEADERLESS:
→ QUALQUER nó aceita leituras e escritas
→ Client escreve para múltiplos nós simultaneamente
→ Quorum define quando operação é bem-sucedida
┌──────────────────────────────────────────────┐
│ │
│ Client ──write──→ [Node 1] ✓ │
│ ──write──→ [Node 2] ✓ │
│ ──write──→ [Node 3] ✗ (falhou) │
│ │
│ W=2 de N=3: escrita bem-sucedida │
│ │
│ Client ──read──→ [Node 1] v2 │
│ ──read──→ [Node 2] v2 │
│ ──read──→ [Node 3] v1 (stale) │
│ │
│ R=2 de N=3: retorna v2 (mais recente) │
│ │
└──────────────────────────────────────────────┘
Usado por: DynamoDB, Cassandra, Riak, Voldemort
Quorum Reads/Writes
QUORUM: W + R > N garante que leituras veem a escrita mais recente
Onde:
N = número total de réplicas
W = número de nós que confirmam uma escrita
R = número de nós consultados em uma leitura
Se W + R > N, pelo menos 1 nó lido terá o dado mais recente.
Configurações comuns:
┌────────┬─────────────────────────────────────────────────┐
│ Config │ Comportamento │
├────────┼─────────────────────────────────────────────────┤
│ N=3 │ Balanceado — tolerância a 1 falha em writes │
│ W=2 │ e 1 falha em reads │
│ R=2 │ Latência moderada (espera 2 de 3) │
├────────┼─────────────────────────────────────────────────┤
│ N=3 │ Read-optimized — reads de 1 nó (rápido) │
│ W=3 │ Writes lentos (todos devem confirmar) │
│ R=1 │ Bom para read-heavy workloads │
├────────┼─────────────────────────────────────────────────┤
│ N=3 │ Write-optimized — writes de 1 nó (rápido) │
│ W=1 │ Reads lentos (consulta todos os nós) │
│ R=3 │ Risco: dados perdidos se nó com W=1 falhar │
└────────┴─────────────────────────────────────────────────┘
SLOPPY QUORUM (DynamoDB):
→ Se nós designados estão indisponíveis, aceita escrita em outros nós
→ "Hinted handoff": quando nó original volta, dados são transferidos
→ Melhora disponibilidade, mas enfraquece garantia de consistência
READ REPAIR:
→ Ao ler de múltiplos nós, detectar que algum está desatualizado
→ Enviar o valor correto para o nó desatualizado
→ Reparo lazy — funciona apenas para dados lidos frequentemente
ANTI-ENTROPY (background repair):
→ Processo background compara dados entre réplicas
→ Merkle trees para detectar diferenças eficientemente
→ Repara dados que não são lidos frequentemente
Consensus Algorithms
Consensus é o problema de fazer múltiplos nós concordarem sobre um valor, mesmo na presença de falhas. É fundamental para leader election, state machine replication e transações distribuídas.
FLP Impossibility
RESULTADO FLP (Fischer, Lynch, Paterson, 1985):
"Em um sistema assíncrono, é impossível atingir consensus
determinístico se MESMO UM nó pode falhar."
Implicações:
→ Nenhum algoritmo de consensus GARANTE terminação em
tempo finito em sistema assíncrono com falhas
→ Na prática, algoritmos como Raft e Paxos usam TIMEOUTS
(parcialmente síncrono) para contornar FLP
→ Timeouts permitem detectar falhas, mas false positives
(nó lento interpretado como falho) podem causar split-brain
Por que isso importa:
→ Consensus é fundamentalmente DIFÍCIL
→ Não invente seu próprio algoritmo de consensus
→ Use implementações testadas (etcd/Raft, ZooKeeper/ZAB)
Raft Algorithm
Raft (Diego Ongaro, 2014) foi projetado para ser mais compreensível que Paxos, mantendo as mesmas garantias de segurança.
RAFT — VISÃO GERAL:
Cada nó está em um de três estados:
FOLLOWER → CANDIDATE → LEADER
O tempo é dividido em "terms" (mandatos):
→ Cada term tem no máximo 1 leader
→ Terms são numerados sequencialmente (1, 2, 3...)
→ Se um nó vê term mais alto, atualiza seu term
COMPONENTES PRINCIPAIS:
1. Leader Election — como um leader é eleito
2. Log Replication — como o leader replica dados
3. Safety — garantias de que logs são consistentes
1. LEADER ELECTION:
Estado inicial: todos são FOLLOWERS
Follower não recebe heartbeat do leader em tempo X:
→ Incrementa term
→ Transiciona para CANDIDATE
→ Vota em si mesmo
→ Envia RequestVote RPC para todos os outros nós
Regras de votação:
→ Cada nó vota em no máximo 1 candidate por term
→ Vote é dado ao primeiro candidate que pedir
→ Candidate precisa de maioria (N/2 + 1) para vencer
→ Se recebe AppendEntries de leader com term >= seu, volta a FOLLOWER
Cenários:
a) Candidate recebe maioria → vira LEADER, envia heartbeats
b) Outro nó vira leader → candidate volta a FOLLOWER
c) Timeout sem resultado → incrementa term, nova eleição
(election timeout randomizado para evitar empate perpétuo)
┌──────────────────────────────────────────────────┐
│ │
│ [A: Follower] [B: Follower] [C: Follower] │
│ │
│ (timeout em B, nenhum heartbeat recebido) │
│ │
│ [A: Follower] [B: Candidate] [C: Follower] │
│ ↓ RequestVote │
│ [A: vota em B] [B: vota em si] [C: vota em B] │
│ │
│ B recebe 3/3 votos (maioria) → Leader │
│ │
│ [A: Follower] [B: LEADER] [C: Follower] │
│ ↓ heartbeats periódicos │
│ │
└──────────────────────────────────────────────────┘
2. LOG REPLICATION:
Toda operação passa pelo leader:
Client → Leader → AppendEntries RPC → Followers
Processo:
1. Client envia request ao Leader
2. Leader adiciona entry ao seu log (não commitada)
3. Leader envia AppendEntries RPC a todos os Followers
4. Followers adicionam entry ao seu log
5. Followers respondem com sucesso
6. Quando MAIORIA confirma → Leader COMMITA a entry
7. Leader aplica ao state machine e responde ao Client
8. Followers são notificados do commit no próximo heartbeat
Log consistency:
→ Se duas entries em logs diferentes têm o mesmo index e term,
elas contêm o mesmo comando
→ Se duas entries em logs diferentes têm o mesmo index e term,
TODOS os entries anteriores também são idênticos
→ Leader nunca sobrescreve ou deleta entries do seu log
┌────────────────────────────────────────────────┐
│ Leader log: [1:set x=1] [2:set y=2] [3:set z=3] │
│ Follower A: [1:set x=1] [2:set y=2] [3:set z=3] │
│ Follower B: [1:set x=1] [2:set y=2] │
│ │
│ Entries 1 e 2: committed (maioria tem) │
│ Entry 3: committed se Leader + A = maioria (2 de 3) │
│ Follower B receberá entry 3 no próximo AppendEntries│
└────────────────────────────────────────────────┘
3. SAFETY GUARANTEES:
Election Restriction:
→ Candidate só recebe voto se seu log for pelo menos tão
atualizado quanto o do voter
→ Garante que leader sempre tem todas as entries committed
→ Impede que nó desatualizado vire leader
Leader Completeness:
→ Se uma entry é committed em um dado term, ela estará presente
no log de TODOS os leaders de terms futuros
→ Consequência: uma vez committed, dado nunca é perdido
(enquanto maioria dos nós estiver viva)
State Machine Safety:
→ Se um nó aplica uma entry ao seu state machine em um dado index,
nenhum outro nó aplica entry diferente no mesmo index
→ Garante que todos os nós têm o mesmo estado
IMPLEMENTAÇÕES DE RAFT:
→ etcd: usado pelo Kubernetes para coordenação
→ CockroachDB: database distribuído PostgreSQL-compatible
→ TiKV: storage engine do TiDB
→ HashiCorp Consul: service discovery e configuration
→ RabbitMQ Quorum Queues: replicação de filas
Logical Clocks
Em sistemas distribuídos, não há relógio global confiável. Relógios físicos (NTP) têm clock skew de milissegundos a segundos. Relógios lógicos estabelecem ordenação de eventos sem depender de relógios físicos.
Lamport Timestamps
LAMPORT TIMESTAMPS (Leslie Lamport, 1978):
Cada nó mantém um contador lógico C:
Regra 1: Antes de cada evento local, incrementa C
Regra 2: Ao ENVIAR mensagem, inclui timestamp C
Regra 3: Ao RECEBER mensagem com timestamp T,
C = max(C, T) + 1
Propriedade: Se evento A aconteceu antes de B (A → B),
então C(A) < C(B)
IMPORTANTE: O inverso NÃO é verdade!
C(A) < C(B) NÃO significa que A aconteceu antes de B
Eventos concorrentes podem ter qualquer ordenação de timestamps
Exemplo:
Node 1: [C=1: write x] ──msg(C=1)──→ [C=3: read x]
Node 2: [C=1: write y] [C=2: read y]
C(write_x)=1, C(write_y)=1
São concorrentes! Lamport não distingue.
Limitação: não captura concorrência — não sabe se dois eventos
são causalmente relacionados ou independentes
Vector Clocks
VECTOR CLOCKS:
Cada nó mantém um VETOR de contadores, um por nó no sistema.
Node i: VC[i] = [c1, c2, ..., cn]
Regras:
1. Antes de evento local em nó i: VC[i][i]++
2. Ao enviar mensagem: inclui VC do sender
3. Ao receber mensagem com VC_msg:
Para cada j: VC[j] = max(VC[j], VC_msg[j])
Depois: VC[i]++
Comparação:
VC(A) < VC(B) sse para todo i: A[i] <= B[i] e existe j: A[j] < B[j]
→ A HAPPENED-BEFORE B (A causou B)
Se nem VC(A) < VC(B) nem VC(B) < VC(A):
→ A e B são CONCORRENTES (sem relação causal)
Exemplo com 3 nós:
Node A: [1,0,0] → [2,0,0] ──msg──→
Node B: [0,1,0] → recebe → [2,2,0] → [2,3,0]
Node C: [0,0,1] → [0,0,2]
[2,0,0] e [0,0,2] são concorrentes (nem um < outro)
[2,0,0] < [2,2,0] (A causou o estado de B)
Limitação: vetor cresce com O(N) onde N = número de nós
Impraticável para sistemas com muitos nós (>100)
Hybrid Logical Clocks (HLC)
HYBRID LOGICAL CLOCKS (Kulkarni et al., 2014):
Combina relógio físico (NTP) com relógio lógico.
HLC = (physical_time, logical_counter)
Vantagens sobre vector clocks:
→ Tamanho fixo (64 bits), não cresce com número de nós
→ Captura causalidade como Lamport timestamps
→ Próximo do tempo real (facilitando debugging)
→ Pode ser comparado com timestamps de wall clock
Usado por:
→ CockroachDB (MVCC timestamps)
→ YugabyteDB
→ MongoDB (cluster time)
Regra básica:
1. Ao gerar evento: HLC = max(local_HLC, physical_clock)
2. Se physical_clock > HLC: reset logical_counter = 0
3. Se physical_clock <= HLC: logical_counter++
4. Ao receber mensagem: HLC = max(local_HLC, msg_HLC, physical_clock)
Failure Modes
Entender os tipos de falha é fundamental para projetar sistemas que tolerem cada cenário.
TIPOS DE FALHA:
1. CRASH-STOP (fail-stop):
→ Nó para de funcionar e NUNCA volta
→ Outros nós eventualmente detectam via timeout
→ Mais simples de lidar
→ Ex: hardware queimou, VM terminada
2. CRASH-RECOVERY (fail-recover):
→ Nó para de funcionar mas pode VOLTAR depois
→ Ao voltar, pode ter estado desatualizado
→ Precisa de persistent storage para recover
→ Ex: restart após OOM, reboot após patch
→ MAIS COMUM em produção
3. OMISSION FAILURE:
→ Nó não envia/recebe mensagens que deveria
→ Send omission: mensagem nunca é enviada
→ Receive omission: mensagem nunca é recebida (dropped)
→ Difícil distinguir de crash
→ Ex: network partition, buffer overflow, queue full
4. TIMING FAILURE:
→ Nó responde mas FORA do tempo esperado
→ Resposta chega mas tarde demais
→ Em sistemas síncronos: detectável por timeout
→ Em sistemas assíncronos: indistinguível de falha
→ Ex: GC pause, CPU throttling, rede congestionada
5. BYZANTINE FAILURE (mais difícil):
→ Nó se comporta ARBITRARIAMENTE (incluindo maliciosamente)
→ Pode enviar mensagens incorretas, mentir, conspirar
→ Requer Byzantine Fault Tolerance (BFT): 3f+1 nós para tolerar f
→ Ex: hardware corrompido, software com bug, ataque malicioso
→ Relevante: blockchains (Bitcoin usa Proof-of-Work como BFT)
→ Na maioria dos sistemas internos: ignoramos Byzantine
PARTIAL FAILURES (o desafio real):
→ Em um sistema monolítico: ou funciona ou não
→ Em distribuído: PARTE do sistema pode falhar
→ Alguns nós OK, outros down, outros lentos, outros inconsistentes
→ O sistema deve continuar operando com funcionalidade reduzida
→ É o cenário mais difícil de testar e de raciocinar sobre
┌──────────────────────────────────────────────────┐
│ Exemplo de partial failure: │
│ │
│ [API Server 1: OK] │
│ [API Server 2: OK] │
│ [API Server 3: SLOW (GC pause)] │
│ [Database Primary: OK] │
│ [Database Replica 1: OK] │
│ [Database Replica 2: DOWN (disk failure)] │
│ [Redis Cache: OK] │
│ [RabbitMQ: PARTITIONED (network issue)] │
│ │
│ O sistema funciona? Parcialmente. Depende: │
│ → Reads: OK (primary + replica 1) │
│ → Writes: OK (primary, async to replica 1) │
│ → Cache: OK │
│ → Async events: DEGRADED (RabbitMQ particionado) │
│ → API: DEGRADED (1/3 dos servers lentos) │
└──────────────────────────────────────────────────┘
Patterns for Distributed Systems
Saga Pattern
SAGA: substituição para transações distribuídas (2PC)
Problema: sem ACID distribuído, como manter consistência
entre múltiplos serviços?
Saga = sequência de transações locais, cada uma com uma
compensação (rollback) correspondente.
ORQUESTRAÇÃO (orquestrador central):
┌───────────┐
│ Saga │
│ Orchestr. │
└─────┬─────┘
│
Step 1: Reserve Inventory ✓
Step 2: Process Payment ✓
Step 3: Ship Order ✗ (falhou!)
│
Compensações (na ordem reversa):
Comp 2: Refund Payment
Comp 1: Release Inventory
COREOGRAFIA (event-driven, sem orquestrador):
OrderService → OrderCreated
→ InventoryService → InventoryReserved
→ PaymentService → PaymentProcessed
→ ShippingService → ShipmentFailed!
→ PaymentService → PaymentRefunded
→ InventoryService → InventoryReleased
Prós: desacoplado, sem single point of failure
Contras: difícil de rastrear fluxo, debugging complexo
REGRAS DE SAGAS:
→ Cada step é uma transação LOCAL (ACID no banco local)
→ Compensações devem ser IDEMPOTENTES (podem ser executadas múltiplas vezes)
→ Compensações são semânticas (refund, não rollback técnico)
→ Estado intermediário é VISÍVEL para outros (sem isolamento)
CQRS (Command Query Responsibility Segregation)
CQRS: separar modelo de escrita do modelo de leitura
Commands Queries
(write model) (read model)
│ │
┌────▼─────┐ ┌────▼─────┐
│ Command │ │ Query │
│ Handler │ │ Handler │
└────┬─────┘ └────┬─────┘
│ │
┌────▼─────┐ ┌────▼─────┐
│ Write DB │──replication─│ Read DB │
│(normalized│ (async) │(denormal.)│
│ schema) │ │ optimized│
│ │ │ for reads│
└──────────┘ └──────────┘
Quando usar:
→ Read/write ratio muito desigual (100:1)
→ Modelos de leitura e escrita são fundamentalmente diferentes
→ Precisa de views materializadas otimizadas para queries
→ Acoplado com Event Sourcing
Quando NÃO usar:
→ CRUD simples (overhead não justifica)
→ Consistência forte entre read e write é obrigatória
→ Time pequeno sem experiência em eventual consistency
Event Sourcing
EVENT SOURCING: armazenar EVENTOS em vez de estado atual
Ao invés de:
UPDATE accounts SET balance = 950 WHERE id = 1;
Armazene:
INSERT INTO events VALUES ('AccountDebited', {id: 1, amount: 50});
Estado atual = replay de todos os eventos
Vantagens:
→ Audit trail completo e imutável
→ Pode reconstruir estado de qualquer ponto no tempo
→ Event store é append-only (performance de escrita excelente)
→ Natural para CQRS (eventos alimentam projeções de leitura)
→ Debug: "por que o saldo é X?" → replay eventos
Desafios:
→ Eventual consistency entre event store e projeções
→ Schema evolution de eventos (versionamento)
→ Reconstruir estado de bilhões de eventos: SNAPSHOTS
→ Complexidade de implementação significativa
Outbox Pattern
OUTBOX PATTERN: garantir que evento é publicado quando
dado é persistido (atomicidade)
Problema: sem transação distribuída entre DB e message broker,
como garantir que ambos são atualizados?
Solução:
1. Na mesma transação do DB, salvar o evento em tabela "outbox"
2. Processo separado lê outbox e publica no broker
3. Após publicação confirmada, marca evento como publicado
BEGIN TRANSACTION;
INSERT INTO orders (...) VALUES (...);
INSERT INTO outbox_events (type, payload) VALUES
('OrderCreated', '{"orderId": "123", ...}');
COMMIT;
-- Processo separado (polling ou CDC/Debezium):
-- Lê outbox_events não publicados
-- Publica no Kafka/RabbitMQ
-- Marca como published
Alternativa: CDC (Change Data Capture)
→ Debezium lê WAL/binlog do banco e publica eventos
→ Sem tabela outbox, sem polling
→ Mais eficiente, mais complexo de operar
Idempotency Keys
IDEMPOTÊNCIA: executar a mesma operação múltiplas vezes
produz o mesmo resultado
Por que é crucial em distribuídos:
→ Retries inevitáveis (rede não confiável)
→ At-least-once delivery em message brokers
→ Client não sabe se request foi processado (timeout)
Implementação com idempotency keys:
// Client envia:
POST /payments
X-Idempotency-Key: "pay_abc123_attempt_1"
{ amount: 100, currency: "BRL" }
// Server:
async function processPayment(req) {
const key = req.headers['x-idempotency-key'];
// 1. Verificar se já processou esta key
const existing = await db.query(
'SELECT result FROM idempotency_keys WHERE key = $1',
[key]
);
if (existing) return existing.result; // Retorna resultado anterior
// 2. Processar e salvar resultado atomicamente
const result = await db.transaction(async (tx) => {
const payment = await processPaymentLogic(req.body);
await tx.query(
'INSERT INTO idempotency_keys (key, result, expires_at) VALUES ($1, $2, NOW() + interval \'24h\')',
[key, JSON.stringify(payment)]
);
return payment;
});
return result;
}
// Mesmo request com mesma key → mesmo resultado
// Sem efeitos colaterais duplicados (sem cobrar 2x)
Observabilidade em Sistemas Distribuídos
OS TRÊS PILARES:
1. LOGS (o que aconteceu):
→ Structured logging (JSON) com correlation IDs
→ Log aggregation (ELK Stack, Loki, Datadog)
→ Correlation ID propaga entre serviços para rastrear request
2. MÉTRICAS (como está performando):
→ RED: Rate, Errors, Duration (para serviços)
→ USE: Utilization, Saturation, Errors (para recursos)
→ Dashboards com Grafana, Prometheus
→ Alertas baseados em SLOs
3. TRACES (como requests fluem):
→ Distributed tracing (Jaeger, Zipkin, Datadog APM)
→ Span = unidade de trabalho; Trace = conjunto de spans
→ Identifica latência entre serviços
→ OpenTelemetry como padrão
Correlação entre os três:
→ Trace ID conecta logs + métricas + traces de um request
→ "P99 subiu?" → encontra trace lento → vê logs → identifica causa
Referências
LIVROS ESSENCIAIS:
- "Designing Data-Intensive Applications" — Martin Kleppmann
A BÍBLIA de sistemas distribuídos. Leitura obrigatória.
- "Understanding Distributed Systems" — Roberto Vitillo
Versão mais acessível e prática dos mesmos conceitos.
- "Database Internals" — Alex Petrov
Aprofunda storage engines, replicação e consensus.
PAPERS FUNDAMENTAIS:
- "In Search of an Understandable Consensus Algorithm" — Ongaro & Ousterhout (Raft)
https://raft.github.io/raft.pdf
- "Spanner: Google's Globally-Distributed Database" — Google
https://research.google/pubs/pub39966/
- "Dynamo: Amazon's Highly Available Key-value Store" — Amazon
https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- "Time, Clocks, and the Ordering of Events in a Distributed System" — Lamport
O paper original de relógios lógicos (1978).
RECURSOS ONLINE:
- Jepsen.io — Kyle Kingsbury testa claims de consistência de databases
https://jepsen.io
- raft.github.io — Visualização interativa do Raft
- "The Morning Paper" — Resumos de papers de distributed systems
- Martin Kleppmann's blog e talks
- Marc Brooker's blog (AWS) — distribuídos na prática