MÓDULO 14 · CONCEITO 09 DE 12

Distributed Database Internals — LSM vs B-tree, consenso, replicação e CAP/PACELC

O que está por baixo das escolhas de banco. Por que Cassandra é write-heavy e Postgres é read-heavy. Como Raft elege um líder em segundos enquanto Paxos exige PhD para implementar. O que consistent hashing resolve que sharding por range não resolve. Replicação síncrona vs assíncrona em termos de RPO e RTO reais. CAP como mapa de decisão (não como teorema acadêmico) e PACELC como sua extensão prática para sistemas sem partição. Entender esses internals é o que separa "usei Postgres porque sempre usei" de "escolhi Postgres porque a workload exige snapshot isolation e o write throughput cabe num primary".

Tempo de leitura ~35 min Pré-requisito 08 · Notifications · módulo 05 (Bancos de Dados) Próximo 10 · CDN e Cache Distribuído →

Em 2007, Giuseppe DeCandia e outros engenheiros da Amazon publicaram um paper de 16 páginas chamado "Dynamo: Amazon's Highly Available Key-value Store". O paper descrevia o sistema interno que rodava o carrinho de compras da Amazon — um sistema desenhado com uma única obsessão: nunca falhar a operação de adicionar item ao carrinho, mesmo se metade dos data centers estivesse offline. Para conseguir isso, abriram mão de consistência forte (dois nós podem ter versões diferentes do carrinho ao mesmo tempo), abraçaram conflitos como resolúveis depois ("union dos itens nos dois carrinhos"), e usaram técnicas como consistent hashing, vector clocks, e gossip protocol que eram conceitos acadêmicos pouco aplicados na indústria. O paper se tornou a inspiração de uma geração inteira de bancos: Cassandra, Riak, Voldemort, DynamoDB (o serviço da AWS), todos descendem diretamente das ideias do Dynamo. Antes desse paper, "banco de dados" significava Postgres ou MySQL com replicação master-slave. Depois, o mundo entendeu que existia um espaço inteiro de tradeoffs diferentes — e que a escolha de banco era a escolha de quais tradeoffs aceitar.

Entender os internals desses bancos não é exercício acadêmico — é o que permite tomar decisões informadas em system design. "Vou usar Cassandra" sem entender LSM tree é uma decisão sem fundamento. "Vou usar Postgres" sem entender MVCC é igual. A escolha entre Cassandra, DynamoDB, CockroachDB, Postgres e MySQL deriva de quatro perguntas fundamentais: (1) qual é a razão entre reads e writes, (2) qual nível de consistência o produto exige, (3) qual é o padrão de acesso (queries ad-hoc vs por chave primária), e (4) onde o sistema precisa estar geograficamente. Este conceito não substitui o módulo 05 (Bancos de Dados) — aprofunda especificamente nos mecanismos internos que tornam cada escolha justificável ou não em system design.

LSM tree vs B-tree: a fronteira entre read-heavy e write-heavy

Toda escolha de banco começa com a estrutura de dados do storage engine. Postgres, MySQL/InnoDB, Oracle e SQL Server usam variações de B-tree. Cassandra, RocksDB, LevelDB, ScyllaDB e o storage do Kafka usam LSM tree (Log-Structured Merge tree). A diferença não é detalhe — define o que o banco faz bem e mal.

# B-TREE: a estrutura clássica desde 1970

# Estrutura:
# - Árvore balanceada de nodes em disco; tipicamente fan-out de 100-200 chaves por node
# - Cada lookup: descer da raiz até a folha, O(log_B(N)) acessos a disco
# - Update in-place: encontra a página, modifica, escreve de volta

# Write:
# 1. Localizar a página onde a chave deve estar (log N reads)
# 2. Modificar a página em memória
# 3. Escrever WAL (write-ahead log) para durabilidade
# 4. Eventualmente, fsync da página modificada para disco
# Cost: 1 random write para a página (após o WAL append)

# Read:
# 1. Descer a árvore: log N seek reads
# 2. Para queries com cache quente, primeiros níveis ficam em RAM → ~1 disk seek
# Cost: 1 random read no caso quente

# Características:
# + Reads consistentemente rápidos (1-3 random seeks)
# + Range scans são naturais (folhas linkadas em lista)
# + Update in-place: espaço crescimento previsível
# - Writes precisam encontrar e modificar página específica → random I/O
# - Splits/merges causam write amplification em hot ranges
# - Concurrent writes precisam de locking de página (contention)

# LSM TREE: a estrutura que dominou os sistemas write-heavy desde 2010s

# Estrutura:
# - Writes vão para um MEMTABLE (in-memory sorted structure, ex: skip list)
# - Quando o memtable enche, é flushed como um SSTABLE imutável no disco
# - SSTABLE = Sorted String Table: arquivo ordenado por chave, com index esparso
# - Background: compaction merge SSTables menores em maiores

# Write:
# 1. Append no WAL (sequential write)
# 2. Insert no memtable em memória (O(log N) na skip list)
# 3. Eventualmente: flush memtable → SSTable no disco (sequential write)
# Cost: ZERO random writes! Apenas sequential writes
# Throughput: 10x-100x mais que B-tree em workloads write-heavy

# Read:
# 1. Verificar memtable (RAM, rápido)
# 2. Se não encontrado: verificar SSTables, do mais novo para o mais antigo
# 3. Cada SSTable: bloom filter em RAM diz "talvez está aqui" ou "garantido que não"
# 4. Se bloom filter diz sim: lookup no index esparso + seek nos dados
# Cost: pode precisar verificar N SSTables = N seeks no pior caso
# Mitigação: bloom filters reduzem falsos lookups; compaction reduz N

# Compaction:
# - Tiered compaction: SSTables agrupadas por tamanho similar; merge cria SSTable maior
# - Leveled compaction: níveis L0..LN; cada nível tem 10x o tamanho do anterior
# - Importante: compaction é I/O em background; pode causar latência picos

# Características:
# + Writes EXTREMAMENTE rápidos (apenas appends e ops em memória)
# + Naturalmente "log-friendly" — bom para SSDs
# + Compressão melhor (SSTables imutáveis comprimem bem)
# - Reads podem ser mais lentos (multiple SSTables a verificar)
# - Compaction overhead: I/O constante em background; pode dobrar o write amplification
# - Delete não é instantâneo: cria um "tombstone" que persiste até a próxima compaction

# Comparação prática:
# Workload: 10k writes/s sustentado
#   B-tree (Postgres single instance): saturado em ~10-20k writes/s
#   LSM (Cassandra single node): confortável até 50-100k writes/s
# Workload: 100k reads/s com cache 90% hit
#   B-tree: latência P99 ~5ms (1 disk seek se cache miss)
#   LSM: latência P99 ~15ms (pode tocar 3-5 SSTables se cache miss)

# Quem usa o quê:
# B-tree: PostgreSQL, MySQL/InnoDB, Oracle, SQL Server, MongoDB (WiredTiger)
# LSM: Cassandra, ScyllaDB, RocksDB (base de CockroachDB, TiDB, MyRocks), LevelDB,
#      HBase, BigTable, DynamoDB internals (variante proprietária)

