Sharding de banco de dados
1. Fundamentos do Sharding
1.1 Definição e motivação
Sharding é a técnica de particionar horizontalmente um banco de dados em múltiplos fragmentos independentes chamados shards. Cada shard contém um subconjunto dos dados e opera como um banco de dados separado. A motivação principal é superar as limitações de escalabilidade vertical — quando um único servidor não consegue mais lidar com o volume de dados ou o throughput de operações.
Os gargalos típicos que levam ao sharding incluem:
- Crescimento de dados acima de terabytes: Um único servidor pode gerenciar até dezenas de terabytes, mas além disso o custo de hardware e o tempo de backup tornam-se proibitivos.
- Throughput de escrita elevado: Operações de escrita concorrentes em um único nó criam contenção de locks e filas de transação.
- Latência de leitura: Consultas em tabelas enormes sem índices eficientes degradam o desempenho.
1.2 Diferença entre sharding, replicação e particionamento vertical
| Técnica | Descrição | Caso de uso |
|---|---|---|
| Sharding | Divide os dados horizontalmente entre servidores distintos | Escalabilidade de escrita e armazenamento |
| Replicação | Copia os mesmos dados para múltiplos servidores (leader-follower ou multi-leader) | Alta disponibilidade, leitura escalável |
| Particionamento vertical | Divide tabelas em colunas separadas em servidores diferentes | Otimização de acesso a colunas específicas |
Sharding resolve problemas que replicação não consegue: quando o volume total de dados excede a capacidade de um único nó, replicar não ajuda — você apenas duplica o problema.
1.3 Quando o sharding é necessário
Indicadores práticos:
- Volume de dados > 1 TB em um único banco relacional
- Mais de 10.000 escritas por segundo sustentadas
- Necessidade de isolamento de carga entre diferentes domínios de dados (ex.: clientes por região geográfica)
- SLA de latência abaixo de 10ms que não pode ser atingido com replicação simples
2. Estratégias de Distribuição de Dados
2.1 Sharding por chave de hash
A chave de shard é submetida a uma função hash que determina o shard destino. Exemplo com hash modular:
def obter_shard(user_id):
hash_value = hash(user_id) % NUM_SHARDS
return f"shard_{hash_value}"
Vantagens: Distribuição uniforme dos dados, previsível.
Desvantagens: Rebalanceamento exige re-hashing de todos os dados, consultas por intervalo são ineficientes.
Hash ring (consistent hashing) minimiza o rebalanceamento: ao adicionar/remover shards, apenas uma fração dos dados precisa ser migrada.
2.2 Sharding por intervalo (range-based)
Os dados são divididos por faixas de valores da chave de shard:
shard_0: user_id 1 a 1000000
shard_1: user_id 1000001 a 2000000
shard_2: user_id 2000001 a 3000000
Vantagens: Consultas por intervalo são eficientes (ex.: SELECT * FROM usuarios WHERE user_id BETWEEN 500 AND 1500).
Risco: Hotspots — se a distribuição dos dados for desigual (ex.: usuários ativos concentrados em um intervalo), um shard fica sobrecarregado.
2.3 Sharding por diretório (lookup table)
Um serviço de metadados mapeia cada chave para o shard correspondente:
Tabela de roteamento:
user_id -> shard
1 -> shard_0
2 -> shard_2
3 -> shard_1
Vantagens: Flexibilidade total para mover dados entre shards sem afetar a aplicação.
Desvantagens: Dependência de um ponto único de falha (o serviço de lookup), latência adicional em cada consulta.
3. Roteamento de Consultas no Sharding
3.1 Roteamento no lado do cliente
A aplicação conhece a topologia dos shards e decide qual banco consultar:
# Exemplo com biblioteca Vitess
from vitess import Vtgate
conn = Vtgate.connect()
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = 123", shard_key=123)
Prós: Baixa latência (sem proxy intermediário).
Contras: Lógica de roteamento precisa ser mantida em cada serviço cliente.
3.2 Roteamento com proxy intermediário
Um proxy (ex.: ProxySQL, Vitess gate) intercepta as queries e as roteia:
Aplicação -> ProxySQL -> shard_0
-> shard_1
-> shard_2
Prós: Transparência para a aplicação, centralização da lógica de roteamento.
Contras: Latência adicional (1-5ms), proxy se torna ponto único de falha.
3.3 Consultas cross-shard
Consultas que envolvem dados de múltiplos shards exigem scatter-gather:
# Fan-out: enviar a mesma query para todos os shards
for shard in all_shards:
results.append(shard.execute("SELECT * FROM orders WHERE status = 'pending'"))
# Gather: combinar resultados
final_result = merge(results)
Joins cross-shard são caros e devem ser evitados. Estratégias comuns:
- Desnormalização para evitar joins
- Agregação em duas fases (parcial em cada shard, final no coordenador)
4. Desafios de Consistência e Transações
4.1 Transações distribuídas
Two-phase commit (2PC): Garante atomicidade entre shards, mas é lento e bloqueante:
Fase 1: Coordenador pergunta a cada shard "pode commit?"
Fase 2: Se todos responderem "sim", coordenador envia "commit" para todos
Saga pattern: Alternativa não bloqueante, com ações compensatórias em caso de falha:
Passo 1: Debitar conta A (shard_0)
Passo 2: Creditar conta B (shard_1)
Se Passo 2 falhar: executar ação compensatória (reverter débito em shard_0)
4.2 Garantia de consistência eventual
Em cenários onde consistência imediata não é crítica (ex.: contagem de likes), a consistência eventual é aceitável. O desafio é gerenciar conflitos de atualização concorrente — técnicas como last-write-wins (LWW) ou CRDTs (Conflict-free Replicated Data Types) são usadas.
4.3 Chaves globais únicas
Em ambiente sharded, gerar IDs únicos globalmente sem um banco centralizado é complexo:
# Snowflake ID (Twitter): 64 bits
# 1 bit de sinal + 41 bits timestamp + 10 bits worker_id + 12 bits sequência
# Exemplo: 1234567890123456789
# UUID v4: 128 bits, aleatório, sem coordenação
# Exemplo: 550e8400-e29b-41d4-a716-446655440000
Snowflake IDs são preferíveis para índices ordenados; UUIDs são mais simples mas consomem mais espaço.
5. Rebalanceamento e Manutenção de Shards
5.1 Adição e remoção de shards
Migração offline: Para o sistema, move dados, reinicia. Simples, mas causa downtime.
Migração online: Usa consistent hashing para minimizar dados movidos:
# Ao adicionar shard_4 no hash ring:
# Apenas dados entre shard_3 e shard_4 no anel são migrados
# Aproximadamente 1/N dos dados (N = número de shards)
5.2 Hotspots e redistribuição
Hotspots ocorrem quando um shard recebe carga desproporcional. Estratégias:
- Re-sharding: Alterar a função hash ou o número de shards
- Virtual shards: Dividir cada shard físico em múltiplos shards virtuais, redistribuindo-os entre nós físicos
- Cache local: Reduzir leituras repetitivas no shard quente
5.3 Backup e recuperação em ambiente sharded
Backup consistente entre shards requer coordenação:
1. Pausar escritas em todos os shards (janela de inconsistência)
2. Fazer snapshot de cada shard
3. Registrar o timestamp global de consistência
4. Retomar escritas
Restore point-in-time exige que todos os shards sejam restaurados para o mesmo instante lógico.
6. Observabilidade e Monitoramento em Shards
6.1 Métricas por shard
Coletar para cada shard:
- Latência média e P99 de queries
- Throughput (queries por segundo)
- Tamanho dos dados (GB)
- Taxa de erros (timeouts, conexões recusadas)
6.2 Logs e tracing distribuído
Correlacionar requisições que atravessam múltiplos shards exige um ID único de trace:
X-Request-ID: 12345
[gateway] -> [shard_0] -> [shard_1]
Todos os logs carregam o mesmo X-Request-ID
Ferramentas como Jaeger ou Zipkin permitem visualizar o fluxo completo.
6.3 Alertas e auto-scaling
Gatilhos típicos:
- Latência P99 > 100ms por mais de 5 minutos
- Tamanho do shard > 80% da capacidade do nó
- Taxa de erros > 1%
Auto-scaling pode adicionar shards automaticamente quando a utilização de CPU/memória ultrapassa limites definidos.
7. Alternativas e Considerações Finais
7.1 Quando evitar sharding
Sharding adiciona complexidade significativa:
- Custos operacionais de gerenciar múltiplos bancos
- Dificuldade em manter consistência transacional
- Complexidade de backup e restore
Alternativas antes de shardear:
- Otimizar índices e consultas
- Usar caching (Redis, Memcached)
- Migrar para banco NoSQL com sharding nativo (MongoDB, Cassandra)
7.2 Sharding em ambientes cloud
Serviços gerenciados abstraem parte da complexidade:
| Serviço | Abordagem | Diferencial |
|---|---|---|
| Amazon Aurora | Sharding automático com leitura/escrita | Compatibilidade MySQL/PostgreSQL |
| CockroachDB | Sharding automático com consistência forte | SQL distribuído, auto-rebalanceamento |
| Google Spanner | Sharding global com relógio atômico | Consistência forte em escala global |
7.3 Boas práticas de design
- Escolha a chave de shard com cuidado: Deve distribuir uniformemente os dados e ser usada na maioria das consultas
- Planeje o crescimento: Comece com mais shards do que o necessário (potências de 2 facilitam rebalanceamento)
- Evite joins cross-shard: Desnormalize dados ou use agregações em duas fases
- Teste o rebalanceamento: Simule adição/remoção de shards em staging antes de produção
- Monitore hotspots continuamente: Use dashboards com métricas por shard
Sharding é uma ferramenta poderosa para escalabilidade horizontal, mas deve ser aplicada com planejamento cuidadoso. Quando bem implementado, permite que sistemas cresçam para petabytes de dados e milhões de operações por segundo.
Referências
- Vitess Documentation - Sharding — Documentação oficial do Vitess, explicando sharding com MySQL, roteamento e rebalanceamento
- MongoDB Sharding Architecture — Guia completo sobre sharding no MongoDB, incluindo estratégias de chunking e balanceamento
- CockroachDB - How Sharding Works — Explicação do sharding por ranges no CockroachDB com consistência forte
- AWS Database Sharding Best Practices — Artigo técnico da AWS sobre padrões de sharding e considerações de design
- Google Spanner - TrueTime and Global Consistency — Documentação do Spanner sobre consistência global e sharding automático