Pular para conteúdo

a. Visão geral

Neste lab

  • Construa o inventory, o coração fortemente consistente do BoxOffice: assentos e reservas (holds), em que um assento nunca pode pertencer a dois fãs.
  • Compare três estratégias de concorrência numa venda relâmpago — check-then-act ingênuo, lock pessimista (SELECT … FOR UPDATE) e lock otimista (coluna de versão) — e prove quais vendem o mesmo assento duas vezes2.
  • Adicione uma réplica de leitura com replicação por streaming, roteie para ela as transações somente leitura e observe o atraso de replicação quebrando o read-your-writes.
  • Ponha o PgBouncer entre os serviços e o PostgreSQL, e use EXPLAIN ANALYZE + índices para corrigir a listagem lenta do catálogo.
  • Torne a replicação síncrona e pague por isso, derrube o primário com uma reserva em andamento, faça o failover à mão, provoque um split brain e traga o antigo primário de volta com o pg_rewind.
  • Meça cada mudança: conflitos, vendas duplicadas, p99, atraso, conexões, tempo de consulta, escritas perdidas.

A venda relâmpago é o momento para o qual o BoxOffice existe: às 10:00, 60.000 assentos e dois milhões de fãs. O catálogo pôde ir para o cache porque alguns segundos de defasagem não fazem mal. Os assentos são o oposto: vender o mesmo assento duas vezes é o único bug que uma empresa de ingressos não pode se permitir. Este lab trata da camada de dados sob pressão1 — primeiro a corretude (transações e locks), depois a escala (réplicas, pooling, índices), depois a sobrevivência (failover) — e dos trade-offs entre elas.

Este lab constrói o capítulo 6 da história da disciplina, o banco de dados do qual todo serviço stateless agora depende. Ele implementa as decisões da aula 6: um assento, uma reserva com locks ou uma verificação de versão, uma réplica de leitura e o seu atraso, pool de conexões com o PgBouncer, índices lidos com EXPLAIN e um failover, com as suas escritas perdidas e o split brain.

Antes de começar

Tempo 5–6 horas (quatro partes: faça uma por encontro, se precisar)
Você precisa de as ferramentas do lab 0; Docker com 8 GB (onze contêineres, seis JVMs); portas 80, 8080, 8083, 5432, 5433, 6432 e 6379 livres
Diretório todo comando roda na raiz do seu repositório boxoffice/
docker compose version        # Docker Compose v2.x (or newer)
java -version                 # 21 or newer
mvn -v                        # Apache Maven 3.9+
k6 version                    # k6 v1.0+
uv --version                  # uv (runs the Python helper scripts)

Estado inicial — o fim do lab 5:

boxoffice/ at the start of lab 6
📁 boxoffice/
├── YAML docker-compose.yaml        # db, redis, catalog-1..3, auth, gateway, edge
├──  infra/nginx/nginx.conf
├── 📁 load/ ...  scripts/ ...  results/ ...
└── 📁 services/auth/  services/catalog/ (with the cache)  services/gateway/

Recupere o atraso

