Visão geral
São 10:00:00 e o show no estádio abre as vendas. Dois milhões de fãs apertam "Reservar assento" no app do BoxOffice praticamente no mesmo segundo, e 60.000 assentos precisam ser divididos entre eles — cada assento vendido exatamente uma vez. Uma fã, dentro do trem, aperta o botão, o spinner gira por cinco segundos e o app mostra "Algo deu errado, tente novamente". Ela aperta de novo. A primeira requisição reservou um assento? Ela está prestes a reservar dois? Vai ser cobrada duas vezes?
Em uma única máquina essa pergunta não existiria: a função retornou ou lançou uma exceção. O BoxOffice, porém, roda em dezenas de máquinas — gateways, várias réplicas do serviço inventory, um PostgreSQL replicado, Redis, um cluster Kafka — e a única coisa que as conecta é uma rede capaz de perder, atrasar e duplicar mensagens. Isso faz do BoxOffice um sistema distribuído, e esta aula trata do que isso muda.
Um sistema distribuído é um conjunto de computadores independentes (nós) que cooperam apenas trocando mensagens por uma rede, sem memória compartilhada e sem relógio compartilhado, e que aparecem para seus usuários como um único sistema coerente1. A definição meio em tom de brincadeira de Leslie Lamport é a mais útil para um engenheiro: "um sistema distribuído é aquele em que a falha de um computador que você nem sabia que existia pode tornar o seu próprio computador inutilizável"2.
A história até aqui
Capítulo 3 da história da disciplina. A aula 2 encontrou uma diferença de 30× entre a demanda da abertura de vendas (~33.000 reservas/s) e o que um banco sustenta (~1.100): o BoxOffice precisa de várias máquinas — réplicas da aplicação, um banco standby, uma sala de espera. Esta aula responde: o que muda quando a única ligação entre essas máquinas é uma rede que perde, atrasa e duplica mensagens — e como impedir que um clique repetido reserve ou cobre duas vezes?
Objetivos de aprendizagem
Ao final desta aula, você deve ser capaz de:
- Justificar quando distribuir um sistema (escala, tolerância a falhas, latência, isolamento) e listar o que isso custa.
- Classificar falhas com os modelos de falha clássicos (crash, omissão, temporização, bizantina, falha cinza) e reconhecer as 8 falácias da computação distribuída em um projeto.
- Explicar por que quem chama não consegue distinguir um nó lento de um nó morto, e projetar operações reexecutadas para serem idempotentes (chaves de idempotência, escritas condicionais).
- Comparar modelos de replicação e modelos de consistência (linearizável → causal → read-your-writes → eventual) e escolher um por domínio de dados.
- Aplicar CAP e PACELC para justificar um trade-off entre consistência, disponibilidade e latência.
- Dimensionar um cluster de consenso (Raft, quóruns
R + W > N) e explicar eleição de líder, split brain e fencing. - Escolher entre two-phase commit e uma saga (com compensações, reservas e um outbox) para uma operação que abrange vários serviços.
- Ordenar eventos entre máquinas sem confiar em relógios de parede (relógios de Lamport, vetoriais e híbridos, TrueTime).
Roteiro
- Esta visão geral — por que distribuir, o que isso custa, modelos de falha, a rede não confiável (8 falácias), timeouts, semânticas de entrega e idempotência.
- Replicação e Consistência — replicação com um líder, com vários líderes e sem líder; o espectro de consistência, de linearizável a eventual; garantias de sessão como read-your-writes.
- CAP e PACELC — a escolha entre consistência e disponibilidade durante uma partição, a escolha entre latência e consistência no resto do tempo, e projetos multirregião.
- Consenso e Quóruns — maiorias, Raft (eleição e replicação do log),
R + W > N, leases e fencing tokens, e as ferramentas que fazem isso por você. - Transações Distribuídas — atomicidade entre serviços: two-phase commit e por que ele bloqueia, sagas com compensações, reservas try–confirm–cancel e o outbox transacional.
- Tempo e Relógios — NTP e deriva de relógio, relógios de Lamport e vetoriais, relógios lógicos híbridos, TrueTime e como ordenar eventos na prática.
- Quiz — verifique seu entendimento.
Slides
Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba
Por que distribuir?
Ninguém distribui um sistema por diversão: um único processo com um único banco de dados é mais fácil de escrever, testar, depurar e entender. Só pagamos o preço da distribuição quando uma máquina não consegue mais atender a um requisito não funcional:
| Motivo | O que uma máquina não consegue fazer | No BoxOffice |
|---|---|---|
| Escalabilidade | Atender mais requisições, guardar mais dados ou executar mais computação do que sua CPU, memória, disco ou placa de rede permitem | 2 M de fãs no primeiro minuto de uma abertura de vendas; um servidor não consegue nem aceitar tantas conexões TLS |
| Tolerância a falhas / disponibilidade | Sobreviver à própria falha — um disco, uma fonte, um kernel panic, um deploy que deu errado | Se a única máquina do inventory morre às 10:00:05, a venda para |
| Latência | Estar perto de todos os usuários; a luz na fibra leva ~100 ms para atravessar meio planeta (~20.000 km), só de ida | Fãs em Lisboa e em São Paulo esperam um mapa de assentos rápido |
| Isolamento e organização | Permitir que times façam deploy de forma independente, ou conter uma falha em uma parte do produto | Um bug em search não deve parar o payment |
| Residência de dados | Manter dados pessoais em uma jurisdição (por exemplo, LGPD no Brasil, GDPR na Europa) | Os dados dos fãs europeus ficam em uma região da UE |
Escalar verticalmente — uma máquina maior — é a primeira resposta e, por um tempo, muitas vezes a certa. Ela para de funcionar por três motivos: existe uma maior máquina que se pode comprar; no topo, o preço cresce mais rápido que a capacidade; e, por maior que seja, ela continua sendo uma máquina — um ponto único de falha (SPOF, single point of failure).
Pense antes: se uma máquina é mais simples, por que não rodar o BoxOffice no maior servidor que o dinheiro pode comprar?
Provavelmente ele aguentaria o dia médio. Mas (1) o pico da abertura das vendas às 10:00 está ordens de grandeza acima da média, e a máquina ficaria ociosa (e paga) no resto do tempo; (2) qualquer falha — hardware, sistema operacional, um deploy ruim — derruba a empresa inteira, o que viola qualquer meta razoável de disponibilidade; (3) fãs longe dessa máquina pagam o tempo de ida e volta em toda requisição. Você ainda usa escala vertical para componentes difíceis de dividir (um banco de dados primário), mas acrescenta réplicas e espalha o trabalho sem estado por muitas máquinas.
Em escala, falhar é a regra
O segundo argumento esconde um paradoxo: distribuir para tolerar falhas significa usar mais máquinas, e mais máquinas significam mais falhas. Se cada máquina falha em um dado dia com uma pequena probabilidade \(p\), de forma independente, a probabilidade de que pelo menos uma de \(N\) máquinas falhe nesse dia é \(1 - (1-p)^N\):
Com uma chance (ilustrativa) de 0,1% de falha por máquina por dia, três máquinas veem uma falha em cerca de 0,3% dos dias, mas uma frota de 3.000 máquinas vê pelo menos uma falha em cerca de 95% dos dias. Os primeiros relatos do Google descrevem exatamente essa experiência: na escala de uma frota, falhas de hardware são rotina diária, não exceção3. A conclusão molda o resto do curso: um sistema distribuído precisa tratar a falha como um evento normal e continuar funcionando apesar dela — não dá para esperar evitá-la.
O que a distribuição custa
| Custo | Por quê | Onde volta no curso |
|---|---|---|
| Falha parcial | Alguns nós funcionam enquanto outros não; cada chamada pode falhar sozinha | Modelos de falha, Confiabilidade |
| Rede não confiável | Mensagens são perdidas, atrasadas, duplicadas, reordenadas | A rede não é confiável, Idempotência |
| Sem relógio global | Cada nó tem seu relógio, e eles discordam | Tempo e Relógios |
| Consistência | Cópias do mesmo dado podem divergir | Replicação e Consistência, CAP |
| Coordenação | Concordar sobre qualquer coisa (um líder, um lock, uma ordem, um commit) exige mensagens e maiorias | Consenso, Transações Distribuídas |
| Operação | Mais processos para implantar, observar e depurar | Observabilidade, Cloud-native |
O BoxOffice é um sistema distribuído
Acompanhe um clique em "Reservar assento A-12" pelo BoxOffice. Cada seta é uma mensagem pela rede; cada caixa é um processo que pode falhar, pausar ou ficar lento por conta própria:
flowchart LR
fan([Celular do fã]) -->|"POST /holds"| gw[gateway]
gw -->|"valida o token"| auth[auth]
gw -->|"encaminha"| inv[inventory]
inv -->|"UPDATE seat ... (transação)"| pg[("PostgreSQL primário")]
pg -->|"replica o WAL"| rep[("PostgreSQL réplica")]
inv -->|"cacheia o mapa de assentos"| redis[("Redis")]
inv -->|"evento SeatHeld"| kafka[["Kafka (3 brokers)"]]
kafka -->|"consome"| notif[notification] Nada nessa figura compartilha memória ou relógio. Se o Redis está lento, o inventory espera; se a réplica está atrasada, uma leitura nela está desatualizada; se o Kafka está inacessível, a reserva pode ter sido confirmada no PostgreSQL, mas o evento SeatHeld nunca publicado. Cada um desses casos é uma falha parcial, e o resto desta página trata de nomeá-las com precisão.
Modelos de falha
Um modelo de falha diz o que um componente defeituoso pode fazer. Ele importa porque todo algoritmo só é correto sob algum modelo: um protocolo que tolera crashes pode ser enganado por um nó que mente.
| Modelo | O que o nó (ou enlace) defeituoso faz | Exemplo no BoxOffice | Defesa típica |
|---|---|---|---|
| Crash-stop | Para e nunca mais volta | Uma VM é encerrada | Réplicas + failover |
| Crash-recovery | Para e depois reinicia com o que tinha em disco | Um pod do inventory é morto por falta de memória (OOM) e reiniciado | Logs duráveis; reprocessamento idempotente |
| Omissão | Descarta silenciosamente algumas mensagens que deveria enviar ou receber | Um buffer de socket cheio descarta o evento SeatHeld | Confirmações (acks), retries, timeouts |
| Temporização (desempenho) | Responde, mas tarde demais | Uma pausa de 3 segundos de garbage collection no payment | Timeouts, hedging, circuit breakers |
| Bizantina | Comporta-se de forma arbitrária: corrompe, mente, diz coisas diferentes a nós diferentes | Um bit trocado corrompe uma mensagem; um cliente comprometido forja ingressos | Checksums, assinaturas, validação; protocolos BFT (3f + 1 nós) |
| Falha cinza | Parece saudável para o health check, mas falha para os usuários | /health responde 200 enquanto toda consulta real dá timeout | Health checks que exercitam o caminho real; sinais do lado do cliente |
Mais duas ideias completam o vocabulário:
- Falha parcial: em uma máquina, um programa normalmente funciona ou cai por inteiro. Em um sistema distribuído, parte dele falha enquanto o resto continua rodando, e ninguém tem uma visão completa de qual parte. As falhas cinza, estudadas pela Microsoft no Azure, são o pior caso — o monitoramento do próprio sistema discorda dos usuários4.
- Hipóteses de tempo: em um modelo síncrono, mensagens e processamento têm limites superiores conhecidos; em um modelo assíncrono, não há limite algum. Redes reais são parcialmente síncronas: comportam-se bem na maior parte do tempo, com atrasos ocasionais sem limite (um switch congestionado, uma pausa de GC, a migração de uma VM). Algoritmos práticos (o Raft, por exemplo) são projetados para serem seguros sob assincronia e para progredir apenas quando a rede se comporta.
A falha mais comum não é um crash
Crashes são fáceis: o processo sumiu, as conexões são recusadas, o orquestrador o reinicia. As falhas que doem em produção são respostas lentas, falhas parciais e falhas cinza — o nó está lá, atendendo algumas requisições, segurando suas threads.
A rede não é confiável
As 8 falácias da computação distribuída
Nos anos 1990, L. Peter Deutsch e colegas da Sun Microsystems listaram as suposições que iniciantes em sistemas distribuídos fazem — todas falsas5:
| Falácia | Realidade | No BoxOffice |
|---|---|---|
| 1. A rede é confiável | Pacotes são descartados, enlaces caem, switches reiniciam | A conexão inventory → PostgreSQL é resetada no meio de uma transação |
| 2. A latência é zero | Mesmo data center ≈ 0,5 ms de ida e volta; entre continentes ≈ 150 ms6 | Seis chamadas sequenciais de 30 ms cada fazem um checkout de 180 ms |
| 3. A largura de banda é infinita | Enlaces saturam, principalmente nos picos | Devolver o mapa completo de 60.000 assentos em JSON para 2 M de fãs |
| 4. A rede é segura | O tráfego pode ser lido, alterado ou forjado | Serviços confiam em qualquer chamador dentro do cluster |
| 5. A topologia não muda | Nós entram e saem; IPs mudam | Um pod do Kubernetes é reagendado com um novo IP durante a abertura das vendas |
| 6. Existe um administrador | Muitos times, contas, provedores e políticas | O provedor de pagamentos muda uma configuração de TLS do lado dele |
| 7. O custo de transporte é zero | Serialização custa CPU; tráfego de saída na nuvem custa dinheiro | Tráfego entre zonas na fatura do mês |
| 8. A rede é homogênea | Protocolos, versões e codificações diferentes | Serviços Java, um assistant em Python, o protocolo binário do Kafka |
Cada uma delas aparece em aulas posteriores: a falácia 1 em Confiabilidade, a 2 em Estimativas, a 3 em Tráfego e Borda, a 4 em Rede e Segurança, a 5 em Cloud-native.
Lento ou morto?
Quando o inventory envia uma requisição ao payment e nenhuma resposta chega, ele não tem como saber qual destes casos aconteceu:
sequenceDiagram
participant I as inventory
participant P as payment
I->>P: autorizar R$ 450 (requisição 1)
Note over I,P: (a) requisição perdida (b) payment caiu<br/>(c) resposta perdida (d) payment só está lento
I-->>I: sem resposta após 2 s: timeout
I->>P: autorizar R$ 450 (retry)
P-->>I: OK
Note over I,P: nos casos (c) e (d) o fã foi cobrado duas vezes Do lado de quem chama, uma requisição perdida, um servidor que caiu, uma resposta perdida e um servidor muito lento parecem a mesma coisa: silêncio. A única ferramenta prática é um timeout — depois de um prazo, supor que houve falha. Mas um timeout não diz se a operação foi executada, e daí decorrem duas coisas:
- Escolher o timeout é um trade-off. Longo demais e as threads de quem chama se acumulam esperando (a falha em cascata de Confiabilidade); curto demais e você declara mortos nós saudáveis mas lentos, faz retry e adiciona carga justamente quando o sistema está sofrendo. Um ponto de partida comum é um pouco acima da latência p99,9 do serviço chamado, dentro do orçamento de latência de quem chama. Sistemas como Cassandra e Akka vão além e usam detectores de falha adaptativos (o detector φ accrual), que aprendem a distribuição normal dos atrasos dos heartbeats7.
- Retries precisam ser seguros. Se o retry pode aplicar a operação uma segunda vez, isso precisa ser inofensivo — a operação precisa ser idempotente.
Semânticas de entrega e idempotência
No máximo uma vez, pelo menos uma vez, exatamente uma vez
Sempre que uma mensagem atravessa uma rede não confiável, o remetente escolhe uma semântica de entrega:
| Semântica | Como | Risco | Use para |
|---|---|---|---|
| No máximo uma vez (at most once) | Envia uma vez, nunca faz retry | Mensagens perdidas | Amostras de métricas, sinais de "fã está digitando…" |
| Pelo menos uma vez (at least once) | Faz retry até receber confirmação | Duplicatas | Quase tudo: pedidos, eventos, e-mails |
| Exatamente uma vez (exactly once) | Não é alcançável só pela rede | — | Obtida como pelo menos uma vez + processamento idempotente ("efetivamente uma vez") |
"Entrega exatamente uma vez" por uma rede não confiável é impossível em geral — o remetente nunca pode ter certeza de que o destinatário não processou uma mensagem cuja confirmação se perdeu. O que sistemas como o Kafka chamam de exactly-once é processamento exatamente uma vez: duplicatas podem chegar, mas seu efeito é aplicado uma vez, por deduplicação ou transações8. A aula de Mensageria aprofunda o tema.
Operações idempotentes
Uma operação é idempotente se aplicá-la várias vezes tem o mesmo efeito que aplicá-la uma vez: \(f(f(x)) = f(x)\).
| Operação | Idempotente? | Por quê |
|---|---|---|
GET /events/42 | Sim | Leituras não mudam nada |
PUT /holds/A-12 {fan: 42} | Sim | Define o estado como um valor; repetir define o mesmo valor |
DELETE /holds/A-12 | Sim | A reserva some na primeira chamada; a segunda pode devolver 404, mas o estado é o mesmo |
POST /orders | Não | Cada chamada cria um novo pedido |
UPDATE seats SET sold = sold + 1 | Não | Cada chamada incrementa de novo |
UPDATE seats SET status = 'HELD', fan = 42 WHERE id = 'A-12' AND status = 'FREE' | Sim | Uma escrita condicional: a segunda chamada não encontra nenhuma linha |
Duas técnicas tornam seguras para retry as operações não idempotentes:
- Reescrever como uma transição de estado com condição (compare-and-set): "marque o assento como HELD se ele estiver FREE", "grave a versão 8 se a versão atual for 7". Uma duplicata não satisfaz a condição e não muda nada.
- Chaves de idempotência: o cliente gera uma chave única por operação pretendida e a envia em todas as tentativas; o servidor guarda a chave junto com o resultado e, diante de uma duplicata, devolve o resultado guardado em vez de executar de novo. A API pública do Stripe popularizou o cabeçalho
Idempotency-Key9, e há um rascunho do IETF padronizando-o.
sequenceDiagram
participant F as App do fã
participant O as order
participant DB as PostgreSQL
F->>O: POST /orders, Idempotency-Key: 7f3a…
O->>DB: INSERT da chave 7f3a… (única) + pedido 981
DB-->>O: confirmado (commit)
O--xF: 201 Created (resposta perdida no trem)
F->>O: retry: POST /orders, Idempotency-Key: 7f3a…
O->>DB: INSERT da chave 7f3a…
DB-->>O: violação de unicidade: a chave já existe
O->>DB: SELECT da resposta guardada para 7f3a…
O-->>F: 201 Created, pedido 981 (o mesmo pedido, sem duplicata) A chave e a mudança de negócio precisam ser gravadas na mesma transação; caso contrário, um crash entre as duas escritas reabre a janela para duplicatas:
CREATE TABLE idempotency_keys (
key UUID PRIMARY KEY,
request_hash TEXT NOT NULL, -- reject the same key with a different body
response JSONB, -- what we answered the first time
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
BEGIN;
INSERT INTO idempotency_keys (key, request_hash)
VALUES ('7f3a…', 'sha256:…')
ON CONFLICT (key) DO NOTHING; -- 0 rows inserted → duplicate: return stored response
INSERT INTO orders (id, fan_id, total) VALUES (981, 42, 450.00);
UPDATE idempotency_keys SET response = '{"orderId": 981}' WHERE key = '7f3a…';
COMMIT;
BoxOffice: reservando um assento com segurança
O app gera um UUID quando o fã toca em Reservar assento e o reutiliza em todo retry desse toque. O inventory executa UPDATE seats SET status = 'HELD', fan_id = :fan, hold_key = :key WHERE id = :seat AND (status = 'FREE' OR hold_key = :key). A primeira tentativa reserva o assento; um retry com a mesma chave encontra a linha de novo e não muda nada; a requisição de um fã diferente não encontra nenhuma linha e recebe 409 Conflict. O adaptador payment repassa a mesma chave ao provedor externo, então uma cobrança refeita não é cobrada duas vezes (Integrações).
No código do BoxOffice: uma escrita condicional e uma chave de idempotência (labs 6 e 9)
As duas técnicas estão nos laboratórios. O Hands-on 6 — Reservas pega um assento com uma escrita condicional: o UPDATE só encontra a linha se o assento ainda está AVAILABLE e ainda na version que foi lida, então uma segunda transação muda 0 linhas e recebe 409. Numa execução de teste desse laboratório, a versão ingênua (verifica e depois age) vendeu 9 assentos duas vezes; esta, 0. Note o que o laboratório não faz: uma reserva repetida gera um novo id de reserva, então o fã recebe 409 pelo próprio assento em vez de receber a primeira reserva de volta — a hold_key do quadro acima é o passo que torna a própria reserva idempotente.
// ---- strategy 3: optimistic, compare-and-set on a version column ---------------
private void holdOptimistic(long eventId, List<Long> seatIds, UUID holdId) {
var seats = read(eventId, seatIds, ""); // no lock
requireAvailable(seats, seatIds);
for (var seat : seats) {
int changed = jdbc.sql("""
UPDATE inventory.seats
SET status = 'HELD', hold_id = :holdId, version = version + 1
WHERE id = :id AND version = :version AND status = 'AVAILABLE'""")
.param("holdId", holdId).param("id", seat.id()).param("version", seat.version())
.update();
if (changed == 0) {
// someone changed this seat after we read it: give up (the exception rolls back)
throw new ResponseStatusException(HttpStatus.CONFLICT, "seat " + seat.id() + " was just taken");
}
}
}
O Hands-on 9 — Pagamento usa uma chave de idempotência: o order manda Idempotency-Key: order-<id> em toda tentativa, o payment devolve o resultado guardado quando já viu a chave, e repassa a mesma chave ao provedor. A chamada ao provedor não pode ficar dentro de uma transação do banco (ela pode levar segundos), então a chave é conferida antes e gravada depois dela, sob uma restrição UNIQUE; a janela entre as duas fica coberta porque o provedor deduplica pela mesma chave.
public Result charge(String idempotencyKey, ChargeRequest req) {
Optional<PaymentOut> done = find(idempotencyKey);
if (done.isPresent()) {
return new Result(done.get(), false); // a retry: answer, do not charge again
}
// remote call OUTSIDE any database transaction: never hold a connection while waiting
PspClient.Charge charge = psp.charge(idempotencyKey, req.amount(), "order-" + req.orderId());
String status = "approved".equals(charge.status()) ? "AUTHORIZED" : "DECLINED";
jdbc.sql("""
INSERT INTO payments (id, idempotency_key, order_id, amount, status, provider_ref)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (idempotency_key) DO NOTHING
""")
.params(UUID.randomUUID(), idempotencyKey, req.orderId(), req.amount(), status, charge.id())
.update();
return new Result(find(idempotencyKey).orElseThrow(), true);
}
Pense antes: o retry do fã chega a uma réplica de order diferente da primeira tentativa. A chave de idempotência ainda funciona?
Só se o estado de deduplicação for compartilhado — gravado no banco de dados (como acima) ou em um armazenamento compartilhado como o Redis, e não na memória de uma réplica. Um conjunto em memória de "chaves já vistas" por réplica quebra assim que o load balancer manda o retry para outra réplica, o que é o caso normal. As chaves também precisam de um período de retenção (o Stripe as guarda por 24 horas) longo o bastante para cobrir todos os retries que o cliente pode fazer.
Juntando tudo: o mapa de conceitos
Os três problemas de raiz — falha parcial, uma rede não confiável e nenhum relógio global — explicam quase todas as decisões de projeto do resto do curso. O mapa abaixo liga cada problema às técnicas desta aula (azul), à subpágina que as ensina e aos componentes da paleta em que você as encontrará no playground (verde):
flowchart LR
PF["Falha parcial"]:::problem --> REP["Replicação<br/>(manter cópias)"]
NET["Rede não confiável"]:::problem --> IDEM["Timeouts, retries,<br/>idempotência"]
CLK["Sem relógio global"]:::problem --> ORD["Ordenação:<br/>relógios lógicos / híbridos"]
REP --> CM["Modelos de consistência<br/>linearizável … eventual"]
CM --> CAP["CAP / PACELC<br/>C vs A, L vs C"]
CAP --> CONS["Consenso, quóruns,<br/>fencing"]
IDEM --> TX["Sagas, outbox,<br/>2PC"]
CONS --> TX
ORD --> CONS
REP -.-> sql["sql · nosql"]:::comp
CM -.-> cache["cache · cdn"]:::comp
CONS -.-> kafka["kafka · scheduler"]:::comp
IDEM -.-> pay["payment · queue"]:::comp
TX -.-> stream["stream · kafka"]:::comp
CAP -.-> dns["dns (multirregião)"]:::comp
classDef problem fill:#e5484d22,stroke:#e5484d
classDef comp fill:#1e9e5a22,stroke:#1e9e5a
classDef tech fill:#2f6fed22,stroke:#2f6fed
class REP,IDEM,ORD,CM,CAP,CONS,TX tech | Técnica | Ensinada em | Componentes da paleta em que aparece | Outras aulas que se apoiam nela |
|---|---|---|---|
| Timeouts, retries, chaves de idempotência | esta visão geral | payment, thirdparty, queue | Confiabilidade, Compute — integrações |
| Replicação, modelos de consistência | Replicação e Consistência | sql, nosql, cache, cdn | Bancos de dados — replicação, Caching |
| CAP, PACELC, multirregião | CAP e PACELC | dns, sql, nosql | Confiabilidade, Tráfego e Borda |
| Consenso, quóruns, leases, fencing | Consenso e Quóruns | kafka, scheduler, sql (failover) | Cloud-native — Kubernetes (etcd), Bancos de dados — particionamento |
| 2PC, sagas, outbox, reservas | Transações Distribuídas | stream, kafka, payment | Mensageria — padrões, Estudo de caso: sistema de pagamentos |
| Relógios, ordenação, ids únicos | Tempo e Relógios | kafka, sql, scheduler | Mensageria — Kafka, Estudo de caso: encurtador de URL |
Na entrevista
Raramente perguntarão "o que é um sistema distribuído?". Vão perguntar "o cliente tomou timeout — o que acontece?" ou "como evitar cobrar duas vezes?". Respostas fortes: (1) nomeie a ambiguidade ("um timeout não me diz se a cobrança aconteceu"); (2) torne a operação idempotente com uma chave de idempotência gravada atomicamente com o resultado; (3) faça retry com backoff e jitter em uma única camada; (4) explicite a semântica de entrega ("entrega pelo menos uma vez, processamento efetivamente uma vez"). Falar em "entrega exatamente uma vez" sem ressalvas é um sinal de alerta para o entrevistador.
Boas práticas
- Ponha um timeout em toda chamada remota, menor que o prazo de quem chama — para que quem chama ainda possa responder, ou tentar em outro lugar, antes que o próprio cliente desista. Na BoxOffice: o
orderespera no máximo 1 s pelopayment(lab 9); a borda espera no máximo 5 s por uma réplica (lab 3). - Mande uma chave de idempotência em toda escrita que pode ser repetida, e guarde-a sob uma restrição
UNIQUE— o banco, não a memória de uma réplica, decide o que é duplicata. Na BoxOffice:payments.idempotency_key(Hands-on 9 — Pagamento). - Repita em uma camada só, com backoff exponencial, jitter e um limite — retries em três camadas transformam uma chamada lenta em 27, justamente quando quem é chamado está sofrendo.
- Dê a cada requisição um id na borda e registre-o em todo salto — o único jeito de seguir um clique por máquinas que não compartilham memória. Na BoxOffice: o
X-Request-Idposto pelo Nginx no lab 3. - Teste todo consumidor entregando cada mensagem duas vezes — a entrega pelo menos uma vez vai fazer isso em produção; o teste garante que o efeito seja aplicado uma vez.
Armadilhas
- Tratar uma chamada remota como uma chamada de função local — sem timeout, sem política de retry, sem pensar em falha parcial.
- Fazer retry de operações não idempotentes (
POST, incrementos) sem chave: duplicatas, cobranças em dobro, assentos vendidos duas vezes. - Deduplicar em memória em uma réplica: o retry vai para outra réplica.
- Health checks que mentem:
/healthresponde200porque o processo está vivo, enquanto toda requisição falha (falha cinza). - Supor que as falhas são independentes: réplicas no mesmo rack, na mesma zona ou com a mesma configuração errada falham juntas.
Pontos-chave
- Distribua por escala, tolerância a falhas, latência, isolamento ou residência de dados — e só então: você paga com falhas parciais, uma rede não confiável e nenhum relógio global.
- Em escala, falhar é o caso normal: \(1-(1-p)^N\) se aproxima de 1 rapidamente.
- Conheça os modelos de falha; os perigosos na prática são as falhas lentas, parciais e cinza, não crashes limpos.
- Quem chama não consegue distinguir lento de morto: use timeouts e aceite que uma operação que deu timeout pode ter acontecido.
- Entrega exatamente uma vez é impossível; construa entrega pelo menos uma vez + processamento idempotente com escritas condicionais ou chaves de idempotência gravadas atomicamente.
O que quebra em seguida
O BoxOffice agora tem o vocabulário para sobreviver à rede entre suas máquinas — timeouts, retentativas idempotentes, um modelo de consistência por domínio de dados, consenso para escolher o primário. Ainda não decidiu quem recebe o tráfego. Às 10:00:00 dois milhões de celulares mandam uma requisição no mesmo segundo: cerca de 100.000 requisições/s na borda (a estimativa), enquanto uma instância do catalog medida no laboratório 2 mantém a meta de p99 só até algumas centenas de requisições por segundo. A maioria dessas requisições precisa terminar antes de qualquer serviço, o resto precisa chegar a uma réplica saudável, e os bots não podem chegar primeiro: aula 4 — Tráfego e Borda.
Relacionados
- Playground: System Design Playground — todo componente deste curso pode falhar; ligue o caos (
data-chaos) em qualquer cenário. - Hands-on: Hands-on 7 — Assíncrono (consumidor idempotente, outbox), Hands-on 9 — Resiliência e caos (timeouts, retries, injeção de falhas com Toxiproxy).
- Exercícios: Event-driven (consumidor idempotente), Design doc.
- Estudos de caso: Ingressos (abertura de vendas do BoxOffice), Sistema de pagamentos, Cache distribuído.
- Aulas: Estimativas (números de latência), Bancos de dados — replicação, Mensageria — semânticas, Confiabilidade.
- Preparação para entrevistas: Entrevista.
-
VAN STEEN, M.; TANENBAUM, A. S. Distributed Systems, 4ª ed., 2023. ↩
-
LAMPORT, L. Mensagem de e-mail, 28 de maio de 1987. ↩
-
DEAN, J. Designs, Lessons and Advice from Building Large Distributed Systems. Palestra de abertura do LADIS, 2009. ↩
-
HUANG, P. et al. Gray Failure: The Achilles' Heel of Cloud-Scale Systems. HotOS, 2017. ↩
-
Fallacies of distributed computing — Wikipedia (Deutsch, 1994; Gosling acrescentou a oitava em 1997). ↩
-
Latency Numbers Every Programmer Should Know — a partir de Jeff Dean e Peter Norvig. ↩
-
HAYASHIBARA, N. et al. The φ Accrual Failure Detector. IEEE SRDS, 2004. ↩
-
KLEPPMANN, M.; RICCOMINI, C. Designing Data-Intensive Applications, 2ª ed. O'Reilly. Capítulos sobre replicação, transações e processamento de streams. ↩
-
STRIPE. Idempotent requests. Referência da API do Stripe. ↩