DadosIntermediário7 min de leitura6 seçõesmeados dos anos 1990, Data aproximada

Réplica de leitura

Quando as leituras afogam o banco antes das escritas, a saída barata é copiar: o primário recebe todas as escritas e as replica para nós somente-leitura que absorvem as consultas. Escala leitura sem tocar no modelo — ao preço de um atraso de replicação que faz as réplicas verem o passado por um instante.

Em 10 segundos

Réplica de leitura é a forma mais barata de escalar um banco que está afogando de tanto SELECT: o primário concentra as escritas e as espalha para nós somente-leitura que absorvem as consultas. O custo é o atraso de replicação — as réplicas veem o passado por um instante — e a responsabilidade de rotear cada consulta ao lugar certo. Lembre que um SELECT que decide uma escrita não é leitura pura: ele vai ao primário.

Antes de ter nome

A ideia de manter cópias sincronizadas para dividir carga vem da replicação de sistemas distribuídos dos anos 1980, muito antes de virar um botão na nuvem.

  • #distribuido
  • #replicacao
  • #escalabilidade
  • #leitura
  • #banco
Analogia

O original e as fotocópias da biblioteca

A biblioteca tem um único exemplar original que pode ser corrigido e anotado — e várias fotocópias espalhadas pelas salas para quem só quer ler. Cem pessoas leem as cópias ao mesmo tempo sem incomodar quem cuida do original. Quando o original muda, as cópias são refeitas — e há um intervalo em que uma sala ainda tem a versão antiga. É exatamente o trade-off da réplica: leitura barata e em paralelo, ao preço de um atraso até a cópia alcançar o original.

01Conceito

O atraso que muda tudo

A réplica não é o primário no mesmo instante: ela recebe o log de mudanças (o WAL, no Postgres) e o aplica com um atraso — normalmente milissegundos, mas que cresce sob carga de escrita.

Esse lag é a mesma janela da consistência eventual, e o caso que mais dói é o mesmo: ler da réplica logo depois de escrever no primário devolve o valor antigo. Roteamento cego 'todo SELECT vai à réplica' quebra exatamente aí.

Há uma sutileza que engana muita gente: nem todo SELECT é uma leitura pura. Um SELECT saldo ... FOR UPDATE, ou qualquer consulta cujo resultado decide uma escrita seguinte (checar estoque para depois debitar), precisa do valor atual e do lock — então vai ao primário, apesar de começar com SELECT. Rotear por 'a query começa com SELECT?' é uma heurística que falha justamente nas operações críticas.

A relação com CQRS costuma confundir. Réplica de leitura é infraestrutura: o mesmo esquema, a mesma query, só que servida de outro nó. CQRS é modelagem: um modelo de escrita e um modelo de leitura diferentes, muitas vezes em tecnologias diferentes (o transacional grava, o Elasticsearch serve a busca). Réplica é o degrau anterior e mais barato; CQRS é a mudança de modelo que você faz quando espelhar o mesmo esquema não basta mais.

Sob falha, a réplica introduz um modo de erro próprio: se a replicação para e ninguém percebe, as consultas continuam respondendo — com dado cada vez mais velho. Diferente de um banco que cai e grita, a réplica atrasada mente em silêncio. Por isso o lag de replicação é uma métrica de primeira classe: sem alerta sobre ele, a divergência só aparece quando um usuário reclama.

02No mundo real

Como isso aparece na prática

O relatório que derrubava o checkout

Cenário

Todo início de mês, a equipe financeira rodava um relatório pesado que varria meses de pedidos. Enquanto ele rodava, a latência do checkout subia e algumas compras expiravam por timeout.

Como o padrão entra

O relatório e as demais consultas analíticas passaram a rodar contra uma réplica dedicada, deixando o primário exclusivamente para o caminho transacional do checkout.

Trade-off: O relatório passou a enxergar dados com alguns segundos de atraso em relação ao primário — irrelevante para um fechamento mensal, e um ótimo negócio diante do checkout protegido.

O painel que mostrava o pedido inexistente

Cenário

Após criar um pedido, o usuário era redirecionado para o painel, que lia da réplica. Às vezes o pedido recém-criado ainda não tinha replicado, e o painel exibia 'nenhum pedido encontrado'.

Como o padrão entra

