Pular para conteúdo

CAP e PACELC

O BoxOffice chega à Europa. Para dar aos fãs em Lisboa a mesma experiência rápida dos fãs em São Paulo, o time roda o banco do inventory em duas regiões, cada uma com uma cópia completa dos assentos, replicando entre si. Numa tarde, durante a abertura das vendas de um festival, o enlace transatlântico entre as regiões degrada e depois cai. As duas regiões estão vivas; as duas continuam recebendo requisições "Reservar assento A-12". Cada região tem agora duas opções para cada requisição: responder com o que sabe (e arriscar vender o assento A-12 dos dois lados do oceano) ou recusar até o enlace voltar (e perder a venda). Não há terceira opção. É esse dilema que o teorema CAP formaliza.

O teorema CAP, conjecturado por Eric Brewer em 2000 e provado por Seth Gilbert e Nancy Lynch em 20021, afirma que um armazenamento de dados replicado não consegue garantir ao mesmo tempo as três propriedades a seguir:

Teorema CAP Três círculos sobrepostos: Consistência, Disponibilidade e Tolerância a partições. As interseções são CP e AP, as duas escolhas reais, e CA, que só existe sem partições. O centro, as três ao mesmo tempo, é impossível. Consistência toda leitura vê a última escrita Disponibilidade toda requisição recebe uma resposta sem erro Tolerância a partições funciona com a rede dividida CP ZooKeeper, etcd AP Cassandra, DNS CA sem partições (um único nó) as três: impossível Partições vão acontecer: a escolha real é entre CP e AP — durante a partição.


As três propriedades

Consistência (C) : Toda leitura devolve a escrita mais recente, ou um erro. No CAP, "consistência" significa linearizabilidade: as réplicas se comportam como se houvesse uma única cópia (veja Linearizabilidade).

Disponibilidade (A, availability) : Toda requisição recebida por um nó que não falhou recebe uma resposta que não é erro — mesmo que ela não contenha a escrita mais recente. Note o rigor: todo nó que não falhou, não "na maior parte do tempo".

Tolerância a partições (P) : O sistema continua operando quando a rede se particiona — divide os nós em grupos que estão vivos, mas não conseguem trocar mensagens (ou as trocam com atraso ilimitado).

P não é opcional

Partições não são uma escolha de projeto; elas acontecem — um switch falha, um cabo é cortado, o enlace de uma região de nuvem degrada, uma pausa longa de GC faz um nó parecer inacessível. Qualquer sistema cujos dados vivem em mais de uma máquina precisa decidir o que fazer quando elas acontecem. Então a escolha real é C ou A, durante uma partição. O próprio Brewer, doze anos depois, ressaltou que partições são raras e que o trabalho de projeto interessante está em detectá-las, operar em modo limitado enquanto duram e se recuperar depois4.

O C do CAP não é o C do ACID

Em transações ACID, consistência significa "a transação preserva as invariantes" (restrições, chaves estrangeiras, CHECK). No CAP significa linearizabilidade. Um único servidor PostgreSQL é consistente no sentido ACID e o CAP nem se aplica a ele — o CAP trata de réplicas.


Uma partição, passo a passo

A mesma requisição, tratada por um sistema que escolhe consistência (CP) e por um que escolhe disponibilidade (AP):

Experimente

Desligue o tráfego aleatório para ver uma requisição por vez. Em CP · PC/EC, reserve um assento em qualquer lado: São Paulo (a região de origem do evento, onde fica o líder) só responde depois que Lisboa gravou a reserva — 184 ms, a ida e volta oceânica de 180 ms mais 4 ms de trabalho local, em toda escrita. Agora particione o link: Lisboa responde 503 a leituras e escritas (não tem como saber se sua cópia está atual), São Paulo continua vendendo; desfaça a partição e Lisboa só se atualiza — reservas confirmadas perdidas continua 0. Recomece em AP · PA/EL: toda reserva é confirmada em 4 ms, mas uma leitura em Lisboa menos de 90 ms depois de uma reserva em São Paulo é velha. Particione, reserve duas vezes em São Paulo e depois uma em Lisboa, e desfaça a partição: o last-write-wins mantém nos dois lados o valor mais recente, de Lisboa, e 2 reservas confirmadas se perdem — dois fãs ouviram "reservado" para assentos que podem ser vendidos de novo.

Nenhuma das respostas é "errada" em abstrato: o sistema CP recusa em vez de arriscar uma venda em dobro; o sistema AP atende todo fã e precisa reconciliar depois. Qual das duas é aceitável depende dos dados e do negócio — não da marca do banco de dados.

Pense antes: existe um jeito de continuar vendendo assentos dos dois lados do oceano e nunca vender um assento duas vezes?