MVCC: como Postgres faz transações concorrentes sem bloquear leituras

MVCC (Multi-Version Concurrency Control) é o mecanismo que permite ao Postgres oferecer snapshot isolation sem locking pesado de leitura. Cada linha tem várias versões; transações leem a versão consistente com seu snapshot. É uma das ideias mais elegantes em databases — e é o que faz Postgres ser preferido sobre MySQL para workloads OLTP complexos.

# Cada tupla (linha) no Postgres tem dois campos invisíveis:
# - xmin: ID da transação que inseriu/atualizou esta versão
# - xmax: ID da transação que deletou/substituiu (ou 0 se ainda viva)

# Cada transação T começa com um snapshot:
# - xmin_snapshot: menor TX ID ativo no momento do START
# - xip_list: lista de TXs em progresso no START
# - xmax_snapshot: maior TX ID atribuído + 1

# Regra de visibilidade (simplificada):
# Uma tupla é visível para a transação T se:
#   - tupla.xmin < T.xmax_snapshot (foi criada antes do START)
#   - tupla.xmin NOT IN T.xip_list (a TX criadora já tinha commitado quando T começou)
#   - tupla.xmax == 0 OR tupla.xmax > T.xmax_snapshot (não foi deletada)

# Exemplo concreto:
# T1 (TX 100) começa, snapshot = (xmin=99, xip=[], xmax=101)
# T2 (TX 101) começa, snapshot = (xmin=99, xip=[100], xmax=102)
#
# T1: UPDATE balance SET amount = 200 WHERE id = 1
#   → Cria nova versão da linha com xmin=100, xmax=0
#   → Marca versão antiga com xmax=100 (deletada por T1)
#
# T2 lê id=1 ANTES de T1 commitar:
#   → Encontra duas versões: (xmin=99, xmax=100) e (xmin=100, xmax=0)
#   → Versão antiga (xmin=99) é visível para T2 (99 < 102, 99 not in xip)
#     E xmax=100 está em T2.xip_list → trata como "ainda viva" para T2
#   → Versão nova (xmin=100) é INVISÍVEL para T2 (xmin=100 está em xip_list)
#   → T2 lê amount = valor antigo, mesmo concorrente com a atualização

# Vantagens do MVCC:
# + Readers nunca bloqueiam writers, e vice-versa
# + Snapshot consistency: T2 sempre vê o estado do banco no momento do START
# + Sem deadlocks típicos de locking pessimista
# + Long-running analytics queries (rodando por horas) não bloqueiam OLTP

# Custos do MVCC:
# - Bloat: versões antigas ocupam espaço até serem limpas por VACUUM
# - VACUUM precisa rodar periodicamente para reclaim de espaço
# - Tuple visibility check tem custo em cada read
# - Se uma transação fica MUITO tempo aberta, impede VACUUM de limpar tuplas → tabela bloat

# AUTOVACUUM: o processo background que limpa tuplas mortas
# - Por padrão dispara quando 20% da tabela é "dead"
# - Em tabelas com alta taxa de update, pode não dar conta → tuning manual

# Em escala (lição aprendida na prática):
# - Tabelas com muito UPDATE pesado em Postgres podem ter performance degradada por bloat
# - Mitigação: particionamento, UPDATE→DELETE+INSERT em alguns casos, ou usar Postgres 14+
#   com HOT updates (Heap-Only Tuples) onde possível
# - Para workloads write-extreme, considerar storage engine alternativo (MyRocks) ou
#   banco diferente (Cassandra para append-only, ScyllaDB para mixed)

Consistent hashing: por que sharding por range tem hotspots

O problema clássico de sharding: se você usar hash(key) % N e adicionar um servidor, quase todas as chaves precisam migrar (porque o módulo muda). Consistent hashing resolve isso elegantemente: só ~1/N das chaves migra quando N muda em 1.

# Sharding NAIVE com hash modulo:
# servers = [s0, s1, s2]
# shard = hash(key) % len(servers)
# Problema: adicionar s3 → shard = hash(key) % 4
#   75% das chaves agora estão no shard "errado" e precisam migrar

# Sharding por RANGE:
# servers = [s0: keys "a-h", s1: "i-p", s2: "q-z"]
# Problema: distribuição desigual (mais usuários começam com "j-m" que "x-z")
# Hot range: bursts de tráfego em chaves próximas saturam um único shard

# CONSISTENT HASHING:
# 1. Mapear todos os servidores e todas as chaves em um anel (hash circular [0, 2^32))
# 2. Para encontrar o servidor de uma chave: hash(chave), depois andar no sentido horário
#    até encontrar o primeiro servidor no anel
# 3. Adicionar/remover servidor: só as chaves entre o predecessor e o novo servidor migram

# Pseudocódigo:
class ConsistentHashRing:
    def __init__(self, servers):
        self.ring = {}  # hash_value → server
        for s in servers:
            for vnode in range(VIRTUAL_NODES):  # ex: 256 virtual nodes por servidor
                h = hash(f"{s}:{vnode}")
                self.ring[h] = s

    def get_server(self, key):
        h = hash(key)
        # Encontrar o menor hash >= h no anel (binary search no sorted ring)
        sorted_hashes = sorted(self.ring.keys())
        idx = bisect.bisect_right(sorted_hashes, h) % len(sorted_hashes)
        return self.ring[sorted_hashes[idx]]

    def add_server(self, s):
        for vnode in range(VIRTUAL_NODES):
            h = hash(f"{s}:{vnode}")
            self.ring[h] = s
        # Apenas as chaves nos "slots" novos precisam migrar

# Por que VIRTUAL NODES (vnodes)?
# Sem vnodes, com 3 servidores, cada um pega ~1/3 do anel — mas a distribuição
# real depende dos hashes específicos e tende a ser desigual (load skew)
# Com 256 vnodes por servidor (3 × 256 = 768 pontos no anel), a distribuição
# se aproxima muito da uniforme (lei dos grandes números)
# Vantagem adicional: ao adicionar servidor S4, as 256 vnodes de S4 estão
# espalhadas pelo anel → migração vem proporcionalmente de S1, S2, S3
# (não apenas do "vizinho" de S4)

# REPLICAÇÃO em consistent hashing (Dynamo-style):
# Para cada chave, replicar nos próximos N servidores do anel
# Replication factor RF=3: chave vai para o servidor "primary" + 2 vizinhos
# Leitura: contactar R servidores; quorum read W + R > N para consistência
# Escrita: enviar para W servidores; sucesso quando W ACKs recebidos
# Configurações comuns:
#   N=3, W=1, R=1: máxima disponibilidade, eventual consistency (default DynamoDB)
#   N=3, W=2, R=2: quorum, consistency em leitura
#   N=3, W=3, R=1: tudo escrito antes de ack, leitura local

