Pular para conteúdo

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

  1. 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.
  2. 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.
  3. 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.
  4. 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ê.
  5. 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.
  6. 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.
  7. 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\):

2026-11-01T21:55:01.806040 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/

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:

  1. 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.
  2. 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:

  1. 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.
  2. 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:

idempotency.sql
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.

services/inventory/src/main/java/boxoffice/inventory/HoldService.java (compare-and-set)
// ---- 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.

services/payment/src/main/java/boxoffice/payment/PaymentService.java (the key)
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 order espera no máximo 1 s pelo payment (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-Id posto 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: /health responde 200 porque 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.



  1. VAN STEEN, M.; TANENBAUM, A. S. Distributed Systems, 4ª ed., 2023. ↩

  2. LAMPORT, L. Mensagem de e-mail, 28 de maio de 1987. ↩

  3. DEAN, J. Designs, Lessons and Advice from Building Large Distributed Systems. Palestra de abertura do LADIS, 2009. ↩

  4. HUANG, P. et al. Gray Failure: The Achilles' Heel of Cloud-Scale Systems. HotOS, 2017. ↩

  5. Fallacies of distributed computing — Wikipedia (Deutsch, 1994; Gosling acrescentou a oitava em 1997). ↩

  6. Latency Numbers Every Programmer Should Know — a partir de Jeff Dean e Peter Norvig. ↩

  7. HAYASHIBARA, N. et al. The φ Accrual Failure Detector. IEEE SRDS, 2004. ↩

  8. KLEPPMANN, M.; RICCOMINI, C. Designing Data-Intensive Applications, 2ª ed. O'Reilly. Capítulos sobre replicação, transações e processamento de streams. ↩

  9. STRIPE. Idempotent requests. Referência da API do Stripe. ↩