Home Comunidade Eventos YouTube Networks Desafios // produtos Academy News CTI | CVE Contato

04 - Estratégias de Roteamento de Consultas

#consistencia#lag#orm#read-write-splitting#roteamento
video explicativo
Video explicativoEspaco reservado para o video desta anotação — adicione video: https://youtu.be/... no frontmatter da nota.

Estratégias de Roteamento de Consultas

Roteamento de consultas é a disciplina de decidir, para cada operação emitida pela aplicação, qual nó do banco de dados vai atendê-la: o primário, que concentra escritas e leituras consistentes, ou uma réplica, que oferece leitura com defasagem de milissegundos a segundos. Com a topologia primário e réplicas em pé, o ganho de escala só se materializa se o tráfego for dividido corretamente; roteamento mal feito manda consultas pesadas ao primário ou leituras sensíveis a réplicas defasadas, produzindo os dois piores cenários ao mesmo tempo. A decisão pode acontecer na camada de aplicação, com código e ORM escolhendo o pool por consulta, ou na camada de infraestrutura, com proxies e endpoints dedicados executando a divisão de forma transparente. Em produção, o roteamento é onde a replicação encontra a consistência percebida pelo usuário: é aqui que se resolve o problema de escrever um dado e não encontrá-lo na leitura seguinte.

Conceito: quem decide o destino de cada consulta

Existem três pontos possíveis de decisão. O primeiro é o código da aplicação, que mantém dois pools de conexão e escolhe por operação, com regras explícitas no serviço ou no repositório. O segundo é o ORM ou framework, que centraliza a escolha em roteadores declarativos, como routers do Django ou role switching do Rails. O terceiro é a infraestrutura de rede, com proxies como ProxySQL e HAProxy ou endpoints gerenciados, que recebem conexões únicas e distribuem por heurística de instrução, transação ou usuário.

Cada ponto de decisão tem custos próprios de visibilidade e de acoplamento. Na aplicação, a decisão é explícita, testável em unidade e ciente do domínio, mas cada serviço novo reimplementa as regras. No proxy, a decisão é centralizada e transparente, mas cega ao contexto de negócio e adiciona um componente crítico no caminho das conexões. Em produção madura, costuma-se começar na aplicação e migrar partes para proxies quando o número de serviços cresce.

O problema que resolve

Sem roteamento, todo o tráfego cai no primário, e réplicas viram infraestrutura ociosa ou usada apenas por ferramentas externas. O primário satura primeiro porque escrita é inerentemente mais cara: índices secundários, WAL, locks e vacuum acumulam custo a cada transação. Mover para as réplicas a fatia de leitura tolerante a defasagem devolve ao primário a capacidade de processar transações de negócio.

O segundo problema é a consistência percebida. Se a divisão fosse simples, bastaria separar verbos; na prática, um mesmo fluxo de usuário escreve e lê em sequência, e a leitura precisa enxergar a escrita recém-feita. O roteamento define políticas para esse caso, como enviar leituras pós-escrita ao primário ou esperar o dado alcançar a réplica. O terceiro problema é o isolamento de cargas: consultas analíticas e relatórios concorrem com leituras interativas, e o roteamento por perfil, direcionando cada uma a réplicas dedicadas, preserva a latência das duas classes.

Vale registrar que roteamento não substitui design: ele é um mecanismo de execução sobre uma decisão de arquitetura. A decisão de quais leituras toleram defasagem pertence ao desenho do produto e do domínio; o roteamento apenas materializa essa decisão em tempo de execução. Times que tratam o roteamento como detalhe de infraestrutura costumam descobrir tarde que faltou mapear a semântica de consistência de cada fluxo.

Roteamento na camada de aplicação

A forma mais comum em times pequenos e médios é a divisão explícita no código: dois pools nomeados, uma função de decisão e disciplina de usar o pool certo por operação. O exemplo abaixo mostra a estrutura mínima em Python, válida como esqueleto para qualquer linguagem:

# Dois pools nomeados por papel; a decisão fica explícita no repositório
pools = {
    "writer": criar_pool(host="vendas-prod.rds.internal", max_size=40),
    "reader": criar_pool(host="vendas-replica-01.rds.internal", max_size=80),
}