# Quem usa consistent hashing:
# - Cassandra (com tweaks: tokens em vez de vnodes nativos pré-3.0)
# - DynamoDB (interno)
# - Memcached (clients usam libketama)
# - Riak, Voldemort
# - Redis Cluster (com tweak: hash slots, mas mesma ideia)
# - Maglev (Google load balancer)
# - Sistemas de WebSocket sticky session

Replicação: leader-follower, multi-leader e leaderless

# Três modelos fundamentais de replicação:

# 1. LEADER-FOLLOWER (single-leader, master-slave):
# Todos os writes vão para o leader, que replica para followers
# Reads podem ir para leader ou followers (com possível stale data)
# Usado por: Postgres (streaming replication), MySQL, MongoDB, Redis, Kafka (per-partition)

# Replicação síncrona vs assíncrona:
# - Síncrona: o leader espera ACK do follower antes de commitar
#   + RPO = 0 (zero data loss em failover)
#   - Latência inclui RTT até o follower
#   - Se follower está down/lento, writes do leader são bloqueados
# - Assíncrona: leader commit ANTES do follower receber
#   + Latência baixa, leader independente do follower
#   - RPO > 0 (writes não replicados perdem-se se leader cair)
#   - Replication lag visível para reads no follower

# Semi-sync (MySQL): o leader espera ACK de PELO MENOS UM follower
# Postgres: synchronous_commit pode ser local, on, remote_write, remote_apply
#   - remote_write: follower escreveu no buffer (ainda em RAM)
#   - remote_apply: follower aplicou WAL (commit completo)

# Failover:
# - Detecção de falha do leader (heartbeats, geralmente 5-30s)
# - Eleição de novo leader entre os followers
# - Redirecionar writes; aceitar que alguns writes recentes podem ter sido perdidos
# - Replicação reverse: ex-leader (quando voltar) precisa virar follower
# - SPLIT-BRAIN: dois leaders simultâneos pensando que são o leader
#   → resolvido com fencing (STONITH) ou consenso (Raft)

# 2. MULTI-LEADER (multi-master):
# Múltiplos nós aceitam writes, replicam entre si
# Usado em: multi-datacenter (cada DC tem seu leader local)
# Tendências modernas: descontinuado em muitos bancos por complexidade
# Exemplos: MySQL com tools como pt-table-sync; CouchDB; Google Spanner (parcialmente)

# Problema central: CONFLITOS
# Se dois leaders aceitam writes na mesma chave simultaneamente, qual vence?
# Resoluções:
# - Last write wins (LWW): timestamp mais recente vence (perde dados!)
# - Conflito reportado à aplicação: aplicação resolve (complexo)
# - CRDTs: estruturas que convergem matematicamente (G-counter, OR-Set)

# 3. LEADERLESS (peer-to-peer):
# Sem leader; client envia write para múltiplos nós (W) e lê de múltiplos (R)
# Quorum: W + R > N garante read-your-writes consistency
# Usado por: Cassandra, DynamoDB, Riak

# Vantagens:
# + Alta disponibilidade: nenhum nó é especial; qualquer um pode servir qualquer request
# + Failure handling natural: se um nó está down, escreve para os outros que ainda
#   estão no quorum
# + Sem failover: não há leader para falhar

# Desvantagens:
# - Sem ordering global: dois clients podem ver writes em ordens diferentes
# - Resolução de conflito é responsabilidade da aplicação (geralmente LWW)
# - Read repair / hinted handoff: complexidades adicionais para garantir convergência

# Read repair (Cassandra):
# Client lê de R nós; se respondem valores diferentes, escreve o "mais recente" para
# os outros nós para reconciliar — repair on read
# Anti-entropy: processo background que compara hashes de ranges entre nós

# Hinted handoff:
# Se um nó está down quando o write chega, o coordenador armazena um "hint" temporário
# Quando o nó volta, recebe os hints e aplica os writes perdidos

Consensus: Raft em termos práticos (e por que não Paxos)

Quando vários nós precisam concordar em algo (quem é o leader, qual é o próximo entry no log replicado), eles precisam de um protocolo de consenso. Paxos (Leslie Lamport, 1989) é o protocolo clássico mas é notoriamente difícil de entender e implementar correto. Raft (Ongaro & Ousterhout, 2014) foi explicitamente desenhado para ser compreensível — e por isso virou o padrão para sistemas novos.

# RAFT: três estados e três regras

# Estados de cada nó:
# - FOLLOWER: estado default; recebe AppendEntries do leader
# - CANDIDATE: tentando virar leader (após timeout sem heartbeat)
# - LEADER: aceita writes, replica para followers

# Term: número monotonicamente crescente; cada eleição incrementa
# Cada nó conhece o currentTerm; mensagens com term menor são rejeitadas

# Eleição:
# 1. Follower não recebe heartbeat por electionTimeout (150-300ms aleatório)
# 2. Vira CANDIDATE, incrementa currentTerm, vota em si mesmo
# 3. Envia RequestVote para todos os outros
# 4. Cada nó vota em até um candidato por term (regra fundamental)
# 5. Se receber votos da maioria → vira LEADER, envia heartbeats
# 6. Se outro nó for eleito (heartbeat com term ≥ atual) → volta para FOLLOWER
# 7. Se timeout sem ganhar → incrementa term, tenta de novo

# Por que electionTimeout RANDOMIZADO?
# Se todos votassem simultaneamente, vários candidatos empatariam (split vote)
# Randomização garante que UM candidato dispara o timeout primeiro

# Replicação de log:
# Leader recebe write do client
# Adiciona ao próprio log com (term, index, command)
# Envia AppendEntries para followers (com o new entry e o (term, index) do anterior)
# Cada follower:
#   - Verifica que o entry anterior bate (consistência do log)
#   - Se bate: append, retorna sucesso
#   - Se não bate: rejeita; leader manda entries mais antigas até alinhar
# Quando MAIORIA dos followers respondeu sucesso: leader COMMITA o entry
# Próximos heartbeats informam aos followers que esse índice está commited
# Followers commitam → aplicam à state machine

# COMMIT é a chave: somente após a maioria persistir, o entry é considerado durável
# Em caso de partição/falha:
#   - Se o leader cair antes do commit, novos leader pode descartar o entry
#   - Se o leader cair APÓS o commit, novo leader (que tem o entry) replica para os outros

# Garantias do Raft:
# - Leader Completeness: se um entry foi committed no term T, todos os leaders
#   subsequentes (terms > T) terão esse entry no log
# - Safety: dois leaders nunca commitam diferentes valores para o mesmo log index
# - Availability: enquanto maioria dos nós está online, o sistema progride

# Quórum: maioria SIMPLES (N/2 + 1)
# - 3 nós: quorum = 2 (tolera 1 falha)
# - 5 nós: quorum = 3 (tolera 2 falhas)
# - 7 nós: quorum = 4 (tolera 3 falhas)
# Diminishing returns: 5 nós é o sweet spot para a maioria dos sistemas
# Mais nós = mais resiliência, mas mais latência (precisa esperar ACKs de mais nós)