Sim — não replicando a decisão. Dê a cada evento uma região de origem (a região do local do show) e mande toda reserva daquele evento para o primário da sua região de origem. Dentro de uma região, os dados são consistentes; entre regiões, não há decisão compartilhada sobre a qual discordar. Durante uma partição transatlântica, uma fã em Lisboa comprando um show em São Paulo não alcança a região de origem e é recusada (CP para aquele evento), mas todo evento cuja origem está acessível continua vendendo. Você não escapou do CAP — você reduziu o raio de impacto da escolha entre C e A às requisições que realmente atravessam a partição. Veja Projetos multirregião.


Sistemas CP e AP

CP — consistente e tolerante a partições

Durante uma partição, os nós que não conseguem confirmar o estado mais recente (tipicamente o lado minoritário) recusam requisições. Quando a partição se resolve, eles se atualizam antes de voltar a atender.

flowchart LR
    CA(["Fã A"]) -->|"1 · reservar A-12"| A[("Nó A (maioria)<br/>A-12 = HELD")]
    A -.-x|"2 · replicar ✗ partição"| B[("Nó B (minoria)<br/>A-12 = FREE")]
    CB(["Fã B"]) -->|"3 · A-12 está livre?"| B
    B -->|"4 · erro: sem quórum"| CB
  • Exemplos: etcd, ZooKeeper, Consul, Spanner, CockroachDB; PostgreSQL com replicação síncrona; MongoDB com leituras e escritas por maioria.
  • Use quando: uma resposta errada é pior do que nenhuma resposta — estoque, dinheiro, identidade, liderança, locks.

AP — disponível e tolerante a partições

Durante uma partição, todo nó continua respondendo com os dados que tem. As réplicas divergem e convergem depois que a partição se resolve (consistência eventual), resolvendo conflitos com LWW, vetores de versão ou CRDTs (Consistência eventual).

flowchart LR
    CA(["Fã A"]) -->|"1 · curtir evento 42"| A[("Nó A<br/>curtidas = 101")]
    A -.-x|"2 · replicar ✗ partição"| B[("Nó B<br/>curtidas = 100")]
    CB(["Fã B"]) -->|"3 · ler curtidas"| B
    B -->|"4 · 100 (desatualizado)"| CB
    A ==>|"5 · partição resolvida: merge"| B
  • Exemplos: Cassandra e DynamoDB nas configurações padrão, CouchDB, Riak, DNS, CDNs, a maioria dos caches.
  • Use quando: uma resposta um pouco desatualizada é melhor do que nenhuma — catálogo, recomendações, contadores, carrinhos, feeds.

"CA"

Um sistema consistente e disponível enquanto não houver partições — na prática, um único nó, ou um cluster que simplesmente não decidiu o que fazer numa partição. Não é uma terceira opção real para um sistema distribuído.

CP AP
Durante uma partição O lado minoritário recusa (erros, timeouts) Todo nó responde, talvez desatualizado
Depois da partição A minoria se atualiza, nada a reconciliar As réplicas convergem; conflitos precisam ser resolvidos
Garantia Linearizável Eventual (ou causal, no máximo)
Falha visível ao usuário "Tente novamente mais tarde" Dado errado ou desatualizado, corrigido depois
Exemplos etcd, ZooKeeper, Spanner, CockroachDB, PostgreSQL com replicação síncrona Cassandra, DynamoDB (padrão), DNS, CDN, caches
Dados do BoxOffice Assentos, reservas, pedidos, pagamentos, sessões de autenticação Catálogo, imagens, contadores, recomendações

CP e AP descrevem uma configuração, não um produto

A maioria dos bancos de dados permite ajustar o trade-off — até por requisição. O Cassandra com leituras e escritas QUORUM se comporta muito mais como um sistema CP; o PostgreSQL servindo leituras de uma réplica assíncrona devolve dados desatualizados durante uma partição. Muitos sistemas reais não são, a rigor, nem CP nem AP: podem ficar indisponíveis e devolver dados desatualizados em alguns modos de falha2. Pergunte "como ele está configurado, e o que minhas requisições veem durante uma partição?" em vez de "este banco é CP ou AP?".


PACELC: o trade-off que você paga todo dia

O CAP só fala de partições, que são raras. O PACELC de Daniel Abadi (2010–2012)3 acrescenta o trade-off que existe o tempo todo:

Se há uma Partição, escolha entre disponibilidade (A) e Consistência; E, senão (else, operação normal), escolha entre Latência e Consistência.

