a. Visão geral
Objetivo principal
O objetivo principal deste hands-on é montar um cluster PostgreSQL no Docker Compose — um primário e duas réplicas mantidas em sincronia por streaming replication — e usá-lo para ver os conceitos de um cluster de banco de dados: replicação, escala de leituras, atraso de replicação (lag), commits síncronos, failover e escritas perdidas.
Todos os hands-on até aqui rodaram um único container db. Quando ele para, todo serviço que depende dele para também: ele é um ponto único de falha. Escalar o gateway para três réplicas (hands-on 7) e adicionar um cache (hands-on 8) tirou carga do banco de dados, mas não removeu esse ponto único. Um cluster de banco de dados mantém cópias dos dados em vários servidores, de modo que um deles pode assumir quando outro falha.
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 um cluster de banco de dados
| Objetivo | O que o cluster oferece | O que ele não oferece |
|---|---|---|
| Disponibilidade | outro servidor já tem os dados e pode se tornar o primário | zero downtime: um failover ainda leva segundos |
| Escala de leituras | os SELECTs se distribuem entre as réplicas | escala de escritas: toda escrita continua indo para um primário |
| Durabilidade | uma cópia de cada commit em outra máquina | proteção contra um DELETE por engano: ele também é replicado (é para isso que servem os backups) |
Escalar as escritas exige outra técnica, o sharding (dividir os dados entre primários independentes, como fazem o Citus ou o Vitess). Este hands-on trata de replicação: cada nó guarda todos os dados.
Como o PostgreSQL replica: o WAL
Antes de alterar uma página de dados, o PostgreSQL grava a alteração no WAL (write-ahead log), um log em disco que só recebe acréscimos (append-only). Depois de um crash, o servidor reexecuta o WAL para se recuperar. A replicação reaproveita o mesmo log: o primário envia o seu WAL às réplicas, que o reexecutam, byte a byte, e terminam com cópias idênticas.
sequenceDiagram
participant App as Serviço
participant P as Primário
participant R1 as Réplica 1
participant R2 as Réplica 2
App->>P: INSERT INTO orders ...
P->>P: acrescenta a alteração ao WAL
P-->>App: COMMIT ok
P->>R1: envia registros do WAL (stream)
P->>R2: envia registros do WAL (stream)
R1->>R1: reexecuta o WAL
R2->>R2: reexecuta o WAL
App->>R1: SELECT * FROM orders
R1-->>App: linhas (até o último registro reexecutado) | Replicação física (streaming) | Replicação lógica | |
|---|---|---|
| O que trafega | bytes do WAL: alterações em páginas de dados | alterações de linhas (INSERT/UPDATE/DELETE) por tabela |
| Réplica | uma cópia exata do servidor inteiro, somente leitura | um banco de dados independente, que aceita escritas |
| Granularidade | tudo (todos os bancos) | tabelas escolhidas (publication / subscription) |
| Versões | mesma versão major | podem ser diferentes (usada em upgrades) |
| Usada para | alta disponibilidade, réplicas de leitura | migrações, integrações, cópias parciais |
Este hands-on usa replicação física por streaming: a base da alta disponibilidade no PostgreSQL e de serviços gerenciados como o Amazon RDS.
Assíncrona ou síncrona?
A pergunta é quando o primário responde COMMIT ok:
| Assíncrona (padrão) | Síncrona | |
|---|---|---|
| O commit retorna | assim que o WAL está no disco do primário | só depois que uma réplica confirma que tem o WAL |
| Latência de escrita | baixa | + uma ida e volta na rede |
| Se o primário morre | os últimos commits podem não ter chegado a nenhuma réplica: escritas perdidas (RPO > 0) | nenhuma transação confirmada é perdida (RPO = 0) |
| Se a réplica morre | nada muda | os commits bloqueiam até uma réplica voltar (ou a configuração mudar) |
Este é o teorema CAP na prática: a replicação síncrona prefere a consistência (nenhuma escrita confirmada é perdida) e abre mão da disponibilidade das escritas quando a réplica está inacessível; a replicação assíncrona prefere a disponibilidade e aceita que quem lê de uma réplica veja dados desatualizados e que um failover perca as últimas escritas.
Arquitetura
flowchart LR
app["serviço\n(account, order...)"]
subgraph cluster [Cluster PostgreSQL]
p[("pg-primary\n:5432\nleitura e escrita")]
r1[("pg-replica1\n:5433\nsomente leitura")]
r2[("pg-replica2\n:5434\nsomente leitura")]
p e1@-->|"stream do WAL"| r1
p e2@-->|"stream do WAL"| r2
end
app e0@==>|"escritas + leituras"| p
app e3@-.->|"leituras"| r1
app e4@-.->|"leituras"| r2
e0@{ animate: true }
e1@{ animate: true }
e2@{ animate: true } - Os três nós rodam a imagem oficial
postgres:17. Um pequeno entrypoint decide o papel: um nó vazio comPRIMARY_HOSTclona o primário compg_basebackupe sobe como hot standby (ele aceita leituras). - A aplicação decide para onde vai cada consulta. Aqui, o roteamento fica na connection string: a libpq e o driver JDBC do PostgreSQL aceitam uma lista de hosts e escolhem o que tem o papel certo (
target_session_attrs/targetServerType).
Failover
Quando o primário falha, uma réplica precisa ser promovida a primário, as outras réplicas precisam segui-la e os clientes precisam encontrá-la. Neste hands-on, você faz cada passo à mão, para ver o que uma ferramenta automática faz por você:
| Passo | À mão (este hands-on) | Automatizado |
|---|---|---|
| detectar a falha | docker compose ps | health checks, heartbeats |
| escolher e promover uma réplica | SELECT pg_promote(); | eleição de líder |
| reapontar as outras réplicas | ALTER SYSTEM SET primary_conninfo | a ferramenta as reconfigura |
| redirecionar os clientes | connection string com vários hosts | IP virtual, DNS, proxy (HAProxy, PgBouncer) |
| trazer o primário antigo de volta | pg_rewind | a ferramenta faz o rewind dele |
Split brain
A falha perigosa não é um primário morto, mas um primário que parece morto — isolado pela rede, mas ainda aceitando escritas. Se uma réplica for promovida, o cluster passa a ter dois primários e os dados divergem. Por isso, o failover automático precisa de acordo entre vários nós, um algoritmo de consenso: o Patroni guarda o lock de líder no etcd (Raft), e os serviços gerenciados (RDS Multi-AZ, Aurora, Cloud SQL) fazem o mesmo internamente.
-
Escreva o
compose.yamle os scripts, suba o cluster e verifique que as duas réplicas estão fazendo streaming. -
Escreva no primário, leia nas réplicas, provoque leituras desatualizadas, roteie por papel e ative os commits síncronos.
-
Pare o primário, promova uma réplica, reaponte o cluster, perca uma escrita de propósito e traga o primário antigo de volta.