# Quem usa Raft:
# - etcd (Kubernetes config store)
# - Consul (HashiCorp service discovery)
# - TiKV (storage layer do TiDB)
# - CockroachDB (Raft por range de chaves)
# - InfluxDB
# - RethinkDB (descontinuado)
# - Centenas de novos sistemas distribuídos

# PAXOS (resumo):
# Mesmo problema, abordagem diferente. Conceitos:
# - Proposers propõem valores
# - Acceptors votam em valores propostos
# - Learners aprendem o valor decidido
# Two-phase: PREPARE (proposer pede aos acceptors um "ballot number"),
# ACCEPT (proposer propõe valor com ballot number)
# Por que Paxos é difícil:
# - O paper original ("The Part-Time Parliament") é uma metáfora obscura
# - Múltiplas variantes: Multi-Paxos, Cheap Paxos, Fast Paxos, Generalized Paxos
# - Detalhes sutis levam a implementações incorretas (e bugs sutis em produção)
# Quem usa Paxos:
# - Google Chubby (lock service) - original
# - Google Spanner (Multi-Paxos)
# - Apache ZooKeeper (Zab — variante de Paxos)
# - Sistemas legados, principalmente na Google

# Regra prática: para um sistema novo em 2026, escolher Raft, não Paxos

CAP e PACELC: o mapa de decisão

# CAP (Eric Brewer, 2000; provado por Gilbert & Lynch, 2002):
# Num sistema distribuído sujeito a PARTIÇÕES de rede, você escolhe entre:
# - Consistency (C): todos os nós veem o mesmo dado
# - Availability (A): toda request recebe resposta (não-erro)

# Importante: CAP é DURANTE A PARTIÇÃO. Sem partição, todos os sistemas podem ser CA.
# E partição é INEVITÁVEL em sistemas distribuídos suficientemente grandes.
# (network blips, switch failure, cable cut, GC pause longo simulando partição...)

# PACELC (Daniel Abadi, 2010): a extensão prática
# - Se Particionado: escolher entre A e C (CAP)
# - Else (sem partição): escolher entre Latência (L) e Consistency (C)

# Quase todos os sistemas reais têm tradeoff de latência mesmo SEM partição:
# Replicação síncrona = consistência forte mas latência alta
# Replicação assíncrona = baixa latência mas consistência eventual

# Classificação PACELC de bancos comuns:

# PA/EL (disponibilidade + latência; prioriza ambos sobre consistência):
# - DynamoDB (default), Cassandra, Riak, Voldemort
# - Comportamento: sempre responde (mesmo com dado stale); latência consistentemente baixa
# - Use case: shopping cart, sessões, leaderboards onde "alguns segundos atrasado" é OK

# PC/EC (consistência sempre; sacrifica disponibilidade durante partição):
# - HBase, BigTable, Zookeeper, etcd, Consul
# - Comportamento: em caso de partição, minoria fica indisponível; maioria continua
# - Use case: configuração crítica, service discovery, locks distribuídos

# PA/EC (disponibilidade durante partição; consistência sem partição):
# - MongoDB (com majority writeconcern e linearizable read concern)
# - Comportamento híbrido: rotina consistente, mas pode degradar disponibilidade

# PC/EL (consistência durante partição; latência sem partição):
# - PNUTS (Yahoo), original PNUTS-like systems

# Postgres é interessante:
# - Single-instance: ACID, sem partição interna (não distribuído)
# - Streaming replication assíncrona: PA/EL (replicas podem ter stale data)
# - Synchronous replication: PA/EC (espera replica, mas se replica cair pode quebrar)
# - PostgreSQL não é primariamente um sistema distribuído — para multi-master usa
#   ferramentas externas (BDR, Citus) que mudam a classificação

# Em system design:
# A pergunta NÃO é "este banco é AP ou CP" abstratamente
# A pergunta É: "para a operação X no meu produto, o que é pior:
#   receber dado stale, ou não receber resposta?"
# A resposta determina qual banco escolher para qual sub-sistema

# Exemplos práticos:
# - Carrinho de compras Amazon: receber dado stale é OK (resolvemos depois)
#   → AP system (DynamoDB original)
# - Saldo bancário: dado stale é catastrófico (cliente saca dinheiro que não existe)
#   → CP system (RDBMS tradicional ou Spanner)
# - Like contador: dado stale é totalmente OK
#   → AP system, eventual consistency
# - Inventory de e-commerce: cinco pessoas comprando o último item é problema
#   → CP system (ou AP com compensação business-level: cancelar pedidos depois)

Casos práticos: por que cada banco existe

# PostgreSQL — o "general purpose database"
# Strengths:
#   - ACID completo, snapshot isolation via MVCC, transações multi-table
#   - SQL completo, JOINs eficientes, queries ad-hoc
#   - Extensível: JSON, geographic (PostGIS), full-text, time-series (TimescaleDB)
#   - Comunidade enorme, ferramental maduro
# Weaknesses:
#   - Single-writer (master-slave); escala write só com sharding manual
#   - VACUUM como overhead; bloat em workloads write-heavy
# Use cases:
#   - 90% dos sistemas OLTP novos (e-commerce, SaaS, dashboards)
#   - Workloads onde consistência transacional importa
# Sweet spot: até 10k-50k writes/s, até 5TB single-instance

# MySQL (InnoDB) — irmão do Postgres, com tradeoffs diferentes
# Strengths:
#   - Mais simples operacionalmente, replicação master-slave nativa
#   - Foreign keys e transações, mas com isolamento weaker default (READ COMMITTED)
#   - Ecosistema gigante (WordPress, etc)
# Weaknesses:
#   - Menos features avançadas (full-text, geographic são fracos)
#   - Query optimizer historicamente atrás do Postgres
# Use cases:
#   - Aplicações tradicionais; legacy
#   - Casos onde simplicidade operacional importa mais que features

# Cassandra — write-heavy, distribuído, AP
# Strengths:
#   - Linear scalability: adicionar nó = throughput proporcional
#   - Multi-datacenter nativo
#   - Excelente para time-series e workloads append-only
# Weaknesses:
#   - Sem JOINs; queries só por partition key
#   - Tunable consistency é poderoso mas confunde devs
#   - Operação tem learning curve íngreme
# Use cases:
#   - Time-series (métricas, logs)
#   - Messaging history (WhatsApp, Discord, Instagram DMs)
#   - Feeds de atividades
# Sweet spot: 100k+ writes/s, billions de rows

# DynamoDB — serverless AP, gerenciado pela AWS
# Strengths:
#   - Zero ops (autoscaling, backup, replicação automáticos)
#   - Latência consistente (single-digit ms)
#   - Strongly consistent reads opt-in
#   - Global tables (multi-region active-active)
# Weaknesses:
#   - Caro em escala (RCU/WCU billing)
#   - Modelo de dados restrito (sem JOINs, queries limitadas)
#   - Lock-in à AWS
# Use cases:
#   - Workloads serverless (Lambda + DynamoDB é stack natural)
#   - Casos com throughput muito variável (autoscaling vence)
# Sweet spot: até 100k req/s, single-digit ms P99

