DevGuideTK
← Trilha Backend

Replicação

Cópias do banco para leitura e para tolerar falhas, e o atraso entre a principal e as réplicas.

O código desta página foi executado nas cinco linguagens contra um PostgreSQL 18 com um principal e uma réplica em replicação por fluxo. As medições do nível Conceito vêm do mesmo par.

Ler o que acabou de gravar, em cada linguagem

Cada versão tem duas funções. criarPedido grava no principal e, num segundo comando, lê a posição atual do log (pg_current_wal_lsn()). buscarPedido recebe esse LSN e pergunta à réplica se ela já aplicou o log até ali (pg_last_wal_replay_lsn()): se sim, lê da réplica; se não, lê do principal.

O teste chama as funções três vezes: com a réplica em dia; com a aplicação do log na réplica pausada (pg_wal_replay_pause()), passando o LSN; e com a réplica pausada, sem o LSN.

import pg from "pg";

const principal = new pg.Pool({ connectionString: process.env.URL_PRINCIPAL });
const replica = new pg.Pool({ connectionString: process.env.URL_REPLICA });

// Grava no principal e devolve a posição do log (LSN) depois da confirmação
async function criarPedido(total: number) {
  const { rows } = await principal.query(
    "INSERT INTO pedidos (total) VALUES ($1) RETURNING id", [total]);
  // Num comando separado: dentro do INSERT, o LSN ainda não inclui o COMMIT
  const lsn = await principal.query("SELECT pg_current_wal_lsn()::text AS lsn");
  return { id: Number(rows[0].id), lsn: lsn.rows[0].lsn as string };
}

// Lê da réplica só se ela já aplicou a gravação; senão, do principal
async function buscarPedido(id: number, lsnMinimo?: string) {
  if (lsnMinimo) {
    const { rows } = await replica.query(
      "SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS em_dia", [lsnMinimo]);
    if (!rows[0].em_dia) {
      const r = await principal.query("SELECT id, total FROM pedidos WHERE id = $1", [id]);
      return { origem: "principal", pedido: r.rows[0] };
    }
  }
  const r = await replica.query("SELECT id, total FROM pedidos WHERE id = $1", [id]);
  return { origem: "réplica", pedido: r.rows[0] };
}

// Teste: réplica em dia, e depois com a aplicação do log pausada
const a = await criarPedido(10);
await new Promise((r) => setTimeout(r, 300));
console.log("réplica em dia:", JSON.stringify(await buscarPedido(a.id, a.lsn)));
await replica.query("SELECT pg_wal_replay_pause()");
const b = await criarPedido(20);
console.log("réplica pausada, com LSN:", JSON.stringify(await buscarPedido(b.id, b.lsn)));
console.log("réplica pausada, sem LSN:", JSON.stringify(await buscarPedido(b.id)));
await replica.query("SELECT pg_wal_replay_resume()");
await principal.end();
await replica.end();

O resultado, nas cinco linguagens

SituaçãoLeu deAchou o pedido?
Réplica em dia, com o LSNréplicasim
Réplica pausada, com o LSNprincipalsim
Réplica pausada, sem o LSNréplicanão

A terceira linha é o problema que a técnica resolve: sem conferir o LSN, a leitura logo depois da gravação não encontrou o pedido recém-criado. O resultado foi o mesmo nas cinco linguagens.

O que muda de uma linguagem para outra

Dois bancosComparar LSNs
TypeScriptDois pg.PoolO LSN vai como texto e é convertido com $1::pg_lsn
PythonDuas conexões (ou dois pools)Igual, com %s::pg_lsn
GoUma struct com dois pgxpool.PoolIgual, com $1::pg_lsn
JavaDuas Connection (ou dois DataSource)Igual, com ?::pg_lsn
C#Dois NpgsqlDataSourceIgual, com $1::pg_lsn

A comparação fica no banco, com o tipo pg_lsn: o LSN tem o formato 0/3000148, e comparar como texto daria resultados errados. Num sistema real, o LSN da última gravação do usuário fica na sessão ou num cookie, e vale para todas as leituras dele nos segundos seguintes.

