c. Replicação
Rode todos os comandos a partir da pasta pgcluster/, com o cluster no ar (docker compose up -d --wait). Cada experimento mostra uma propriedade do cluster; leia a saída antes de seguir em frente.
1. Escrever no primário, ler nas réplicas
Crie uma tabela e insira duas linhas no primário:
docker compose exec pg-primary psql -U store -c "CREATE TABLE orders (id serial PRIMARY KEY, item text NOT NULL, created_at timestamptz DEFAULT now());" -c "INSERT INTO orders (item) VALUES ('keyboard'), ('mouse');"
Leia-as em uma réplica:
id | item | created_at
----+----------+-------------------------------
1 | keyboard | 2026-10-31 18:46:45.59094+00
2 | mouse | 2026-10-31 18:46:45.59094+00
Ninguém criou a tabela na réplica: o CREATE TABLE e o INSERT chegaram até ela pelo WAL. Experimente também a pg-replica2.
2. Réplicas são somente leitura
Uma réplica física é uma cópia exata das páginas do primário: uma escrita local a faria divergir. Toda escrita precisa ir para o primário — é por isso que a replicação escala as leituras, mas não as escritas.
3. Atraso de replicação (lag) e leituras desatualizadas
A replicação é assíncrona: uma réplica está sempre um pouco atrás. Normalmente são milissegundos, rápido demais para ver; então torne o lag visível pausando a reexecução na pg-replica1 (ela continua recebendo o WAL, mas para de aplicá-lo):
Insira uma linha no primário:
Conte as linhas em cada réplica:
docker compose exec pg-replica1 psql -U store -c "SELECT count(*) FROM orders;"
docker compose exec pg-replica2 psql -U store -c "SELECT count(*) FROM orders;"
A pg-replica1 responde 2, a pg-replica2 responde 3. O primário vê o quanto cada réplica está atrasada, em bytes de WAL:
docker compose exec pg-primary psql -U store -c "SELECT application_name, pg_wal_lsn_diff(sent_lsn, replay_lsn) AS bytes_behind FROM pg_stat_replication;"
Retome a reexecução — a linha aparece:
docker compose exec pg-replica1 psql -U store -c "SELECT pg_wal_replay_resume();" -c "SELECT count(*) FROM orders;"
Read-your-writes
É isso que um usuário vê quando a aplicação escreve no primário e depois lê de uma réplica atrasada: "eu criei o pedido, mas ele não está na minha lista". O lag de replicação cresce com carga pesada, consultas longas na réplica ou uma rede lenta. Correções comuns: ler do primário logo depois de uma escrita (para aquele usuário, por alguns segundos) ou usar replicação síncrona com remote_apply (experimento 5) nas consultas que não podem ver dados desatualizados.
4. Roteamento por papel na connection string
Quem decide se uma consulta vai para o primário ou para uma réplica? Aqui, o driver do cliente. A libpq (a biblioteca C por trás do psql, do psycopg do Python, do pg do Node...) aceita vários hosts e um papel exigido:
docker compose run --rm client "host=pg-primary,pg-replica1,pg-replica2 target_session_attrs=read-write" -c "SELECT current_setting('cluster_name');"
O driver tenta os hosts em ordem e fica com o primeiro que aceita escritas. Para leituras, peça um standby e distribua as conexões aleatoriamente:
for i in 1 2 3 4 5 6; do
docker compose run --rm client "host=pg-primary,pg-replica1,pg-replica2 target_session_attrs=standby load_balance_hosts=random" -Atc "SELECT current_setting('cluster_name');"
done
Você deve ver pg-replica1 e pg-replica2 misturados, nunca pg-primary.
target_session_attrs | Escolhe |
|---|---|
read-write / primary | o primário |
standby | uma réplica (falha se não houver nenhuma) |
prefer-standby | uma réplica; o primário se nenhuma réplica estiver no ar |
any (padrão) | o primeiro host que responder |
O driver JDBC do PostgreSQL, usado pelo Spring Boot, tem o mesmo recurso, com outros nomes:
jdbc:postgresql://pg-primary:5432,pg-replica1:5432,pg-replica2:5432/store?targetServerType=primary
jdbc:postgresql://pg-primary:5432,pg-replica1:5432,pg-replica2:5432/store?targetServerType=preferSecondary&loadBalanceHosts=true
Em Failover, você vai ver o benefício real: depois de uma promoção, a mesma connection string encontra o novo primário sem nenhuma alteração.
5. Replicação síncrona
Torne a pg-replica1 síncrona: um commit no primário só retorna depois que a pg-replica1 tem o WAL em disco.
docker compose exec pg-primary psql -U store -c "ALTER SYSTEM SET synchronous_standby_names = 'FIRST 1 (\"pg-replica1\")';" -c "SELECT pg_reload_conf();"
docker compose exec pg-primary psql -U store -c "SELECT application_name, sync_state FROM pg_stat_replication;"
application_name | sync_state
------------------+------------
pg-replica1 | sync
pg-replica2 | async
Agora pare a réplica síncrona e tente escrever:
docker compose stop pg-replica1
docker compose exec pg-primary psql -U store -c "INSERT INTO orders (item) VALUES ('headset');"
O comando trava: o primário espera uma confirmação que não vai chegar. Pressione Ctrl+C:
WARNING: canceling wait for synchronous replication due to user request
DETAIL: The transaction has already committed locally, but might not have been replicated to the standby.
INSERT 0 1
Leia o DETAIL com atenção: a linha está confirmada no primário; o que foi cancelado é apenas a espera. A replicação síncrona não torna o commit atômico entre servidores — ela adia a resposta até a cópia existir. (O statement_timeout não ajuda aqui: ele não cobre a espera no commit.) Em outro terminal, enquanto um comando está travado, você pode vê-lo esperando:
docker compose exec pg-primary psql -U store -c "SELECT pid, wait_event, query FROM pg_stat_activity WHERE wait_event = 'SyncRep';"
Esperar por uma réplica específica a torna um ponto único de falha para as escritas. Um quórum é mais robusto: esperar por qualquer uma das duas réplicas.
docker compose exec pg-primary psql -U store -c "ALTER SYSTEM SET synchronous_standby_names = 'ANY 1 (\"pg-replica1\", \"pg-replica2\")';" -c "SELECT pg_reload_conf();"
docker compose exec pg-primary psql -U store -c "INSERT INTO orders (item) VALUES ('webcam');"
O insert retorna na hora: a pg-replica2 o confirmou. Traga a pg-replica1 de volta e veja os estados:
docker compose start pg-replica1
docker compose exec pg-primary psql -U store -c "SELECT application_name, sync_state FROM pg_stat_replication;"
application_name | sync_state
------------------+------------
pg-replica2 | quorum
pg-replica1 | quorum
Quanto o primário espera é definido por synchronous_commit:
synchronous_commit | O commit retorna quando a réplica síncrona... | Garante |
|---|---|---|
remote_write | recebeu o WAL (em memória) | sobrevive a um crash do primário |
on (padrão) | gravou o WAL em disco | sobrevive a um crash dos dois |
remote_apply | reexecutou o WAL | uma leitura naquela réplica vê o commit (sem leitura desatualizada) |
Volte à replicação assíncrona antes da próxima página:
docker compose exec pg-primary psql -U store -c "ALTER SYSTEM RESET synchronous_standby_names;" -c "SELECT pg_reload_conf();"
6. Sob carga (opcional)
O pgbench, que vem com o PostgreSQL, gera uma carga no estilo bancário. Crie as tabelas dele (≈ 160 MB) e rode 8 clientes por 30 segundos no primário:
docker compose run --rm --entrypoint pgbench client -h pg-primary -i -s 10
docker compose run --rm --entrypoint pgbench client -h pg-primary -c 8 -T 30 -P 5
Enquanto ele roda, acompanhe o lag em outro terminal (repita o comando algumas vezes):
docker compose exec pg-primary psql -U store -c "SELECT application_name, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn)) AS behind, replay_lag FROM pg_stat_replication;"
Depois, rode um benchmark somente leitura (-S) apenas contra as duas réplicas, cada conexão para uma delas, escolhida ao acaso — as leituras escalam com o número de réplicas. O -n pula o VACUUM que o pgbench roda primeiro, e que uma réplica recusa:
docker compose run --rm --entrypoint pgbench client -S -n -c 8 -T 15 "host=pg-replica1,pg-replica2 load_balance_hosts=random"
Repita o exercício com a replicação síncrona ligada (ANY 1) e compare o tps do benchmark de escrita: essa diferença é o preço do RPO = 0.