# CockroachDB — distributed SQL, CP, geo-distribuído
# Strengths:
#   - SQL completo (drop-in para Postgres em muitos casos)
#   - Distribuído nativamente: shard automático com consistent hashing
#   - Multi-region com latência otimizada
#   - Serializable isolation
# Weaknesses:
#   - Latência maior que Postgres single-instance (consensus overhead)
#   - Custo (licença enterprise para algumas features)
#   - Ecosistema menor que Postgres
# Use cases:
#   - Aplicações que precisam de SQL + escala global
#   - Quando Postgres single-instance vai começar a apertar mas ainda preciso de SQL

# MongoDB — document store, foi AP, agora ambíguo
# Strengths:
#   - Schema flexível (bom para iteração rápida)
#   - JSON nativo (boa fit para JavaScript stacks)
#   - Aggregation pipeline poderoso
# Weaknesses:
#   - Historicamente teve problemas de durabilidade (defaults perigosos)
#   - JOINs ($lookup) são caros e tarde
#   - Sharding tem armadilhas operacionais
# Use cases:
#   - Document storage natural (catálogos com schemas variados)
#   - Stack JavaScript end-to-end
# Sweet spot: quando o modelo de dados é genuinamente document-oriented

# Redis — in-memory, key-value, single-thread
# Strengths:
#   - Latência sub-milissegundo (in-memory)
#   - Estruturas de dados ricas (sorted sets, hyperloglog, geo, streams)
#   - Pub/Sub nativo
# Weaknesses:
#   - Persistência opcional (snapshots ou AOF — tradeoffs)
#   - Single-threaded por shard
#   - Cluster mode tem limitações (multi-key ops restritas)
# Use cases:
#   - Cache (uso #1)
#   - Sessions, rate limiting, leaderboards
#   - Filas de jobs (com cuidado — RabbitMQ/Kafka melhor para mensagens persistentes)

Arquitetura comparativa: por que cada banco se parece com o que se parece

POSTGRES (single-instance, traditional)
┌──────────────────────────────────┐
│  Clients                          │
└─────────────┬────────────────────┘
              │ TCP / SQL protocol
              ▼
┌──────────────────────────────────┐
│  postmaster (parent process)     │
│    └── postgres backend (per conn)│
│         ├── Query parser          │
│         ├── Planner / optimizer   │
│         ├── Executor              │
│         └── Buffer cache (RAM)    │
└─────────────┬────────────────────┘
              │
              ▼
┌──────────────────────────────────┐
│  WAL (write-ahead log)            │
│  Heap files (B-tree storage)      │
│  Indexes (B-tree, GiST, BRIN, ...)│
└──────────────────────────────────┘
              │ async streaming
              ▼
       Read replicas (followers)

CASSANDRA (peer-to-peer, no leader)
        Client
          │ chooses any coordinator
          ▼
┌──────────────────────────────────┐
│  Coordinator (any node, rotating)│
│  - Decide replicas via ring      │
│  - Send to N nodes (RF=3 typical)│
│  - Wait for W ACKs (quorum)      │
└─────────────┬────────────────────┘
              │ gossip + native protocol
              ▼
   ┌──────────┼──────────┐
   ▼          ▼          ▼
 Node A    Node B    Node C    Node D    ...
 (LSM:     (LSM)     (LSM)     (LSM)
  memtable
  + sstables
  + bloom
  + WAL)

Cada nó tem o anel completo em memória → routing local
Replication factor configurável por keyspace
Read e Write consistency tunáveis (ONE, QUORUM, ALL)

COCKROACHDB (distributed SQL, Raft per range)
        SQL Client
          │
          ▼
   Gateway node (any node)
          │ SQL parser, planner
          ▼
   Distributed execution
          │
   ┌──────┼──────┐
   ▼      ▼      ▼
 Range1  Range2  Range3  (key-space dividido em ranges de ~512MB)
   │      │      │
 Raft   Raft   Raft     cada range é um grupo Raft separado
 group  group  group
   │      │      │
 ┌─┴─┐  ┌─┴─┐  ┌─┴─┐
 │N1N2│  │N2N3│  │N1N3│  réplicas em 3 nós diferentes (RF=3)
 │N3 │  │N1 │  │N2 │   leader rotacional para load balancing
 └───┘  └───┘  └───┘

Quando uma transação toca múltiplas ranges: 2PC sobre os Raft groups envolvidos

REDIS CLUSTER (single-thread shards)
   Client (cluster-aware)
       │ knows hash slot map
       ▼
   ┌───┴───┐
   ▼   ▼   ▼
  M1  M2  M3   masters (cada um dono de ~5461 hash slots de 16384)
   │   │   │
  R1  R2  R3   replicas (async replication)

Hash slot = CRC16(key) % 16384
MOVED redirect: se client manda para shard errado, recebe MOVED e refaz
Resharding: mover slots entre masters em batch (sem downtime)
o motivo pelo qual quase ninguém precisa de "distributed SQL"

Sistemas como CockroachDB e Spanner prometem o melhor dos dois mundos: SQL completo com distribuição transparente. Em escala, eles são impressionantes. Mas a grande maioria dos sistemas que escolhem distributed SQL em 2026 ainda caberiam confortavelmente num único Postgres bem dimensionado. Um Postgres em hardware moderno (64 cores, 512GB RAM, NVMe) suporta 50k writes/s e 200k reads/s — o que cobre 99% dos SaaS. A migração para distributed SQL adiciona latência de consensus (mínimo 2-5ms em vez de sub-ms), complica operação (versioning, rolling upgrades, debugging distribuído), e geralmente vem com custos significativos. A pergunta a se fazer antes de adotar distributed SQL: "estou perto de saturar uma instância single-master, e o crescimento projetado de 2 anos ultrapassa o que uma única instância maior aguenta?" Se a resposta é não, distributed SQL provavelmente está resolvendo um problema que você não tem.

Decisões de engenharia

SQL vs NoSQL — quando cada um faz sentido

SQL é a escolha default para qualquer sistema novo onde a estrutura dos dados é estável, há queries ad-hoc, transações multi-row são necessárias, ou o time já tem fluência em SQL. Postgres ou MySQL suportam milhões de usuários sem necessidade de NoSQL — a maioria dos cases que se acreditam precisar de NoSQL na verdade são apenas mal-modelados em SQL. NoSQL faz sentido em três casos claros: (1) padrão de acesso é estritamente por chave primária e schema é volátil (DynamoDB para serverless); (2) workload é write-heavy time-series ou append-only em escala onde Postgres satura (Cassandra para logs/metrics/feeds); (3) modelo de dados é genuinamente document-oriented com schemas variáveis (MongoDB para catálogos heterogêneos).