def consultar(sql, decisao):
    # regra central: leitura vai a réplica, exceto quando marcada como crítica
    if decisao.eh_escrita or decisao.exige_consistencia_forte:
        alvo = "writer"
    elif decisao.tag == "relatorio":
        alvo = "replica_analytics"          # réplica dedicada a cargas pesadas
    else:
        alvo = "reader"
    with pools[alvo].conexão() as conn:
        return conn.executar(sql)

A vantagem desse modelo é o domínio total sobre as exceções: o time decide exatamente quais leituras podem ser defasadas. A desvantagem é a dispersão: cada serviço, cada script e cada job precisa seguir as mesmas regras, e a conformidade se degrada com o tempo sem revisão contínua.

Roteamento por proxy e endpoints dedicados

Na infraestrutura, o proxy assume a decisão no lugar da aplicação. O ProxySQL, clássico do MySQL, classifica consultas por regras de regex e roteia SELECTs a hostgroups de réplica e o resto ao primário, com monitoramento de lag embutido. O HAProxy expõe portas distintas por papel, uma para escrita e outra para leitura, delegando a escolha à aplicação por porta escolhida. No mundo gerenciado, o RDS Proxy e os endpoints reader do Aurora e do Multi-AZ DB Cluster oferecem o mesmo serviço com operação zero.

Aspecto Roteamento na aplicação Roteamento por proxy
Visibilidade da decisão Explícita no código, fácil de testar Oculta em regras de configuração
Consciência de domínio Total, entende fluxo de usuário Limitada à sintaxe da instrução
Conformidade entre serviços Depende de disciplina e revisão Centralizada, um lugar para ajustar
Componente no caminho Nenhum adicional Proxy crítico, exige HA própria
Custo de latência Zero, conexão direta Um salto a mais, tipicamente submilissegundo
Caso típico Poucos serviços, times pequenos Muitos serviços, poliglotismo, legado

A regra prática: proxies valem quando o número de clientes supera a capacidade do time de manter regras em cada um; antes disso, a simplicidade da aplicação ganha.

Read-your-own-writes

O requisito mais cobrado pelo usuário final é ler o que acabou de escrever; o nome técnico é read-your-own-writes, e sua quebra gera relatos do tipo "salvei e o sistema perdeu meu dado". A origem é sempre a mesma: a escrita vai ao primário, a leitura seguinte vai a uma réplica que ainda não aplicou a transação. O efeito é intermitente e dependente de carga, o que o torna especialmente difícil de reproduzir em ambiente de teste.

As soluções clássicas formam uma gradação. A mais simples é o roteamento fixo: leituras do mesmo fluxo que acabou de escrever vão ao primário por um período, implementado como sessão sticky com TTL curto, por exemplo cinco segundos. A segunda é a validação de token de consistência, que espera a réplica alcançar a transação escrita antes de responder. A terceira é a evitação estrutural: desenhar os fluxos críticos para ler direto do primário, reservando réplicas para conteúdo que o usuário não acabou de produzir.

Estratégia Precisão da garantia Custo de latência Complexidade de implantação
Sticky session com TTL Aproximada, baseada em janela de tempo Nenhuma extra Baixa, uma marca na sessão
Token de consistência Exata, por transação escrita Pequena espera controlada Média, exige capturar e propagar o token
Tudo no primário Absoluta, sem defasagem Saturação do nó em picos Nenhuma, porém perde a escala de leitura
Redesenho do fluxo Definida por projeto Variável Alta, decisão arquitetural por fluxo

Consistency token

O token de consistência transforma a espera em checagem precisa. A escrita retorna um marcador de posição no log, como o LSN do PostgreSQL ou o GTID do MySQL; leituras subsequentes carregam o marcador mínimo exigido, e a aplicação verifica na réplica se a posição já foi aplicada antes de executar a consulta. Se não foi, ou se espera em loop curto, ou se redireciona ao primário.

-- No primário: captura do LSN no momento da escrita (retornado ao cliente)
SELECT pg_current_wal_lsn() AS token_de_consistencia;

-- Na réplica: verificação se a posição já foi aplicada
SELECT pg_last_wal_replay_lsn() >= '0/1E4B2A0'::pg_lsn AS dado_disponivel;

No MySQL, a função GTID_WAIT oferece o primitivo equivalente: a sessão bloqueia até a réplica aplicar o GTID informado, com timeout configurável. O padrão funciona muito bem dentro de um serviço com acesso aos dois nós, e é a base de bibliotecas de roteamento consciente de consistência. O custo é a latência extra da checagem, geralmente inferior a alguns milissegundos quando o lag é baixo.

