MÓDULO 07 · CONCEITO 05 DE 12

Replicação para escala de leitura

Primary/replica, replication lag, read-your-writes, replica selection. Complemento ao módulo 03 (mecânica) e ao módulo 06 conceito 11 (uso na aplicação) — aqui o foco é como replicação resolve escala de leitura sem comprometer consistência inadequadamente.

Tempo de leitura ~22 min Pré-requisito Módulo 03 (replicação como mecanismo) Próximo Sharding e particionamento

Bancos transacionais modernos — PostgreSQL, MySQL, SQL Server, Oracle — todos suportam replicação assíncrona desde os anos 90. A configuração é familiar: um nó primário aceita writes; um ou múltiplos nós réplica recebem o stream de mudanças e aplicam em modo só-leitura. Aplicações podem rotear queries de leitura para réplicas, multiplicando capacidade de leitura linearmente sem afetar o primário.

Esse padrão é tão estabelecido que parece detalhe operacional. Não é. A escolha de quando usar réplica, qual replica selecionar, como lidar com replication lag, e quando promover réplica em failover — cada uma dessas decisões afeta diretamente a experiência do usuário, e fazer errado é fonte recorrente de bugs sutis. Usuário atualiza perfil; próxima request lê de réplica que ainda não recebeu a mudança; usuário vê dado antigo e culpa o sistema.

Este conceito articula o uso de replicação como ferramenta de escala. A topologia (primary/replica, multi-master, primary/primary), o problema de replication lag e suas mitigações, padrões para garantir read-your-writes, replica selection inteligente, e failover regional. O foco é complementar — o módulo 03 conceito 7 cobriu a mecânica em profundidade; o módulo 06 conceito 11 cobriu uso aplicacional em performance; aqui o ângulo é "como replicação resolve escala horizontal de leitura".

A premissa: replicação é a forma mais econômica de escalar leitura em bancos transacionais antes de partir para sharding (conceito 06 do módulo). Antes de adotar sharding com toda sua complexidade, vertical primário + N réplicas resolve a maioria dos casos.

Topologias canônicas

Há três topologias principais de replicação. Cada uma é apropriada em contexto distinto.

Primary/replica (single primary)

O modelo clássico. Um nó primário aceita writes; múltiplas réplicas recebem stream e aplicam em modo read-only. Aplicação faz writes no primário, lê de qualquer réplica (ou primário) conforme preferência.

Ganhos: simplicidade conceitual, consistência forte no primário, replicas praticamente ilimitadas para leitura. Failover é processo bem entendido (promover uma réplica a primário). Limitações: writes ficam concentrados no primário (limite vertical em writes); replication lag entre primário e réplicas (eventual consistency). Dominante em bancos relacionais tradicionais.

Multi-primary (multi-master)

Múltiplos nós aceitam writes; cada um propaga para os outros. Aplicação pode escrever em qualquer nó próximo (útil para multi-region). Reduz latência de write para usuários geograficamente distantes.

Ganhos: scale write horizontal, latência de write reduzida em multi-region. Limitações: conflict resolution vira problema. Quando dois nós escrevem no mesmo registro simultaneamente, qual vence? Last-write-wins perde dados; CRDTs são complexos. Cassandra, DynamoDB usam multi-master com conflict resolution definido.

Primary/primary (active-active simétrico)

Variante de multi-primary onde nós são verdadeiramente simétricos. Comum em sistemas que tratam apenas operações idempotentes (counters baseados em CRDT, KV stores). Galera Cluster (MySQL), CockroachDB usam variantes.

Para a maioria das aplicações empresariais, primary/replica é a topologia. Os outros entram em cena quando requisitos específicos (multi-region write locality, ou trabalho 100% idempotente) justificam complexidade.

O problema de replication lag

Replicação assíncrona implica que réplicas ficam ligeiramente atrás do primário. O atraso — replication lag — é tipicamente milissegundos em condições normais, mas pode crescer para segundos ou minutos sob carga, falhas de rede, ou writes em batch grande.

Lag tem três fontes principais:

Aplicação assíncrona. Réplica aplica writes em ordem, e cada write tem custo similar ao primário. Se primário gera writes mais rápido que réplica aplica (por exemplo, uma única thread de aplicação na réplica vs múltiplas conexões no primário), lag cresce. PostgreSQL pode usar parallel apply em versões recentes para mitigar.

Latência de rede. Stream de replicação atravessa rede. Em multi-AZ, ~1 ms; em multi-region, dezenas a centenas de ms.

Long transactions. Transação grande no primário gera muito WAL ao mesmo tempo; réplica recebe rajada e demora para processar.

Para sistemas com SLO de "minha mudança é imediatamente visível para mim", lag é problema direto. Usuário atualiza endereço; próxima request lê de réplica desatualizada; usuário vê endereço antigo. Reclamação de "o sistema não salvou".

Read-your-writes — o requisito de UX

O fenômeno acima tem nome: violação de read-your-writes consistency. O usuário espera ver as próprias modificações imediatamente após fazê-las. Se o sistema lê de réplica desatualizada, viola a expectativa.

Há quatro estratégias para garantir read-your-writes apesar da replicação assíncrona.

Estratégia 1 — Read from primary after write

Por uma janela de tempo após write (ex.: 5 segundos), aplicação roteia leituras do mesmo usuário para o primário, não para réplicas. Implementação: cookie ou session com timestamp do último write; lógica de roteamento decide.

Ganhos: simples, robusto. Custos: aumenta carga no primário; janela tem que ser maior que lag típico (que pode variar).

Estratégia 2 — Replica com timestamp/LSN

Aplicação rastreia a posição de write feito (LSN em PostgreSQL, GTID em MySQL). Em leitura, escolhe réplica que já aplicou pelo menos esse LSN. Se nenhuma estiver atualizada, espera ou cai para primário.

Ganhos: precisão fina — garante exatamente que a mudança específica é visível. Custos: complexidade na aplicação (rastrear LSN); pode introduzir latência esperando réplica.

Estratégia 3 — Synchronous replication

Configurar pelo menos uma réplica como síncrona — o primário só confirma write após receber confirmação dela. Garante que essa réplica está sempre consistente; pode ser usada para reads críticos sem risco de lag.

PostgreSQL synchronous_commit = on + synchronous_standby_names; MySQL semi-sync replication. Custos: write latência mais alta (espera replicação); se réplica sync cai, primário pode bloquear writes.

Estratégia 4 — Cache / aplicação layer

Após write, aplicação cacheia o valor escrito (em Redis, ou local). Próximas reads do mesmo usuário pegam do cache, não do banco. Mais simples; só funciona para padrões "read após write" pelo mesmo usuário.

Em produção, sistemas tipicamente combinam várias estratégias. Estratégia 1 (read from primary após write) para padrão geral; estratégia 3 (sync replication) para uma réplica crítica; estratégia 4 para casos específicos.

Replica selection — qual réplica usar

Em sistema com 5 réplicas, cada query precisa escolher uma. A política importa.

Round-robin. Mais simples. Distribui uniformemente. Bom default.

Latência (closest replica). Em multi-region, prefira réplica mais próxima geograficamente. Reduz latência cross-region.

Lag-aware. Prefira réplica com menor lag conhecido. Útil em padrões read-your-writes ou em queries que toleram pequeno lag mas não grande.

Workload-aware. Distinguir "queries OLTP" (tempo curto) de "queries analíticas" (tempo longo). Roteie analíticas para uma réplica dedicada para evitar competir com OLTP.

Drivers modernos (npgsql, Postgres JDBC, libpq) têm suporte a multi-host com algumas dessas políticas. Aplicações também podem fazer roteamento explícito via two connection strings (uma para primário, outra para réplicas).

Failover — promovendo réplica

Quando o primário cai, uma réplica é promovida. Esse processo — failover — pode ser manual ou automático, e tem subtilezas que precisam de atenção.

Detecção de falha. Quem decide que primário está fora? Heartbeat de health check é primeiro passo, mas há risco de "split brain" (rede particiona; um quórum acha que primário está fora; primário ainda está vivo, acessível por outro grupo). Soluções modernas usam consensus (etcd, Zookeeper) para evitar split brain.