flowchart LR
    Q{"Há uma<br/>partição?"} -->|"sim · P"| PAC{"Disponibilidade<br/>ou Consistência?"}
    PAC -->|A| PA["PA: responde, talvez desatualizado"]
    PAC -->|C| PC["PC: recusa ou espera"]
    Q -->|"não · E (senão)"| ELC{"Latência<br/>ou Consistência?"}
    ELC -->|L| EL["EL: responde antes de<br/>as réplicas confirmarem"]
    ELC -->|C| EC["EC: espera<br/>as réplicas"]

O ramo else existe por causa da própria replicação: para ser consistente, uma escrita precisa esperar outras réplicas confirmarem (uma ou mais idas e voltas pela rede, talvez até outro continente); para ser rápida, precisa responder antes delas e aceitar que uma réplica pode ficar brevemente para trás.

Quanta latência a consistência custa?

A resposta é "uma ida e volta até onde estiverem as outras réplicas". Os tempos de ida e volta (RTT, round-trip time) abaixo são ordens de grandeza típicas, usadas aqui como suposições ilustrativas:

Onde estão as réplicas que confirmam RTT típico Custo de uma escrita EC (uma ida e volta a mais)
Mesma máquina / mesmo rack < 0,5 ms Desprezível
Outra zona de disponibilidade, mesma região ~1–2 ms Pequeno: o padrão para bancos de produção
Outra região, mesmo continente ~10–40 ms Perceptível em toda escrita
Outro continente (por exemplo, São Paulo ↔ Europa) ~150–200 ms Domina a requisição; um checkout com 5 escritas custa um segundo

O simulador acima mostra a última linha sem partição nenhuma: no modo PC/EC cada escrita leva 184 ms, no modo PA/EL 4 ms — e o modo rápido paga com leituras velhas.

Essa tabela explica por que a maioria dos sistemas escolhe EC dentro de uma região (replicação síncrona ou por quórum entre zonas custa um ou dois milissegundos) e EL entre regiões (replicação assíncrona, ou particionar os dados por região de origem).

Sistema (configuração típica) Durante a partição Senão PACELC
Cassandra, ScyllaDB (ONE) A L PA/EL
DynamoDB (leituras eventualmente consistentes) A L PA/EL
MongoDB (read/write concern majority) C C PC/EC
PostgreSQL + standby síncrono C C PC/EC
Spanner, CockroachDB C C PC/EC
PostgreSQL + réplicas de leitura assíncronas, leituras nas réplicas (o failover pode perder escritas) L —/EL

Projetos multirregião

Regiões falham — raramente, mas por inteiro — e o BoxOffice quer sobreviver a isso. Os trade-offs entre C, A e L ficam concretos em cinco topologias comuns:

Topologia Escritas Durante a perda de uma região Consistência Custo e complexidade
Uma região, várias zonas Um primário, standby síncrono em outra zona Região fora = BoxOffice fora Forte O menor; o ponto de partida padrão
Ativo–passivo (warm standby) Só na região primária; réplica assíncrona na região 2 Failover (DNS) para a região 2; perde os últimos segundos de escritas (RPO > 0) Forte em operação normal Moderado; o failover precisa ser ensaiado
Ativo–ativo, particionado por origem Cada registro tem uma região de origem; as escritas vão para lá Registros com origem na região morta ficam indisponíveis; os outros não são afetados Forte por registro Maior; roteamento por região de origem
Ativo–ativo, tudo em todo lugar Qualquer região escreve qualquer registro Todas as regiões continuam escrevendo Eventual; conflitos a resolver O maior; só para dados que toleram conflitos
Banco de dados globalmente consistente Consenso entre regiões (Spanner, CockroachDB) Sobrevive a uma região se a maioria das réplicas restar Forte em todo lugar Latência de escrita ≥ RTT entre regiões; custo premium

Experimente. No cenário multi-region, rode o perfil load e depois derrube a região primária. Observe quanto tempo o DNS leva para desviar o tráfego (o TTL), quantas requisições falham durante a troca e o que acontece com as escritas que ainda não tinham sido replicadas. Depois reduza o TTL do DNS e repita.

Abrir o playground completo

BoxOffice: o projeto escolhido

  • Assentos, reservas, pedidos, pagamentos — ativo–ativo particionado por região de origem: o estoque de cada evento vive na região do local do show, em um primário PostgreSQL com um standby síncrono em outra zona (PC/EC dentro da região). Uma fã em Lisboa comprando um show em São Paulo paga uma ida e volta transatlântica na reserva — aceitável para uma ação ocasional.
  • Catálogo, imagens, índice de busca — replicados de forma assíncrona para todas as regiões e cacheados na CDN (PA/EL). Um evento novo aparece na Europa alguns segundos depois; ninguém percebe.
  • Contas / autenticação — majoritariamente leitura; replicadas em todo lugar, com escritas (troca de senha, logout em todos os dispositivos) indo para uma região de origem.
  • Recuperação de desastres — o banco de cada região envia backups e uma réplica assíncrona para outra região (ativo–passivo por região); as metas de RPO/RTO são discutidas em Confiabilidade — recuperação de desastres.