A leitura imediatamente após a criação passou a ir ao primário por alguns segundos (read-your-writes), enquanto as visitas posteriores ao painel seguiram na réplica.

Trade-off: Uma fração pequena das leituras voltou ao primário logo após cada escrita. É um custo mínimo perto de um usuário achando que o pedido que ele acabou de fazer sumiu.

03Cuidado

Onde isto dá errado

  1. Rotear por 'começa com SELECT'

    Uma consulta que lê para decidir uma escrita — checar estoque, ler saldo com FOR UPDATE — precisa do valor atual e do lock, e tem que ir ao primário. A heurística de rotear todo SELECT para a réplica falha exatamente nas operações que menos podem falhar.

  2. Ignorar o lag de replicação

    Réplica que para de replicar não cai: continua respondendo com dado cada vez mais velho, em silêncio. Sem alerta sobre o lag, a divergência só aparece quando um usuário reclama de um valor que 'sumiu'.

  3. Esperar que réplica escale escrita

    Toda escrita continua indo ao único primário; a réplica só divide leitura. Se o gargalo é escrever, adicionar réplicas não move o ponteiro — o caminho passa a ser sharding.

  4. Ler da réplica logo depois de escrever

    É o read-your-writes de novo: o usuário acabou de gravar e a leitura seguinte cai numa réplica atrasada, mostrando o valor antigo. A janela é curta, mas é onde o usuário mais tem certeza de qual valor deveria aparecer.

04Estrutura

Um primário, muitas réplicas

Diagrama de classes
05Implementação

Roteando escrita e leitura

// Uma pool para escrever, outra para ler. A regra e simples de
// enunciar e facil de errar: escrita e "read-your-writes" vao ao
// primario; o resto vai a replica.

const primario = criarPool(process.env.DB_PRIMARIO);
const replica = criarPool(process.env.DB_REPLICA); // pode ser N replicas

async function executar(sql: string, params: unknown[], opts = { escrita: false }) {
  const pool = opts.escrita ? primario : replica;
  return pool.query(sql, params);
}

// Consulta pesada de relatorio: replica, sem tocar no primario.
await executar("SELECT ... FROM pedidos WHERE ...", [], { escrita: false });

// Debito: primario, sempre.
await executar("UPDATE contas SET saldo = saldo - $1 WHERE id = $2", [v, id], {
  escrita: true,
});

// A pegadinha: um SELECT que decide uma escrita NAO e leitura pura.
// Ele precisa do valor atual, entao vai ao primario tambem.
06Decisão

Quando usar × quando evitar

Use quando

3
  • Carga de leitura muito maior que a de escrita.
  • Isolar consultas pesadas do caminho transacional.
  • Primeiro degrau de escala, antes do sharding.

Evite quando

2
  • Quando o gargalo é a escrita.
  • Quando nenhuma leitura tolera o atraso de replicação.

Testar o que ficou

4 armadilhas de Réplica de leitura viram perguntas, com o nome do padrão escondido, misturadas com padrões parecidos.

Onde isto aparece de verdade

Você já usa isto, talvez sem saber o nome.

  • Read Replica do Amazon RDSUm clique cria uma cópia somente-leitura que recebe a replicação do primário e absorve as consultas mais pesadas.
  • hot standby do PostgresA réplica de streaming aplica o WAL do primário e serve SELECTs enquanto isso, aliviando o nó de escrita.
  • endpoint de leitura separadoA aplicação manda escritas para um host e leituras para outro — a divisão física entre gravar e consultar.

O padrão em uma linha

Sem nome de domínio, sem cerimônia. Só a ideia.

// Escrita no primario; leitura na replica.
const pool = opts.escrita ? primario : replica;

O que ele cobra

1 salto a mais entre a chamada e o efeito.

  • Introduz um atraso de replicação: a réplica vê o passado até o WAL do primário chegar e ser aplicado
  • Obriga o código a rotear cada consulta, e a saber que um SELECT que decide uma escrita não é leitura pura
  • Falha de replicação vira dado divergente silencioso — precisa de alerta sobre o lag, senão ninguém percebe

Não vale se: o gargalo é a escrita, não a leitura. Réplica não divide carga de escrita — para isso o caminho é sharding, que reparte as próprias linhas.

Onde isto aparece

Continue por aqui