Promoção da réplica. A réplica mais up-to-date é promovida (em algumas configurações, é eleita explicitamente).

Reconfiguração de aplicação. Aplicação precisa parar de escrever no antigo primário (que pode estar fora ou ter virado réplica) e começar a escrever no novo. Em sistemas cloud, isso é frequentemente automático (RDS Multi-AZ refazendo DNS).

Reconciliação. Quando o antigo primário volta, ele precisa ser configurado como réplica (ou descartado). Writes que ele tinha mas não chegaram à nova primária são perdidos — portanto sync replication é importante para sistemas onde isso importa.

AWS RDS Multi-AZ, Aurora, Postgres com Patroni, MySQL com Orchestrator — todos automatizam essas etapas. SLO típico de failover: 30 segundos a 2 minutos de indisponibilidade.

Replica para workload analítico

Padrão importante: réplica dedicada para queries analíticas. Queries OLTP (transacionais, curtas) competem por recursos com queries OLAP (analíticas, longas). Em mesma instância, OLAP pode degradar OLTP — query de relatório que escaneia bilhões de linhas trava locks, satura I/O, prejudica transactions.

Solução clássica: configurar uma réplica especificamente para analytics. Tem hardware potencialmente diferente (mais RAM, CPUs otimizadas para scan), índices diferentes (mais focados em queries de relatório), ou até estrutura diferente (column-oriented via foreign data wrapper).

Aplicação roteia queries OLTP para primário/replica OLTP; queries de relatório para replica analítica. Lag adicional na replica analítica não importa — relatórios toleram alguns segundos de defasagem.

Bancos modernos integram isso como feature: Postgres com hot_standby_feedback, Aurora com replica especializada, BigQuery (data warehouse separado).

Replicação em três stacks

Cada ecossistema configura replicação de forma idiomática.

C# — EF Core com read replicas
// configuração com dois DbContexts (primary e replica)
public class WriteDbContext : DbContext { /* aponta para primary */ }
public class ReadDbContext : DbContext { /* aponta para replica pool */ }

// startup
builder.Services.AddDbContext<WriteDbContext>(opt =>
    opt.UseNpgsql(primaryConnectionString));
builder.Services.AddDbContext<ReadDbContext>(opt =>
    opt.UseNpgsql(replicaConnectionString));

// service routing
public class PedidoService
{
    private readonly WriteDbContext _write;
    private readonly ReadDbContext _read;

    // queries de leitura usam read context
    public async Task<Pedido?> ObterAsync(Guid id) =>
        await _read.Pedidos.FirstOrDefaultAsync(p => p.Id == id);

    // commands usam write context
    public async Task CriarAsync(Pedido p)
    {
        _write.Pedidos.Add(p);
        await _write.SaveChangesAsync();
    }

    // padrão read-your-writes: opt-in para usar primary após write
    public async Task<Pedido?> ObterAposEscritaAsync(Guid id) =>
        await _write.Pedidos.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id);
}

// alternativa: connection string com múltiplos hosts
// Host=primary.db,replica1.db,replica2.db;Target Session Attributes=read-write/read-only
// npgsql (Postgres) suporta isso desde 6.0

.NET tem dois caminhos: dois DbContexts (mais explícito, mais código), ou multi-host connection string com Target Session Attributes (mais simples, menos controle). Para padrões complexos, dois contexts ganha clareza.

Python — SQLAlchemy 2 com routing automático
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker

# engines separadas para primary e replica
engine_primary = create_engine("postgresql://primary.db/app")
engine_replica = create_engine("postgresql://replica.db/app")

# session class com routing baseado em flag
class RoutingSession(Session):
    def get_bind(self, mapper=None, clause=None):
        # se transação tem write, usa primary
        if self._flushing or self.info.get("read_only", False) is False:
            return engine_primary
        return engine_replica

SessionLocal = sessionmaker(class_=RoutingSession)

# uso
async def obter_pedido(id: UUID, session: Session = Depends(get_session)):
    session.info["read_only"] = True
    return session.get(Pedido, id)