Frameworks e ORMs têm configurações para separar leituras e gravações (réplicas no Django e no Rails, DbContext separados no EF Core, AbstractRoutingDataSource no Spring). Eles roteiam por tipo de operação, e o "ler o que gravou" costuma ficar por conta da aplicação.

Montar a réplica

Os comandos usados no teste, no PostgreSQL 18:

# No principal: um usuário de replicação e um slot para a réplica
psql -c "CREATE ROLE replicador WITH REPLICATION LOGIN PASSWORD '...'"
psql -c "SELECT pg_create_physical_replication_slot('replica1')"
# e, no pg_hba.conf: host replication replicador <rede da réplica> scram-sha-256

# Na máquina da réplica: copia o principal e grava a configuração de réplica (-R)
pg_basebackup -h principal -U replicador -D "$PGDATA" -R -S replica1 -X stream

E as configurações que o nível Conceito testou, no principal:

synchronous_standby_names = 'walreceiver'   # no teste; com quórum: 'ANY 1 (r1, r2)'
max_slot_wal_keep_size = '256MB'             # limite de WAL retido por slot

Para conferir o estado:

-- No principal
SELECT application_name, state, sync_state, replay_lag FROM pg_stat_replication;
SELECT slot_name, active, wal_status FROM pg_replication_slots;

-- Na réplica
SELECT pg_is_in_recovery(), pg_last_wal_replay_lsn(), now() - pg_last_xact_replay_timestamp() AS atraso;

Lista de revisão

Escolha

  • O time sabe quanto se perde numa troca de principal no modo de replicação escolhido?
  • A replicação síncrona usa quórum de duas ou mais réplicas, e nunca uma única?
  • Há uma réplica em outra zona, e um plano para desastres regionais?

Troca de principal

  • A troca é automática, com consenso, ou feita por um banco gerenciado?
  • O principal antigo é impedido de voltar aceitando gravações?
  • A aplicação encontra o principal novo e reconecta sozinha?

Leituras

  • Leituras na réplica são só as que aceitam atraso?
  • Telas que mostram o que o usuário acabou de gravar usam "ler o que gravou", com o LSN lido depois da confirmação?
  • Decisões de negócio leem do principal?

Monitoramento

  • Há alerta para atraso de replicação, slots inativos e sessões esperando a réplica?
  • max_slot_wal_keep_size está configurado?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Montar uma réplica do PostgreSQL

    Quero montar uma réplica de leitura do PostgreSQL para estudar.
    
    Pergunte se vou usar containers ou máquinas, e qual versão.
    
    Me guie, com os comandos completos:
    1. O usuário de replicação, o pg_hba.conf e o slot.
    2. O pg_basebackup com -R na réplica.
    3. Conferir o estado da replicação nos dois lados.
    4. Medir o atraso sob carga.
    5. Promover a réplica e entender o que aconteceu com o principal antigo.
  • Implementar ler o que gravou

    Quero implementar a técnica de ler o que gravou na minha aplicação, com réplicas de leitura.
    
    Pergunte qual linguagem, framework e driver eu uso, e como guardo a sessão do usuário.
    
    Vamos em etapas:
    1. Ler o LSN depois do COMMIT de cada gravação.
    2. Guardar o LSN da última gravação do usuário.
    3. Antes de ler, conferir na réplica se ela já aplicou esse LSN, e cair para o principal se não.
    4. Testes com a réplica pausada.
    5. Métricas de quantas leituras caíram para o principal.
  • Revisar uma configuração de replicação

    Vou colar abaixo a configuração de replicação do meu banco e o código que escolhe entre principal e réplica. Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. O modo de replicação combina com o que pode ser perdido?
    2. Há réplica síncrona única que pode travar as gravações?
    3. A troca de principal é automática e protegida contra dois principais?
    4. Leituras críticas vão para o principal?
    5. Telas que mostram o que o usuário gravou leem o que gravou?
    6. Há limite de WAL retido por slot e monitoramento do atraso?
    
    Para cada problema, descreva o efeito numa falha e mostre a correção.
    
    Configuração e código:
    [cole aqui]