Read replicas: escalando leituras com segurança

Sistemas de banco de dados relacionais frequentemente enfrentam um gargalo clássico: o nó primário (leader) precisa lidar com todas as operações de escrita e, simultaneamente, atender consultas de leitura. Conforme a aplicação cresce, as consultas de leitura — relatórios pesados, dashboards em tempo real, APIs de busca — consomem CPU, memória e I/O, degradando a performance das escritas. A solução mais adotada para esse cenário é a implantação de read replicas. Este artigo explora os fundamentos, a arquitetura, a configuração e as armadilhas desse padrão de escalabilidade horizontal.

1. Fundamentos de Read Replicas

Uma read replica é uma cópia do banco de dados primário que recebe e aplica continuamente as alterações geradas no líder. O mecanismo central é a replicação de WAL (Write-Ahead Log). No PostgreSQL, por exemplo, o primário envia registros de WAL para as réplicas, que os reaplicam localmente.

Existem três modos de replicação:

  • Statement-based: replica comandos SQL completos (INSERT, UPDATE, DELETE). Simples, mas pode gerar inconsistências se os comandos usarem funções não-determinísticas (como NOW() ou RANDOM()).
  • Row-based: replica as linhas modificadas (antes e depois). Mais seguro e preciso, porém gera mais tráfego de rede. É o padrão no MySQL 8.0+ e recomendado no PostgreSQL.
  • Mixed: combina os dois modos — usa statement-based para comandos seguros e row-based para comandos não-determinísticos.

Casos de uso típicos incluem:

  • Dashboards e BI: consultas agregadas pesadas que não precisam de dados em tempo real.
  • APIs de consulta: endpoints que listam produtos, histórico de pedidos ou perfis de usuário.
  • Relatórios noturnos: processamento de grandes volumes de dados sem impactar o OLTP primário.

2. Arquitetura e Topologias de Replicação

A topologia mais comum é single leader com múltiplas réplicas:

Primário (escrita)
   ├── Réplica 1 (leitura)
   ├── Réplica 2 (leitura)
   └── Réplica 3 (leitura)

Todas as escritas vão para o primário; as réplicas atendem apenas leituras. O tráfego de replicação é assíncrono por padrão, o que introduz latência (lag).

Para ambientes muito grandes, usa-se cadeia de replicação (cascading replicas):

Primário
   └── Réplica A (intermediária)
        └── Réplica B (folha)

Isso reduz a carga de WAL no primário, mas aumenta o lag total.

Em implantações multi-região, réplicas são posicionadas geograficamente próximas aos usuários finais para reduzir latência de leitura. A replicação entre regiões é quase sempre assíncrona.

3. Configuração e Provisionamento de Réplicas

Criar uma réplica no PostgreSQL envolve:

  1. Fazer um snapshot do primário (via pg_basebackup ou snapshot do filesystem).
  2. Configurar os parâmetros no postgresql.conf da réplica:
wal_level = replica
hot_standby = on
max_connections = 200
max_wal_senders = 5
  • wal_level: deve ser replica ou logical para permitir replicação.
  • hot_standby: permite consultas na réplica enquanto ela aplica WAL.
  • max_wal_senders: número máximo de conexões de replicação simultâneas.

Para monitorar o lag, use pg_stat_replication:

SELECT application_name, state, 
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS lag_bytes,
       (EXTRACT(EPOCH FROM now() - replay_lag) * 1000)::bigint AS lag_ms
FROM pg_stat_replication;

4. Roteamento de Consultas: Estratégias e Ferramentas

De nada adianta ter réplicas se a aplicação não as utiliza corretamente. Três estratégias principais:

Balanceamento no driver/cliente

Bibliotecas como psycopg2 (Python) ou pgx (Go) permitem configurar múltiplos endpoints. Exemplo com libpq:

host=primario,replica1,replica2 port=5432 dbname=meubd
target_session_attrs=read-write  # força escrita no primário

Uso de proxies

Ferramentas como PgBouncer (com modo de réplica), HAProxy e pgbouncer-rr roteiam consultas com base no tipo de comando (SELECT vs INSERT/UPDATE/DELETE). Exemplo de configuração HAProxy:

frontend db_frontend
    bind *:5432
    use_backend replicas if { ssl_fc_has_crt }  # exemplo simplificado

backend replicas
    server replica1 10.0.1.10:5432 check
    server replica2 10.0.1.11:5432 check

Separação explícita no código

A abordagem mais segura: a aplicação usa duas conexões — uma para escrita (primário) e outra para leitura (pool de réplicas). Exemplo conceitual:

# Pseudocódigo
conexao_escrita = criar_conexao("primario:5432")
conexao_leitura = criar_conexao_pool(["replica1:5432", "replica2:5432"])

def buscar_produtos():
    return conexao_leitura.executar("SELECT * FROM produtos")

def criar_pedido(dados):
    return conexao_escrita.executar("INSERT INTO pedidos ...")

5. Consistência de Dados e Stale Reads

O maior desafio das read replicas é a consistência eventual. Devido ao lag de replicação, uma consulta na réplica pode retornar dados obsoletos. Exemplo:

-- Usuário insere um pedido no primário (T0)
INSERT INTO pedidos (id, status) VALUES (1001, 'PENDENTE');

-- A réplica ainda não recebeu a mudança (T0 + 100ms)
SELECT * FROM pedidos WHERE id = 1001;  -- Retorna vazio! (stale read)

Técnicas de mitigação:

  • Session stickiness: fixa um cliente sempre na mesma réplica durante a sessão.
  • Leitura imediata no primário: após uma escrita crítica (ex: confirmação de pagamento), força a leitura seguinte no primário.
  • pg_sleep controlado: em casos extremos, espera um tempo fixo antes de ler na réplica (não recomendado como padrão).
  • synchronous_commit e synchronous_standby_names: garante que uma réplica específica confirme a escrita antes de retornar ao cliente. Aumenta latência de escrita, mas elimina stale reads para aquela réplica.

Configuração no primário:

synchronous_commit = on
synchronous_standby_names = 'replica_critica'

6. Failover e Manutenção de Réplicas

Quando o primário falha, uma réplica precisa ser promovida. Ferramentas como Patroni e repmgr automatizam esse processo.

Promoção manual:

pg_ctl promote -D /var/lib/postgresql/replica

Para manutenção sem downtime:

  1. Drenar conexões: redirecione o tráfego de leitura para outras réplicas.
  2. Parar a réplica e aplicar atualizações de schema ou patches.
  3. Reiniciar e permitir que ela recupere o lag.

Atualizações de schema (DDL) em réplicas merecem cuidado. No PostgreSQL, DDLs são replicadas via WAL, então uma migração executada no primário automaticamente chega às réplicas. Contudo, se a réplica estiver com lag, a tabela pode ficar temporariamente inconsistente. Para migrações críticas, use replicação lógica (publicação/assinatura) que permite aplicar DDLs seletivamente.

7. Monitoramento e Alertas para Réplicas

Métricas essenciais:

  • Lag em bytes e segundos: via pg_stat_replication e pg_wal_lsn_diff.
  • Taxa de WAL recebida: pg_stat_wal_receiver na réplica.
  • Conexões ativas: número de sessões de leitura concorrentes.
  • Idade das transações mais antigas: evita transaction ID wraparound.

Ferramentas recomendadas:

  • Prometheus + postgres_exporter: coleta métricas de replicação.
  • Grafana: dashboards com alertas para lag > 10s ou réplica offline.
  • Check_pgactivity (Nagios/Icinga): monitora streaming e replay_lag.

Exemplo de alerta via SQL:

SELECT CASE 
    WHEN (pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) > 100000000) 
    THEN 'ALERTA: Lag superior a 100MB'
    ELSE 'OK'
END AS status
FROM pg_stat_replication;

Conclusão

Read replicas são uma ferramenta poderosa para escalar leituras em bancos relacionais, mas exigem planejamento cuidadoso. A escolha da topologia, o roteamento correto de consultas, o monitoramento constante do lag e a estratégia de failover determinam o sucesso da implantação. Ignorar a consistência eventual ou negligenciar o monitoramento pode levar a dados obsoletos e indisponibilidade. Quando bem configuradas, as réplicas permitem que o sistema lide com milhares de consultas simultâneas sem sacrificar a performance das escritas.

Referências