Regra prática: começar com Postgres. Migrar para NoSQL quando há evidência empírica de que a estrutura não cabe (não baseado em projeção otimista de escala). Sistemas híbridos são comuns e saudáveis: Postgres para core OLTP, Redis para cache/sessions, Cassandra ou DynamoDB para timeseries/feeds, Elasticsearch para search. Não é "escolher um banco" — é escolher qual banco para qual sub-sistema.

Replicação síncrona vs assíncrona

Síncrona dá RPO = 0 (zero perda de dados em failover) mas inflige latência: cada write precisa do RTT até a replica. Em WAN (replica em outro DC), isso vira 50-100ms — inaceitável para a maioria dos workloads transacionais. Assíncrona tem latência baixa mas RPO > 0: writes recentes podem ser perdidos se o leader cai antes de replicar. Semi-sync (espera ACK de pelo menos UM follower) é o meio-termo: protege contra perda na maioria dos cenários, latência aceitável.

Regra prática: assíncrona para a maioria dos sistemas web; semi-sync para sistemas financeiros ou onde RPO importa. Síncrona total apenas para sub-sistemas críticos com replica local (LAN, sub-ms RTT). Para multi-DC, geralmente assíncrona com aceitação explícita de perda de últimos N segundos em desastre — a alternativa é latência inaceitável. CockroachDB e Spanner usam consensus (efetivamente síncrono via Raft/Paxos), mas pagam o preço em latência mesmo no caso normal.

Sharding por hash vs por range vs por geografia

Hash sharding (consistent hash) distribui uniformemente, evita hotspots de chaves consecutivas, mas torna range queries proibitivas (precisa scan todos os shards). Range sharding mantém chaves próximas no mesmo shard, range queries são eficientes, mas pode criar hotspots em workloads onde chaves recentes recebem mais tráfego (ex: time-series com timestamp como key sharding). Geographic sharding coloca dados perto dos usuários — ótimo para latência mas adiciona complexidade quando um usuário acessa dados de outra região.

Regra prática: hash para workloads OLTP onde queries são por chave (Cassandra, DynamoDB, Redis Cluster). Range para time-series com paging cronológico (com cuidado: incluir hash prefix na key para evitar hot shard recente). Geographic para apps globais com requisitos de latência regional (Spanner, CockroachDB com locality config). Híbrido é comum: shard primary por geography, secondary por hash dentro de cada região.

Strong consistency vs eventual consistency

Strong consistency (linearizability) faz toda operação se comportar como se fosse a única no sistema — extremamente simples para o developer raciocinar, mas custa latência (consensus, sync replication) e disponibilidade durante partições. Eventual consistency é eficiente e disponível, mas força o developer a pensar em "que aconteceria se duas réplicas tivessem visões diferentes" — modelo mental mais complexo. Snapshot isolation (MVCC, oferecido pelo Postgres) é um meio-termo poderoso: dentro de uma transação, você vê um snapshot consistente; mas concurrent updates podem causar serialization anomalies sutis.

Regra prática: para operações financeiras, inventory crítico, locks distribuídos → strong consistency, mesmo custando latência. Para feeds, contadores, métricas, leaderboards, social actions → eventual consistency é aceitável e libera escala. Para a maioria do core OLTP de SaaS → snapshot isolation (Postgres) é o sweet spot: rápido, suficiente, fácil de raciocinar. Decidir explicitamente por operação, não globalmente por sistema — diferentes operações têm requisitos diferentes.

Perguntas de entrevista

Por que Cassandra é melhor que Postgres para writes em alta escala? E por que isso não significa que Cassandra é "melhor" em geral?

A diferença está no storage engine — não é magia, é matemática de I/O.

Postgres (B-tree): cada write precisa (1) encontrar a página correta no índice, (2) modificar a página (random write se não está em buffer), (3) atualizar índices secundários (mais random writes), (4) fsync do WAL. Random writes em SSD são ~10x mais lentos que sequential. Em HDD, 100x. Postgres single-instance satura em ~10-50k writes/s dependendo do hardware e workload.

Cassandra (LSM tree): write é (1) append no commit log (sequential), (2) insert no memtable (in-memory). É isso. Flush para SSTable acontece em background, sem bloquear writes. Sequential writes saturam a banda do disco (GB/s em SSD), não os IOPS. Single Cassandra node aguenta 50-100k writes/s consistentemente; cluster escala linearmente (adicionar node = throughput proporcional).

O custo: reads. Em Cassandra, um read pode precisar verificar memtable + 5-10 SSTables. Bloom filters reduzem o overhead (eliminam SSTables que garantidamente não têm a chave), mas no pior caso é múltiplos seeks. Postgres faz 1-3 seeks no pior caso. Para workload mixed (70% read, 30% write), Postgres geralmente ganha em latência P99.

Outras razões pelas quais Cassandra não é "melhor": sem JOINs (modelar relacionamentos é trabalho da aplicação), sem transações multi-row (apenas write batch sem ACID), schema rígido em torno da partition key (queries fora desse padrão são impossíveis sem secondary index, que tem limitações), e operação complexa (compaction tuning, gossip, repairs).

A pergunta certa: "minha workload é dominada por writes (>50%), em padrão de acesso por chave, com aceitação de eventual consistency?" Se sim, Cassandra. Senão, Postgres com sharding manual quando precisar é geralmente melhor que migrar tudo para Cassandra.

O que é split-brain em sistemas com replicação, e como Raft resolve?

Split-brain acontece quando uma partição de rede faz com que dois (ou mais) nós acreditem ser o leader simultaneamente, ambos aceitando writes. Quando a partição se cura, há divergência irreconciliável: writes feitos em ambos os "leaders" entram em conflito.

Cenário clássico (sem proteção): cluster de 3 nós (A, B, C). A é o leader. Network partition isola A em uma metade e B, C na outra. A continua aceitando writes (acha que é o leader). B e C disparam election timeout e elegem B como novo leader; B aceita writes. Cura da partição: agora temos two "histórias" de writes incompatíveis.

Raft resolve com a regra de QUORUM: um nó só pode ser leader se conseguiu votos da MAIORIA. Em 3 nós, isso significa 2 votos. Quando a partição isola A, A NÃO tem maioria — apenas 1 voto (o próprio). B e C, na outra partição, conseguem quorum (2 de 3). Apenas B vira leader. A continua "achando" que é leader, mas qualquer write que tenta replicar falha (não consegue ACK de outros 2 nós). A não progride; B sim.

Quando A volta: recebe AppendEntries de B com term > currentTerm de A. A imediatamente reverte para follower e adota o log de B (descartando writes não-commited que A tinha pendentes). Não há divergência permanente.

O insight: a regra "writes precisam de maioria" impede que duas metades em partição ambas aceitem writes. Uma das metades é matematicamente forçada a ser a minoria — e a minoria não pode aceitar writes. Apenas N=2 falha aqui (1+1, sem maioria possível); por isso Raft requer N≥3.

Sistemas que NÃO usam consensus (Postgres com hot standby manual, MySQL replication) podem sofrer split-brain real e precisam de fencing externo (STONITH — "shoot the other node in the head") ou aceitar reconciliação manual após failover.