Na entrevista

Nunca diga "vou escolher CP" (ou AP) para o sistema inteiro. Diga por dado, e diga o que o usuário vê: "O estoque de assentos é CP: durante uma partição, o lado minoritário devolve 503 e o app diz 'tente novamente'; reserva em dobro é inaceitável. O catálogo é AP: pode estar segundos desatualizado." Depois traga o PACELC — o entrevistador normalmente se importa mais com a latência do dia a dia do que com partições: "dentro de uma região eu replico de forma síncrona entre zonas (~1–2 ms); entre regiões eu particiono por região de origem em vez de pagar 150 ms por escrita." Mencionar que o C do CAP é linearizabilidade (e não o C do ACID) é um sinal forte e barato.

Boas práticas

  • Escreva, por operação, o que acontece durante uma partição — "reservas são recusadas (CP); o catálogo é servido defasado (AP)" — e faça o cliente tratar as duas respostas.
  • Dê a cada chave uma região de origem — as escritas de um evento vão para um líder e as outras regiões as encaminham; não existe conflito entre regiões para uma chave que tem um único escritor.
  • Meça quanto a consistência custa nas suas escritas — o p99 de uma escrita com e sem replicação síncrona, na sua topologia; o PACELC é um número, não um slogan. Experimente no cenário db-failover de Consenso.
  • Faça do "indisponível" uma resposta rápida e explícita — uma operação CP que não alcança a maioria deve falhar em milissegundos com um erro que pode ser repetido, não ficar pendurada até um timeout.
  • Rotule respostas defasadas na API — um cabeçalho ou campo com a idade do dado deixa o cliente decidir se uma resposta AP basta.

Armadilhas

  • "Escolhemos CA." Se os dados vivem em mais de uma máquina, partições vão acontecer; "CA" só significa que ninguém decidiu o que acontece nesse caso.
  • Tratar o CAP como um rótulo permanente. A escolha entre C e A só vale durante uma partição; o trade-off diário é a latência (PACELC).
  • Esquecer caches e réplicas. Um cache na frente de um armazenamento CP torna essas leituras AP; uma réplica de leitura assíncrona as torna EL.
  • Replicação síncrona entre regiões por padrão. Faz toda escrita pagar uma ida e volta intercontinental — e um enlace degradado deixa o sistema inteiro lento.
  • Failover não ensaiado. Ativo–passivo só funciona se o failover (e o failback) for testado; caso contrário, a região passiva é uma teoria.

Como se conecta

flowchart LR
    REP["Replicação<br/>(Replicação e Consistência)"] --> Q{"partição?"}
    Q -->|sim| CA{"C ou A"}
    Q -->|não| LC{"L ou C"}
    CA -->|C| MAJ["só o lado majoritário<br/>→ Consenso e Quóruns"]
    CA -->|A| MERGE["diverge e depois mescla<br/>LWW · CRDTs → Tempo e Relógios"]
    LC -->|C| SYNC["replicação síncrona<br/>entre zonas"]
    LC -->|L| ASYNC["replicação assíncrona,<br/>caches, CDN"]
    MAJ --> MR["projeto multirregião<br/>região de origem por evento"]
    ASYNC --> MR
    MR --> DR["failover e DR<br/>→ Confiabilidade"]

Componentes da paleta em que a escolha entre C, A e L é feita: sql (standby síncrono ou réplicas assíncronas) · nosql (quóruns ajustáveis) · cache e cdn (sempre EL) · dns (failover entre regiões, cenário multi-region). Continua em: Consenso e Quóruns, Confiabilidade — recuperação de desastres, Tráfego e Borda — DNS.

Pontos-chave

  • CAP: durante uma partição de rede, um armazenamento replicado precisa escolher entre consistência (recusar) e disponibilidade (responder, talvez desatualizado). P não é opcional.
  • O C do CAP é linearizabilidade, não o C do ACID; "CA" não é uma opção para dados distribuídos.
  • PACELC: senão (sem partição), a escolha é latência vs consistência — e você a paga em toda requisição.
  • Escolha por domínio de dados: assentos, dinheiro e identidade CP/EC; catálogo e contadores AP/EL.
  • Em projetos multirregião, particionar os dados por região de origem mantém a consistência forte local e limita o raio de impacto de uma partição.