04 - Estratégias de Roteamento de Consultas
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.