Como você decide entre 3, 5 ou 7 nós num cluster Raft? Mais nós sempre é melhor?

Diminishing returns acentuados — mais nós não é estritamente melhor, e há razões matemáticas claras para escolher um número específico.

Tolerância a falhas: Raft tolera (N-1)/2 falhas. Para 3 nós: 1 falha tolerada. Para 5: 2 falhas. Para 7: 3 falhas. Mas note que de 3→5 dobra a tolerância (1→2 falhas), enquanto de 5→7 só adiciona 1 falha tolerada — retorno decrescente.

Latência de write: cada write precisa de ACK da maioria. Em 3 nós, esperar 2 ACKs. Em 5, esperar 3 ACKs. Em 7, esperar 4 ACKs. Mais nós = pior latência P99 porque a probabilidade de ter pelo menos um nó lento aumenta com mais participantes.

Custo: linearmente proporcional ao número de nós (CPU, RAM, network bandwidth para replicação). 7 nós custam mais do dobro de 3 nós em infraestrutura.

Sweet spots práticos:

3 nós: default para cluster pequeno/médio. Tolera 1 falha — geralmente suficiente para mantenance windows (rolling restart). Não tolera 2 falhas simultâneas — risco aceitável para sistemas não-críticos.

5 nós: default para sistemas onde uptime importa. Tolera 2 falhas: você pode fazer manutenção em 1 nó (planejado) e ainda sobreviver a 1 falha não planejada simultânea. É o tamanho recomendado para etcd em produção.

7+ nós: raro. Só faz sentido se o custo de uma falha é catastrófico E há histórico de múltiplas falhas concorrentes. Para sistemas geo-distribuídos com 3 regiões, pode-se ter 7 nós (3-3-1) para sobreviver a perda de uma região inteira + 1 nó adicional.

Pegadinha — número PAR: 4 nós são piores que 3. Tolerância é a mesma (1 falha), mas precisa de mais ACKs (3 de 4 vs 2 de 3). Sempre use número ímpar.

Witness/learner nodes: alguns sistemas suportam nós que participam de quorum mas não armazenam dados (witness) ou armazenam dados mas não votam (learner). Permite configurações exóticas como "5 nós de quorum + 10 read replicas".

Por que Spanner consegue ser CP e ainda ter latência baixa?

Spanner (Google) é uma anomalia interessante porque tradicionalmente CP+latência baixa é considerado tradeoff impossível em sistemas geograficamente distribuídos. O segredo é uma combinação rara: hardware especializado + arquitetura cuidadosa.

TrueTime API: a inovação central. Google instalou GPS receivers e relógios atômicos em cada datacenter. Cada servidor sabe a hora com margem de erro conhecida (geralmente <7ms). A API TT.now() retorna não um timestamp, mas um intervalo [earliest, latest] — o tempo está garantidamente nesse intervalo.

Como isso ajuda transações: em Spanner, cada transação recebe um timestamp do TrueTime. Para commit, a transação espera ATÉ que now > latest_da_transação — geralmente alguns milissegundos. Isso garante que nenhuma outra transação posterior pode receber um timestamp anterior. O resultado: serializabilidade externa (toda transação tem ordering global consistente com tempo real).

Paxos por fragmento: cada fragmento de dados (tablet) tem seu próprio grupo Paxos. Writes para um tablet são consensus dentro daquele grupo — geralmente todos os participantes na mesma região, então latência é ~5ms. Apenas transações que cruzam regiões pagam latência inter-região.

Latência típica: reads single-row: ~5ms. Writes single-row: ~10ms. Transações multi-row dentro de uma região: 10-20ms. Transações cross-region: 50-200ms (limitado pela velocidade da luz entre DCs).

Por que isso não é replicável fora do Google: a TrueTime depende de hardware proprietário (GPS + relógios atômicos por DC). Implementações abertas (CockroachDB) aproximam com NTP, mas a margem de erro é maior (~100ms tipicamente), o que reduz throughput máximo de transações com timestamps únicos. CockroachDB compensa com técnicas como "hybrid logical clocks" e aceita serializability mas não linearizability externa.

Lição: CAP é sobre tradeoffs em sistemas commodity. Com hardware especializado e investimento massivo em infra, é possível "trapacear" o teorema (na verdade, reduzir o espaço de partições reais a quase zero). Mas isso não é uma opção para 99,9% dos sistemas.

Um sistema usa Postgres com replicação async para read replicas. Um usuário reclama que após criar um post, ele não aparece imediatamente quando recarrega a página. Como explicar e resolver?

Este é o problema clássico de read-your-writes consistency em sistemas com replicação assíncrona — e a explicação para o usuário precisa começar por entender o que está acontecendo.

O que está acontecendo: o write foi para o primary (master). A leitura subsequente foi roteada para uma read replica (load balancer distribui leituras entre replicas para escalar). A replicação async tem lag de alguns ms a alguns segundos. Durante esse lag, o post existe no primary mas não na replica que serviu a leitura. Resultado: usuário vê página sem o post que acabou de criar.

Soluções, em ordem de complexidade:

1. Ler do primary após writes (write-then-read pinning): após qualquer write, marcar a sessão do usuário com flag last_write_at = now(). Leituras durante os próximos N segundos (ex: 10s — maior que o lag máximo esperado) vão para o primary, não para replica. Simples; funciona para 99% dos casos. Custo: aumenta carga no primary.

2. Sticky session por usuário: uma vez que um usuário fez write, todos os reads dele vão para o primary por X segundos. Variante do anterior, mais granular.

3. Read your own writes via versioning: ao fazer write, retornar o LSN (log sequence number) do Postgres. Cliente envia esse LSN nas próximas leituras. Backend só usa a replica se a replica replicou ATÉ esse LSN; senão, vai para primary. Mais complexo mas precisa: usuário A vê seus próprios writes imediatamente; outros usuários veem com lag normal.

4. Synchronous replication para essa operação específica: usar synchronous_commit=remote_apply apenas para operações críticas. O write não retorna até a replica ter aplicado. Resolve mas custa latência em cada write — geralmente overkill.

5. Optimistic update no cliente: após o write retornar sucesso, o cliente atualiza a UI localmente (mostra o post imediatamente) sem aguardar refetch do servidor. Quando o refetch acontece, pode haver discrepância momentânea, mas para a UX é instantâneo.

Na prática: combinação de (1) e (5) é o padrão. Read-your-writes pinning resolve correção do servidor; optimistic update resolve UX percebida. Para cases mais críticos (banking, healthcare), considerar (3) ou (4).

Exercícios práticos

Exercício 1 — Implementar um mini LSM tree

Implemente um key-value store baseado em LSM tree em Python ou Go. Componentes: (1) Memtable como sorted dict in-memory; (2) ao atingir threshold (ex: 1000 entries), flush para um SSTable em disco (arquivo sorted serializado); (3) leitura: verificar memtable, depois SSTables do mais novo para o mais antigo; (4) bloom filter por SSTable para evitar lookups desnecessários. Testar com 100k inserts + 10k reads, comparar throughput de write com um dict simples persistido (que precisa rewrite a cada flush).

