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