Divisão leitura e escrita no ORM

ORMs modernos trazem o roteamento como recurso de plataforma, o que reduz a dispersão de regras entre serviços. O Django usa database routers declarativos, que decidem por modelo e por operação; o Rails expõe connected_to com papel de writing ou reading e suporta sticky reads pós-escrita; o Spring pode marcar transações como somente leitura e direcioná-las a um datasource específico.

# Router do Django: leituras vão à réplica, escritas ao primário
class RoteadorVendas:
    def db_for_read(self, model, **hints):
        return "replica"

    def db_for_write(self, model, **hints):
        return "default"

    def allow_relation(self, obj1, obj2, **hints):
        return True    # objetos das duas bases pertencem ao mesmo domínio

A armadilha do roteamento automático de ORM é a granularidade por operação, cega ao fluxo: o router sabe que SELECT pode ir à réplica, mas não sabe que aquele SELECT segue um INSERT da mesma requisição. Por isso os frameworks maduros combinam o roteamento com mecanismos de sticky session, que fixam a sessão no primário por uma janela após qualquer escrita.

Risco de consultas pesadas nas réplicas

Uma réplica aplica o log do primário em uma thread; consultas pesadas competem com essa aplicação e atrasam tudo que vem depois, aumentando o lag global da réplica. Pior, a queda de desempenho é invisível para quem só olha o primário: o time vê a réplica lenta, remove-a do pool, e o tráfego volta a concentrar onde não deveria. Consultas analíticas e relatórios ad-hoc são os vilões clássicos desse cenário.

As contramedidas são operacionais e de desenho. Réplicas dedicadas a cargas pesadas, com classes maiores e sem participação no pool interativo, isolam o problema; statement_timeout agressivo nas réplicas interativas evita que uma consulta mal escrita domine o nó; e limites de concorrência por ferramenta de BI impedem que um relatório mensal lance vinte varreduras simultâneas. O monitoramento precisa cobrir a taxa de aplicação do log, e não apenas o tempo médio das consultas.

Monitoramento do lag na decisão

Roteamento consciente usa o lag como entrada em tempo real. A implementação mínima expõe a defasagem de cada réplica em um endpoint de saúde, e o roteador consulta esse estado antes de escolher o alvo: réplicas acima do limite são retiradas da rotação, e o tráfego volta ao primário ou às irmãs saudáveis. Proxies como o ProxySQL implementam a mesma lógica nativamente, com hostgroups de backup ativados quando o lag cresce.

  • Definir limiar por perfil: leitura interativa tolera centenas de milissegundos, dashboards toleram segundos, relatórios toleram minutos.
  • Remover da rotação a réplica com lag acima do limiar, em vez de distribuir leituras cegamente entre nós bons e ruins.
  • Alertar sobre tendência, não apenas sobre valor absoluto; lag crescente constante indica réplica subdimensionada antes do incidente.
  • Medir o lag no nó e não na rede: métricas do banco, como replay_lag do PostgreSQL e ReplicaLag do CloudWatch, são mais confiáveis que pings.

Erros comuns e boas práticas

  • Erro: dividir leitura e escrita por convenção verbal sem regra centralizada, obtendo desvios à medida que o código evolui; prática: função de decisão única, testada, usada por todos os repositórios.
  • Erro: enviar todas as leituras às réplicas e descobrir o problema de read-your-own-writes por ticket de suporte; prática: mapear fluxos críticos de escrita seguida de leitura e marcá-los explicitamente.
  • Erro: confiar no roteamento automático do ORM sem mecanismo de sessão pós-escrita; prática: ativar sticky reads ou token de consistência nos fluxos sensíveis.
  • Erro: permitir relatórios ad hoc nas réplicas interativas; prática: réplicas dedicadas por perfil e statement_timeout por classe de usuário.
  • Erro: escolher alvo de consulta sem olhar o lag; prática: rotação consciente de saúde, com ejeção automática de réplicas defasadas.

O roteamento de consultas é, em síntese, a camada onde a topologia de replicação se torna útil de verdade: correto, ele multiplica a capacidade de leitura e protege o caminho transacional; incorreto, ele fabrica bugs de consistência e sobrecarrega o nó errado. Começar simples na aplicação, instrumentar o lag desde o primeiro dia e centralizar as regras à medida que o número de serviços cresce é o caminho comprovado.