Critério: writes são significativamente mais rápidos que escrita persistente naïve (medir ops/s). Reads de chaves existentes funcionam corretamente após múltiplos flushes. Bloom filter reduz lookups desnecessários (medir % de SSTables consultados desnecessariamente sem o bloom). Bonus: implementar compaction simples (merge de 2 SSTables consecutivos em um maior).

Exercício 2 — Consistent hash ring com virtual nodes

Implemente uma classe ConsistentHashRing que mapeia chaves para servidores usando consistent hashing com virtual nodes. Métodos: add_server, remove_server, get_server(key). Testar com 1M chaves distribuídas em 5 servidores: verificar que a distribuição é uniforme (cada servidor pega ~20% das chaves, com desvio <5%). Adicionar um 6º servidor: medir qual fração das chaves migra (deve ser ~1/6 = 17%, não ~83% como em hash modulo).

Critério: distribuição uniforme com 256 vnodes por servidor (medir variância). Ao adicionar/remover servidor, apenas ~1/N das chaves mudam de servidor (verificar comparando antes/depois). get_server tem latência O(log V) onde V = total de vnodes. Bonus: implementar replication factor — função get_servers(key, n) que retorna os próximos N servidores no anel.

Exercício 3 — Simulação de Raft (eleição apenas)

Implemente apenas a parte de eleição do Raft com 5 nós simulados como processos ou goroutines. Cada nó tem currentTerm, votedFor, state (follower/candidate/leader). Implementar electionTimeout randomizado (150-300ms), RequestVote RPC, e HeartBeat do leader. Testar: matar o leader e verificar que um novo é eleito em <1s. Particionar a rede (impedir comunicação entre 2 nós e os outros 3) e verificar que a maioria (3 nós) elege novo leader, enquanto a minoria (2 nós) NÃO elege.

Critério: em condições normais, exatamente 1 leader em qualquer momento. Após morte do leader, novo leader é eleito em <1s. Em partição, apenas a maioria progride; minoria fica sem leader. Currentterm é monotonicamente crescente. Bonus: implementar log replication (AppendEntries) e demonstrar que entries são commited apenas quando maioria os recebeu.

Exercício 4 — MVCC simplificado em Python

Implemente um in-memory key-value store com MVCC. Cada chave tem múltiplas versões com (xmin, xmax, value). Transações têm snapshot (xmin_snapshot, xip_list). Operações: begin(), read(tx, key), write(tx, key, value), commit(tx). Implementar regra de visibilidade. Testar: T1 começa, T2 começa, T1 escreve x=10 e commit, T2 ainda lê x = valor antigo (snapshot do T2 não inclui o commit de T1).

Critério: snapshot isolation correto — T2 vê x consistente com o momento do begin(), não com writes de outras TXs commited durante T2. T2 não bloqueia leitura quando T1 está escrevendo. Implementar vacuum simples: remover versões cujo xmax < menor xmin ativo (não visíveis para nenhuma TX). Bonus: detectar serialization conflicts (T1 e T2 ambos escrevem mesma chave → uma falha em commit).

Exercício 5 — Comparar tradeoffs CAP com Postgres + replica

Configure um setup local: Postgres primary + 1 replica streaming async. Mida: (1) latência de write no primary; (2) latência de leitura no primary vs na replica; (3) replication lag em condições normais (alguns ms); (4) simular partição (parar a replica) e medir o que acontece com writes no primary. Depois, configurar replication síncrona (synchronous_commit=on com synchronous_standby_names) e repetir as medições. Documentar os tradeoffs observados.

Critério: tabela comparativa com números reais: latência write async vs sync; comportamento durante "falha" da replica em ambos os modos; replication lag típico vs P99. Demonstrar empiricamente o tradeoff CAP/PACELC — em sync, a queda da replica pausa writes (CP); em async, writes continuam mas há perda de dados potencial (AP). Bonus: implementar read-your-writes pinning no client (após write, ler do primary por 10s).

Referências

  1. book Martin Kleppmann — Designing Data-Intensive Applications O'Reilly · 2017 · o livro definitivo para entender storage engines, replication, consensus, e tradeoffs. Capítulos 3 (storage), 5 (replication), 7 (transactions), 9 (consistency) são essenciais para system design
  2. paper DeCandia et al. — Dynamo: Amazon's Highly Available Key-value Store SOSP · 2007 · o paper que inspirou Cassandra, Riak, DynamoDB. Apresenta consistent hashing, vector clocks, e gossip protocol em contexto real
  3. paper Ongaro & Ousterhout — In Search of an Understandable Consensus Algorithm (Raft) USENIX ATC · 2014 · o paper original do Raft, explicitamente desenhado para ser entendível. Leitura obrigatória para quem implementa sistemas distribuídos
  4. article Ongaro — Raft Visualization (raft.github.io) raft.github.io · visualização interativa do Raft em ação — você roda eleições, simula partições, observa replication. Pedagogia excepcional
  5. paper Lamport — Paxos Made Simple 2001 · a segunda tentativa de Lamport de explicar Paxos (a primeira foi a metáfora "Part-Time Parliament"). Ainda denso, mas o ponto de partida canônico se você precisa entender Paxos
  6. paper Brewer — Towards Robust Distributed Systems (CAP) PODC · 2000 · a keynote original onde Brewer apresentou CAP. Posteriormente formalizado por Gilbert & Lynch (2002). Os 12 slides são curtos mas seminais
  7. article Abadi — CAP and PACELC dbmsmusings.blogspot.com · introdução à extensão PACELC do CAP. Mostra como analisar bancos sem partição, focando em tradeoff latência vs consistência
  8. docs PostgreSQL — Internals Documentation postgresql.org/docs/current/internals.html · documentação oficial dos internals: storage layout, WAL, MVCC, planner. Profundidade rara para um banco open-source
  9. paper O'Neil et al. — The Log-Structured Merge-Tree (LSM-Tree) Acta Informatica · 1996 · o paper original que introduziu LSM tree. A base teórica para Cassandra, RocksDB, e dezenas de sistemas modernos
  10. paper Corbett et al. — Spanner: Google's Globally-Distributed Database OSDI · 2012 · o paper que apresenta TrueTime e arquitetura do Spanner. Demonstra como hardware especializado permite consistency forte em escala global
  11. article Wikipedia + Jepsen — Distributed Systems Reading jepsen.io/analyses · análises empíricas de Kyle Kingsbury de claims de consistency em sistemas reais (MongoDB, Cassandra, CockroachDB, etc). Quebra mitos de marketing com testes rigorosos
  12. book Petrov — Database Internals O'Reilly · 2019 · complementa Kleppmann com mais profundidade técnica em storage engines (B-tree, LSM), consensus algorithms, e gossip protocols. Mais "como implementar" que "como decidir"