async def criar_pedido(cmd: CriarCmd, session: Session = Depends(get_session)):
    pedido = Pedido(**cmd.dict())
    session.add(pedido)
    await session.commit()
    return pedido

# alternativa: psycopg async com multi-host
# DATABASE_URL=postgresql://user@primary.db,replica.db/app?target_session_attrs=any

SQLAlchemy 2 permite roteamento via get_bind override. Bibliotecas como sqlalchemy-replicator automatizam mais. Em Postgres, target_session_attrs na connection string distribui automaticamente entre hosts.

Go — pgxpool com pools separados
package main

import (
    "context"
    "github.com/jackc/pgx/v5/pgxpool"
)

type DB struct {
    primary *pgxpool.Pool
    replica *pgxpool.Pool
}

func NewDB(ctx context.Context) (*DB, error) {
    primary, err := pgxpool.New(ctx, "postgres://primary.db/app")
    if err != nil { return nil, err }
    replica, err := pgxpool.New(ctx, "postgres://replica.db/app")
    if err != nil { return nil, err }
    return &DB{primary: primary, replica: replica}, nil
}

// repositório com routing explícito
func (r *PedidoRepo) Obter(ctx context.Context, id uuid.UUID) (*Pedido, error) {
    return r.scan(ctx, r.db.replica, "SELECT ... WHERE id = $1", id)
}

func (r *PedidoRepo) ObterAposEscrita(ctx context.Context, id uuid.UUID) (*Pedido, error) {
    // força primary para read-your-writes
    return r.scan(ctx, r.db.primary, "SELECT ... WHERE id = $1", id)
}

func (r *PedidoRepo) Criar(ctx context.Context, p *Pedido) error {
    _, err := r.db.primary.Exec(ctx, "INSERT INTO pedidos ...", ...)
    return err
}

// alternativa Postgres nativa: target_session_attrs
// "postgres://user@primary.db,replica.db/app?target_session_attrs=any"
// libpq retorna primary se ?target_session_attrs=read-write,
// qualquer um se =any, replica se =read-only

Em Go, padrão é explicitness — duas pools, repositório decide qual usar. Algumas bibliotecas ( github.com/jackc/pgx/v5) suportam multi-host com target_session_attrs. A explicitness vence em sistemas com regras complexas de routing.

Anti-padrões frequentes

Read from replica imediatamente após write. Aplicação faz POST de update, depois GET para mostrar resultado. GET vai para réplica desatualizada. Defesa: rotear leituras do mesmo usuário para primário por janela de N segundos após write.

Asumir consistência. Aplicação assume que dado escrito está disponível em qualquer réplica. Funciona em desenvolvimento (sem lag); quebra em produção. Defesa: testes que simulam lag (sleep entre write e read em test); arquitetar assumindo lag.

Long transaction no primário, replica atrasa indefinidamente. Transação grande (relatório com snapshot, batch update) trava réplica. Defesa: statement_timeout no primário; jobs analíticos em replica analítica dedicada; breaking transactions em chunks.

Failover sem testar. Sistema tem replica para HA mas time nunca testou failover. Quando primary cai em produção, descobre que configuração tem bug (DNS errado, app não reconectando, replica stale demais). Defesa: chaos engineering — derrubar primary em staging regularmente.

Promover replica com lag alto. Failover automático escolhe replica que estava atrás; writes recentes são perdidos. Defesa: sync replication para pelo menos uma replica; promotion policy considera lag.

armadilha em produção

"Snapshot inconsistente" entre múltiplas réplicas. Aplicação faz duas queries em paralelo: uma para replica A, outra para replica B. Se as duas estão em pontos diferentes do stream de replicação, o usuário vê combinações inconsistentes (ex.: pedido aparece como criado em uma query, não na outra). Sintoma: bug intermitente difícil de reproduzir ("às vezes não vejo o pedido novo nas listagens"). Defesa: queries que precisam ver "snapshot consistente" devem usar a mesma replica (ou o primary), não distribuir entre réplicas.

heurística do sênior

