Read replicas: estratégias de consistência eventual
1. Fundamentos de Read Replicas e Consistência Eventual
Read replicas são cópias assíncronas de um banco de dados primário, criadas para distribuir a carga de leitura e melhorar a escalabilidade horizontal. Na arquitetura típica, o primário processa todas as escritas e replica os dados para uma ou mais réplicas de forma assíncrona. Isso significa que as réplicas podem estar ligeiramente desatualizadas em relação ao primário — daí o termo "consistência eventual".
A consistência eventual garante que, se nenhuma nova escrita ocorrer, todas as réplicas convergirão para o mesmo estado após um período. Em contraste, a consistência forte exige que qualquer leitura retorne o valor mais recente, o que geralmente implica em maior latência e menor disponibilidade (Teorema CAP).
Casos de uso típicos incluem:
- Dashboards analíticos que toleram alguns segundos de atraso
- Feeds de notícias e redes sociais
- Catálogos de produtos em e-commerce
- Relatórios operacionais noturnos
2. Estratégias de Roteamento de Leituras
Roteamento baseado em carga (Round-Robin)
Distribui as consultas igualmente entre todas as réplicas disponíveis:
# Configuração de balanceamento round-robin para read replicas
read_replicas:
- host: replica1.internal
port: 3306
- host: replica2.internal
port: 3306
- host: replica3.internal
port: 3306
# Algoritmo de seleção (pseudo-código)
current_replica = (current_replica + 1) % total_replicas
query(current_replica)
Roteamento com afinidade de sessão (Sticky Sessions)
Garante que o mesmo usuário sempre consulte a mesma réplica durante uma sessão:
# Middleware de sticky session para read replicas
def get_session_replica(session_id):
replica_index = hash(session_id) % len(replicas)
return replicas[replica_index]
# Exemplo de uso
replica = get_session_replica("user_session_12345")
result = replica.query("SELECT * FROM products WHERE id = 42")
Roteamento geográfico
Direciona leituras para a réplica mais próxima geograficamente, reduzindo latência:
# Tabela de roteamento geográfico
routing_table:
us-east-1: replica-us-east.internal
us-west-2: replica-us-west.internal
eu-west-1: replica-eu-west.internal
# Função de seleção geográfica
def get_geographic_replica(user_ip):
region = geoip_lookup(user_ip)
return routing_table[region]
3. Garantias de Consistência Eventual na Prática
Monitoramento da janela de replicação
A janela de replicação é o intervalo entre uma escrita no primário e sua propagação para a réplica. É crucial medir o lag:
# MySQL: verificar lag de replicação
SHOW SLAVE STATUS\G
# Saída relevante:
Seconds_Behind_Master: 2
Relay_Log_Space: 1048576
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Leitura após escrita (Read-After-Write)
Para operações críticas, redirecione a leitura para o primário imediatamente após uma escrita:
# Estratégia de read-after-write
def save_and_read(user_id, data):
# Escreve no primário
primary.execute("INSERT INTO users (id, data) VALUES (?, ?)", user_id, data)
# Marca sessão para leitura no primário
session.set("force_primary", True, ttl=5) # 5 segundos
# Próximas leituras deste usuário vão para o primário
if session.get("force_primary"):
return primary.query("SELECT * FROM users WHERE id = ?", user_id)
else:
return replica.query("SELECT * FROM users WHERE id = ?", user_id)
Timeouts e fallbacks
Quando a réplica está muito atrasada, consulte o primário como fallback:
# Fallback para primário quando réplica está atrasada
def read_with_fallback(replica, primary, max_lag=5):
lag = replica.get_replication_lag() # em segundos
if lag > max_lag:
# Réplica muito atrasada, usar primário
return primary.query("SELECT * FROM products")
else:
return replica.query("SELECT * FROM products")
4. Tratamento de Inconsistências e Stale Reads
Versionamento de registros
Use timestamps ou números de versão para detectar dados obsoletos:
# Tabela com versionamento
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(255),
price DECIMAL(10,2),
version INT DEFAULT 1,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
# Verificação de versão durante leitura
def read_product(product_id):
replica_data = replica.query(
"SELECT id, name, price, version FROM products WHERE id = ?",
product_id
)
# Se versão parecer antiga, validar no primário
if replica_data.version < expected_version:
primary_data = primary.query(
"SELECT id, name, price, version FROM products WHERE id = ?",
product_id
)
return primary_data
return replica_data
Estratégias de reconciliação
Após ler da réplica, valide no primário para operações críticas:
# Validação pós-leitura
def validate_order_consistency(order_id):
# Leitura inicial da réplica (rápida)
order = replica.query("SELECT * FROM orders WHERE id = ?", order_id)
# Validação no primário (garantia de consistência)
primary_order = primary.query("SELECT * FROM orders WHERE id = ?", order_id)
if order.status != primary_order.status:
# Dados inconsistentes, usar dados do primário
return primary_order
return order
Definição de SLAs de consistência
Estabeleça limites claros para o lag aceitável:
# SLA de consistência
consistency_sla:
max_lag_seconds: 5
max_lag_percentage: 0.1 # 10% das consultas podem estar desatualizadas
critical_operations: ["payment", "inventory_reservation"]
non_critical_operations: ["product_search", "recommendations"]
5. Monitoramento e Alertas de Lag de Replicação
Métricas essenciais
# Prometheus: métricas de replicação
# HELP mysql_replication_lag_seconds Replication lag in seconds
# TYPE mysql_replication_lag_seconds gauge
mysql_replication_lag_seconds{instance="replica1"} 2.5
# HELP mysql_seconds_behind_master Seconds behind master
# TYPE mysql_seconds_behind_master gauge
mysql_seconds_behind_master{instance="replica1"} 3.0
# HELP mysql_replica_io_running Replica IO thread status
# TYPE mysql_replica_io_running gauge
mysql_replica_io_running{instance="replica1"} 1
Dashboard Grafana
# Exemplo de consulta para dashboard Grafana
avg(
mysql_replication_lag_seconds{instance=~"replica.*"}
) by (instance)
# Alerta quando lag > 10 segundos por mais de 2 minutos
- alert: HighReplicationLag
expr: mysql_replication_lag_seconds > 10
for: 2m
labels:
severity: critical
annotations:
summary: "Replication lag high on {{ $labels.instance }}"
Alertas proativos
# Configuração de alertas no Prometheus
groups:
- name: replication_alerts
rules:
- alert: ReplicationLagCritical
expr: mysql_replication_lag_seconds > 30
for: 1m
labels:
severity: critical
annotations:
description: "Replication lag on {{ $labels.instance }} is {{ $value }}s"
- alert: ReplicaDown
expr: mysql_replica_io_running == 0
for: 30s
labels:
severity: critical
annotations:
summary: "Replica IO thread stopped on {{ $labels.instance }}"
6. Padrões de Design para Alta Disponibilidade com Réplicas
Failover automático
# Script de failover simplificado
def auto_failover(primary, replicas):
if not primary.is_alive():
# Promover réplica mais atualizada
best_replica = max(replicas, key=lambda r: r.get_replication_lag())
best_replica.promote_to_primary()
# Redirecionar escritas
update_connection_pool(best_replica)
# Reconfigurar demais réplicas
for replica in replicas:
if replica != best_replica:
replica.change_master(best_replica)
Multi-AZ e multi-região
# Configuração multi-região
regions:
us-east-1:
primary: db-primary-us-east.internal
replicas:
- db-replica-us-east-1.internal
- db-replica-us-east-2.internal
eu-west-1:
primary: db-primary-eu-west.internal
replicas:
- db-replica-eu-west-1.internal
- db-replica-eu-west-2.internal
# Roteamento com fallback entre regiões
def get_replica(user_region):
if user_region in regions:
return random.choice(regions[user_region]["replicas"])
else:
# Fallback para região mais próxima
return get_nearest_region_replica(user_region)
Cache de leitura local
# Combinação de réplicas com Redis
def get_product_with_cache(product_id):
# Tentar cache local primeiro
cached = redis.get(f"product:{product_id}")
if cached:
return cached
# Cache miss: consultar réplica
product = replica.query("SELECT * FROM products WHERE id = ?", product_id)
# Armazenar em cache com TTL curto
redis.setex(f"product:{product_id}", 60, product) # 60 segundos
return product
7. Casos Práticos e Anti-padrões
Exemplo real: Sistema de e-commerce
# Arquitetura de catálogo de produtos
ecommerce_system:
primary_db:
role: "escritas (pedidos, inventário)"
replicas:
- role: "catálogo de produtos (leituras pesadas)"
- role: "histórico de preços (relatórios)"
- role: "recomendações (consultas complexas)"
# Fluxo de consulta de produto
def search_products(query):
# 95% das consultas vão para réplicas
if random.random() < 0.95:
return replica.query("SELECT * FROM products WHERE name LIKE ?", f"%{query}%")
else:
# 5% para primário (garantir dados recentes)
return primary.query("SELECT * FROM products WHERE name LIKE ?", f"%{query}%")
Anti-padrão: Dependência de consistência forte em réplicas
# ERRADO: Esperar consistência forte de réplicas
def update_inventory(product_id, quantity):
# Escrita no primário
primary.execute("UPDATE products SET stock = stock - ? WHERE id = ?", quantity, product_id)
# Leitura imediata na réplica (pode retornar stock desatualizado)
stock = replica.query("SELECT stock FROM products WHERE id = ?", product_id) # INCORRETO
if stock < 0:
raise Exception("Estoque negativo") # Falso positivo frequente
# CORRETO: Usar primário para leituras críticas
def update_inventory(product_id, quantity):
primary.execute("UPDATE products SET stock = stock - ? WHERE id = ?", quantity, product_id)
# Leitura no primário para garantir consistência
stock = primary.query("SELECT stock FROM products WHERE id = ?", product_id) # CORRETO
if stock < 0:
raise Exception("Estoque negativo")
Checklist de decisão
Checklist: Read Replicas vs Cache vs Sharding
| Critério | Read Replicas | Cache (Redis) | Sharding |
|-----------------------------|---------------|---------------|----------|
| Volume de leituras | Alto | Muito alto | Alto |
| Tolerância a dados antigos | Alta | Alta | Baixa |
| Complexidade operacional | Média | Baixa | Alta |
| Custo | Médio | Baixo | Alto |
| Consistência | Eventual | Eventual | Forte |
| Escalabilidade | Leitura | Leitura | Escrita |
Use read replicas quando:
- Leituras >> escritas (pelo menos 10:1)
- Tolerância a alguns segundos de lag
- Necessidade de dados relacionais completos
Use cache quando:
- Dados acessados com frequência e raramente modificados
- Baixa tolerância a latência (< 10ms)
- Dados podem ser regenerados facilmente
Use sharding quando:
- Volume de escritas excede capacidade de um nó
- Necessidade de consistência forte
- Dados podem ser particionados naturalmente
Referências
-
AWS: Read Replicas - Amazon RDS — Documentação oficial sobre configuração e gerenciamento de read replicas no Amazon RDS, incluindo estratégias de failover e monitoramento de lag.
-
MySQL: Replication Administration — Guia completo de administração de replicação MySQL, incluindo comandos SHOW SLAVE STATUS e configuração de replicação assíncrona.
-
PostgreSQL: Streaming Replication — Documentação oficial sobre replicação por streaming no PostgreSQL, com exemplos de configuração de réplicas de leitura.
-
Redis: Replication and Persistence — Guia de replicação Redis, incluindo estratégias de cache combinadas com réplicas de banco de dados.
-
Google Cloud: Configuring Read Replicas — Documentação do Cloud SQL sobre replicação de leitura, com exemplos de roteamento geográfico e failover automático.