Pulou ou quebrou o lab 5? Baixe boxoffice-after-lab-5.zip, descompacte e cd boxoffice. Este lab adiciona services/inventory, infra/postgres/*, infra/pgbouncer/*, a migração do catalog V3__index_events.sql, quatro arquivos do k6 e scripts/reset-inventory.sh e infra/postgres/rejoin-as-replica.sh (passo 4), e substitui o docker-compose.yaml e o application.yaml e o SecurityConfig.java do gateway; todos aparecem completos nas próximas páginas e estão em code/. Ele também precisa de um volume de banco novo (docker compose down -v, explicado no passo 2).

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

Conexão com a teoria

Antes de medir: o playground

Experimente: o cenário db-replicas tem um primário e réplicas de leitura, com leituras e escritas separadas. Rode-o e depois aumente a fração de escritas: o que acontece com o primário enquanto as réplicas ficam ociosas? Aumente o atraso de replicação e observe o indicador de "leituras desatualizadas". Anote com que tipo de tráfego as réplicas ajudam — e com qual elas não ajudam em nada.

Depois olhe a venda inteira em flash-sale: a sala de espera, o inventário em SQL, o pagamento. O lab 7 constrói a sala de espera; hoje você constrói a caixa do inventário.

O que você vai construir

flowchart LR
    k6([fãs do k6]) --> edge[edge] --> gw[gateway]
    gw -->|"/catalog/**"| cat["catalog ×3"]
    gw -->|"/inventory/**"| inv["inventory :8083"]
    inv -->|"escritas, reservas<br/>(tx de leitura e escrita)"| pgb["pgbouncer :6432"]
    pgb --> db[("db (primário)")]
    inv -->|"mapas de assentos, disponibilidade<br/>(tx readOnly)"| rep[("db-replica")]
    db -->|"streaming do WAL"| rep
    cat --> db
    cat --> redis[(redis)]
Método Caminho Auth Banco Resposta
GET /inventory/events/{eventId}/availability aberto réplica assentos por setor: disponíveis, reservados, vendidos
GET /inventory/events/{eventId}/seats?section=S01 aberto réplica o mapa de assentos de um setor
POST /inventory/holds {eventId, seatIds} token primário 201 reserva (10 min), 409 se algum assento estiver ocupado
GET /inventory/holds/{id} token (dono) primário a reserva
POST /inventory/holds/{id}/confirm token (dono) primário os assentos viram SOLD (chamado pelo order no lab 7)
DELETE /inventory/holds/{id} token (dono) primário libera os assentos
GET /inventory/audit/double-bookings token primário {"doubleBookedSeats": 0} — o invariante

O evento 1 tem 60.000 assentos (30 setores × 40 fileiras × 50 assentos); os eventos 2–100 têm 500 assentos cada.

Passos

  • 1. Reservas e locks


    O serviço inventory, três estratégias de lock, o teste da venda relâmpago e a auditoria de vendas duplicadas.

  • 2. Réplica e pool de conexões


    Replicação por streaming, roteamento de transações readOnly, atraso de replicação e read-your-writes, PgBouncer.

  • 3. Índices e EXPLAIN


    Leia um plano de consulta, acrescente os índices certos à listagem do catálogo, meça antes e depois.

  • 4. Failover


    Replicação síncrona e o seu custo, uma queda com uma reserva em andamento, failover movendo um nome, split brain, pg_rewind.

Meça

Cada passo tem o seu próprio experimento; reúna os resultados em results/lab6.md:

results/lab6.md
# Lab 6 — data at scale

## 1. Flash sale: 200 fans, 100 seats, 30 s (load/flash-sale.js)
| INVENTORY_LOCKING | holds 201 | holds 409 | p50 hold (ms) | p99 hold (ms) | double-booked seats (audit) |
|---|---:|---:|---:|---:|---:|
| naive       | | | | | |
| pessimistic | | | | | |
| optimistic  | | | | | |

## 2. Reads: availability + seat maps at 200 req/s (load/availability.js)
| Reads served by | p50 (ms) | p99 (ms) | CPU db (%) | CPU db-replica (%) |
|---|---:|---:|---:|---:|
| primary (DB_REPLICA_HOST=db) | | | | |
| replica | | | | |

## 3. Read-your-writes (load/read-your-writes.js)
| REPLICA_DELAY | stale_reads (%) |
|---|---:|
| 0  | |
| 1s | |

## 4. Connections
| Setup | connections in pg_stat_activity during the flash sale |
|---|---:|
| inventory → db directly (INVENTORY_DB_HOST=db, PORT=5432) | |
| inventory → pgbouncer | |
PgBouncer SHOW POOLS: cl_active ____ , sv_active ____ , cl_waiting ____

## 5. Catalog listing before/after the indexes (EXPLAIN ANALYZE + sweep)
| | Execution time (ms) | Plan (main node) | Capacity with p99 < 300 ms (req/s) |
|---|---:|---|---:|
| before (V2) | | | |
| after (V3)  | | | |

## 6. Failover (step 4)
| synchronous_commit (REPLICA_DELAY=1s) | stale_reads (%) | iteration_duration avg |
|---|---:|---:|
| on           | | |
| remote_apply | | |
Replica stopped, synchronous: hold HTTP status ____ , seat 10 afterwards ____
Crash: Ana's hold after the failover (HTTP status) ____ ; seconds until holds worked again ____
Split brain: seat 20 on db ____ , on db-replica ____

## Conclusions
- Which locking strategy would you ship for the flash sale, and why?
- Which reads may go to the replica, and which must not?
- Cache (lab 5) or index (lab 6) for the listing — or both? Justify with your numbers.
- Should holds be replicated synchronously? Give the RPO and the cost of each choice.

Se os seus números forem diferentes

Os resultados de corretude precisam bater em qualquer máquina: o naive termina com vendas duplicadas (quantas varia de execução para execução — 9 e 11 em execuções deste lab; se der 0, rode de novo ou aumente VUS), pessimistic e optimistic sempre terminam com 0. Latências, porcentagens de leituras desatualizadas e contagens de conexões dependem da sua máquina; compare-as apenas entre as suas próprias execuções.

Solução de problemas

Sintoma Causa Correção
db-replica termina com permission denied no replica-entrypoint.sh o script não é executável chmod +x infra/postgres/*.sh e docker compose up -d db-replica
db-replica registra no pg_hba.conf entry for replication connection o volume do primário é anterior a este lab, então o 10-replication.sh nunca rodou docker compose down -v e docker compose up --build -d (o Flyway popula tudo de novo)
db-replica sai: max_connections ... must be at least the primary's value DB_MAX_CONNECTIONS no primário está acima de 100 mantenha ≤ 100, ou aumente também na réplica
o inventory registra password authentication failed via PgBouncer userlist.txt não bate com DB_PASSWORD mantenha os dois como boxoffice (ou atualize os dois)
prepared statement "S_1" already exists um PgBouncer sem max_prepared_statements (imagem mais antiga ou ini editado) use edoburu/pgbouncer:v1.26.0-p0 e o ini desta página
503/404 em /inventory/** pelo edge o gateway não foi reconstruído com a nova rota docker compose up --build -d gateway; depois, curl -s localhost:8080/actuator/gateway/routes \| jq -r '.[].route_id' precisa listar inventory
reservas devolvem 401 sem token (as reservas são protegidas) faça login e passe -H "Authorization: Bearer $TOKEN"
ALTER SYSTEM SET synchronous_standby_names falha com syntax error at or near "-" o nome com hífen não está entre aspas 'FIRST 1 ("db-replica")': aspas duplas em volta do nome, simples em volta do valor
escritas travam, pg_stat_activity mostra wait_event = SyncRep a replicação síncrona está ligada e a réplica caiu suba a réplica, ou ALTER SYSTEM RESET synchronous_standby_names + SELECT pg_reload_conf()
depois de docker compose start db-replica, o db está rodando de novo o db-replica depende do db, então o start sobe ele também pare o db de novo antes de promover; use docker compose up -d --no-deps db-replica
pg_rewind: target server needs to use either data checksums or "wal_log_hints = on" o wal_log_hints estava desligado quando o antigo primário rodava (seção 1 pulada) o antigo primário não consegue voltar; recomece o cluster com docker compose down -v e docker compose up --build -d (o Flyway popula tudo de novo) e refaça o passo 4 a partir da seção 1
escritas falham com cannot execute INSERT in a read-only transaction depois do failover db resolve para o standby que voltou e para o novo primário termine a volta ao primário original
db-replica fica unhealthy depois de um clone novo, e o log diz waiting for checkpoint um replica-entrypoint.sh antigo, sem -c fast atualize o script e rode docker compose up -d --force-recreate db-replica
contêineres mortos (exit 137), inicialização muito lenta memória insuficiente para onze contêineres dê 8 GB ao Docker, feche outros programas, ou pare catalog-2/catalog-3 durante os experimentos do inventory

Boas práticas neste lab

  • Decida no banco, não na aplicação — SELECT … FOR UPDATE com ORDER BY id, ou um compare-and-set de versão, no HoldService.java; veja Transações — o bug da venda duplicada.
  • Mande para a réplica as leituras que podem estar desatualizadas, e todo o resto para o primário — @Transactional(readOnly = true) roteado pelo DataSourceConfig.java; veja Replicação — o atraso e as suas anomalias.
  • Faça pool de conexões na frente do PostgreSQL, e migre contornando o pooler — pgbouncer.ini em modo transação, DB_MIGRATION_HOST para o Flyway no application.yaml do inventory; veja Replicação (pool de conexões).
  • Acrescente um índice para uma consulta medida, conferido com EXPLAIN ANALYZE — V3__index_events.sql; veja Modelos de dados e engines de armazenamento (lendo o EXPLAIN).
  • Saiba o que um failover perde, e isole o antigo primário — a replicação assíncrona perdeu uma reserva que tinha respondido 201; o pg_rewind (rejoin-as-replica.sh) é o único jeito de o antigo primário voltar; veja Replicação — failover.
  • Audite o invariante depois de todo teste — SeatQueries.doubleBookedSeats atrás de /inventory/audit/double-bookings; veja Transações — reservas de assentos.

Limpeza

REPLICA_DELAY=0 docker compose up -d db-replica             # if you left the artificial lag on
INVENTORY_LOCKING=pessimistic docker compose up -d inventory
bash scripts/reset-inventory.sh                             # all seats back on sale
docker compose down                                         # keep the volumes for lab 7

Guarde tudo: o lab 7 acrescenta o order e o Kafka a esta stack e confirma as reservas pelo inventory.

Entrega

  • services/inventory rodando atrás do gateway; o gateway roteia /inventory/** (GETs em /inventory/events/** abertos, reservas protegidas).
  • A tabela da venda relâmpago com as três estratégias, incluindo o resultado da auditoria de cada uma: pelo menos uma execução do naive com vendas duplicadas > 0, e pessimistic/optimistic com 0.
  • db-replica em streaming (o pg_stat_replication a mostra), leituras roteadas para ela e o experimento de read-your-writes com e sem REPLICA_DELAY.
  • PgBouncer na frente do primário, com as contagens de conexões.
  • EXPLAIN ANALYZE da listagem do catálogo antes e depois do V3__index_events.sql, e o sweep depois.
  • O failover do passo 4: a tabela da replicação síncrona, a reserva perdida, o split brain, e a stack de volta com o db como primário e o db-replica transmitindo.
  • results/lab6.md completo com as quatro conclusões.
  • Repositório com a tag lab6; quiz respondido.

Estado final

changed in lab 6
📁 boxoffice/
├── YAML docker-compose.yaml        # replaced: + db-replica (5433), + pgbouncer (6432), + inventory (8083)
├── GNU Bash infra/postgres/10-replication.sh  infra/postgres/replica-entrypoint.sh     # new
├── GNU Bash infra/postgres/rejoin-as-replica.sh                                       # new: pg_rewind after a failover
├──  infra/pgbouncer/pgbouncer.ini  infra/pgbouncer/userlist.txt               # new
├── JavaScript load/lib.js  load/flash-sale.js  load/availability.js  load/read-your-writes.js   # new
├── GNU Bash scripts/reset-inventory.sh                                                # new
├── 📁 services/inventory/        # new: seats, holds, three locking strategies, replica routing
├──  services/catalog/src/main/resources/db/migration/V3__index_events.sql    # new
├── YAML services/gateway/src/main/resources/application.yaml                     # replaced: + /inventory/** route
├──  services/gateway/src/main/java/boxoffice/gateway/SecurityConfig.java     # replaced: GET /inventory/events/** open
└── Markdown results/lab6.md

Próximo: Lab 7 — Assíncrono, em que o order confirma as reservas por meio de eventos.


  1. Martin Kleppmann e Chris Riccomini, Designing Data-Intensive Applications, 2ª ed., O'Reilly — capítulos sobre transações e replicação. ↩

  2. Documentação do PostgreSQL, Explicit Locking, Log-Shipping Standby Servers e Using EXPLAIN. ↩