Para cada padrão de leitura na aplicação, pergunte: "tolera quanto lag?". Read-your-writes: zero tolerância → primário (após janela). Listagens gerais: segundos OK → réplica. Relatórios e analytics: minutos OK → replica analítica. Articule e documente o roteamento por endpoint; revise quando lag em produção mudar. Times que deixam roteamento implícito acumulam bugs sutis; times que articulam evitam.

Por que importa para a sua carreira

Replicação para escala de leitura é configuração cotidiana em sistemas de aplicação. Em entrevistas de design, "como você escalaria leitura sem sharding?" é convite para articular replication — a resposta forte cita primary/replica, replica lag, read-your-writes, replica selection. Em revisão de proposta, identificar caminho que viola read-your-writes é serviço ao time. Em pos-mortem de "usuário viu dado obsoleto após salvar", diagnosticar como replication lag e propor mitigation é trabalho de senior. Em discussão de capacity, articular "scale leitura via replica até doer; depois sharding" é vocabulário maduro.

Como praticar

  1. Setup local com replica. Docker compose com Postgres primary + 1 replica configurada. Force lag artificial (delay em rede, ou SQL pesado no primary). Faça write/read e observe quando o read vê o write. Testar mitigations (sync replication, força primary). Esse exercício torna concreta a natureza assíncrona.
  2. Read-your-writes em projeto seu. Em projeto seu, identifique fluxo onde usuário salva e imediatamente vê dado salvo. Verifique se está rotando ao primary ou pode pegar replica desatualizada. Implemente mitigation apropriada (cookie com timestamp, ou sempre primary nesse fluxo). Documente em ADR.
  3. Failover test. Em ambiente staging, configure primary + replica + failover automático. Derrube o primary. Meça: tempo de detecção, tempo de promoção, tempo de reconfiguração. Verifique que aplicação reconecta sem perder requests. Esse teste, raramente feito, revela bugs antes do incidente.

Referências para aprofundar

  1. livro Designing Data-Intensive Applications — Martin Kleppmann (O'Reilly, 2017). Cap. 5 (Replication) é o tratamento canônico de leader-based, multi-leader, leaderless replication. Cobre lag, read-your-writes, eventual consistency.
  2. livro PostgreSQL High Availability Cookbook — Shaun Thomas (Packt, 2017). Receitas práticas para Postgres HA — replication, failover, monitoring. Datado em algumas ferramentas mas conceitualmente sólido.
  3. livro High Performance MySQL (4ª ed.) — Silvia Botros, Jeremy Tinley (O'Reilly, 2021). Cap. 9 (Replication) cobre MySQL replication moderna. Cap. 12 cobre HA e failover.
  4. artigo The DynamoDB Paper — DeCandia et al., Amazon (SOSP, 2007). research.amazon.com — Conexão com leaderless replication e quórum. Indispensável para sistemas distribuídos.
  5. artigo Read-After-Write Consistency at Stripe — Andrew Bonventre (stripe blog, 2018). stripe.com/blog — Caso real de como Stripe garante read-your-writes em sistema com replicação.
  6. docs PostgreSQL Streaming Replication. postgresql.org/docs/current/warm-standby.html — Documentação oficial. Cobre setup, monitoring, failover.
  7. docs PostgreSQL Synchronous Replication. postgresql.org/docs/current/runtime-config-wal.html#GUC-SYNCHRONOUS-COMMIT — Documentação de sync mode. Custos e ganhos articulados.
  8. docs AWS RDS Multi-AZ. docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html — Documentação de multi-AZ replication na AWS, com failover gerenciado.
  9. docs npgsql multi-host connection. npgsql.org/doc/connection-string-parameters.html — Documentação de Target Session Attributes em .NET para Postgres.
  10. docs Patroni. github.com/zalando/patroni — Ferramenta canônica de HA Postgres com leader election via etcd/Consul. Manual sobre split brain e failover.
  11. artigo How Discord Migrated Their Database — Nick Park (discord blog). discord.com/blog — Caso prático de migration entre estratégias de replication/sharding em escala.
  12. vídeo Database Replication — Kent Graziano (vários workshops). YouTube. Apresentações pedagógicas sobre replication patterns. Bom para fixar visualmente.