Estratégias de Sharding para o Processamento de 10 Bilhões de Pedidos em um Sistema de E-commerce

Desafio em Foco

Considere uma plataforma de e-commerce cuja tabela de pedidos no MySQL atingiu escala problemática. Consultas simples e operações de contagem global tornam-se proibitivamente lentas.

-- Consulta simples levando vários segundos
SELECT * FROM pedidos WHERE usuario_id=10086 LIMIT 10;

-- Contagem global extremamente lenta
SELECT COUNT(*) FROM pedidos;

As causas principais incluem índices B+Tree excessivamente profundos, resultando em alto I/O de disco, tabelas monolíticas com backups demorados, e atraso significativo na replicação devido à alta carga de escrita.

Ponto Crítico: O planejamento para sharding deve ser iniciado quando o volume de uma única tabela ultrapassa a marca dos 50 milhões de linhas.

Diante da necessidade de gerenciar 10 bilhões de registros de pedidos, qual abordagem adotar? A solução envolve sharding horizontal e vertical.

1. Estratégias Fundamentais de Sharding

1.1. Separação Vertical: Reduzindo o Escopo Inicial

A primeira etapa é separar colunas raramente acessadas ou de uso secundário (como campos JSON de metadados extensos) para tabelas auxiliares. Isso resulta em:

  • Redução significativa no tamanho das tabelas principais.
  • Melhoria na eficiência do cache, pois as colunas consultadas com frequência ficam concentradas.

1.2. Sharding Horizontal: A Solução de Escala

A escolha da sharding key (chave de fragmentação) é decisiva. Três princípios guiam a seleção:

  1. Dispersão: Evitar pontos de concentração de carga (e.g., usuario_id é preferível a status_pedido).
  2. Correlação com a Consulta: A chave deve estar presente em pelo menos 80% das consultas críticas.
  3. Imutabilidade: Usar um valor que não mude com o tempo (evitar campos como número de telefone).

Existem múltiplas estratégias de roteamento:

Tipo Cenário Ideal Complexidade para Expansão Exemplo
Por Faixa Consultas com filtro temporal Baixa Particionar por mês de criação (data_criacao)
Hash por Módulo Distribuição uniforme Alta usuario_id % 128
Hash Consistente Expansão dinâmica Média Uso de algoritmos como Ketama
Genética (Gene Sharding) Evitar consultas cross-shard Alta Extrair bits do usuario_id para formar o gene do shard

2. Implementação do Sharding Genético

Para um sistema de pedidos, as consultas de alta frequência tipicamente são por usuario_id, comerciante_id e numero_pedido. O sharding genético visa otimizar esses casos.

A ideia é incorporar bits de identificação (o "gene") dentro do próprio identificador único do pedido (e.g., um ID baseado em Snowflake). Isso permite rotear consultas diretamente.

Gerador de ID Modificado:

public class GeradorIdPedido {
    private static final int BITS_GENE = 10; // Utilizar 10 bits para o gene do shard

    public static long gerarId(long usuarioId) {
        long timestamp = System.currentTimeMillis() - 1288834974657L;
        // Extrair os últimos 10 bits do ID do usuário como gene
        long gene = usuarioId & ((1L << BITS_GENE) - 1);
        long sequencia = obterSequencia(); // Lógica para sequência única

        // Montagem do ID: timestamp (41 bits) | gene (10 bits) | sequência (13 bits)
        return (timestamp << 23)
             | (gene << 13)
             | sequencia;
    }

    public static int extrairGene(long pedidoId) {
        // Isolar os 10 bits do gene do ID do pedido
        return (int) ((pedidoId >> 13) & ((1L << BITS_GENE) - 1));
    }
}

Motor de Roteamento:

public class RoteadorShard {
    private static final int NUM_BANCOS = 8;
    private static final int TABELAS_POR_BANCO = 16;

    public static String rotear(long pedidoId) {
        int gene = GeradorIdPedido.extrairGene(pedidoId);
        // O gene determina tanto o banco quanto a tabela
        int idxBanco = gene % NUM_BANCOS;
        int idxTabela = (gene / NUM_BANCOS) % TABELAS_POR_BANCO;
        return "db_pedidos_" + idxBanco + ".t_pedidos_" + idxTabela;
    }
}

Vantagem Chave: Pedidos do mesmo usuário são sempre direcionados para o mesmo shard físico, e a localização pode ser deduzida diretamente do ID do pedido.

3. Lidando com Consultas Complexas

3.1. Índices Secundários Assíncronos

Para consultas por campos não inclusos na chave de sharding (e.g., buscar por comerciante_id), mantém-se um índice em uma tecnologia especializada como o Elasticsearch.

// Mapeamento de índice no Elasticsearch
{
  "mappings": {
    "properties": {
      "numero_pedido": { "type": "keyword" },
      "gene_shard": { "type": "integer" }, // Armazena o gene para facilitar a busca no banco de origem
      "data_criacao": { "type": "date" },
      "comerciante_id": { "type": "long" }
    }
  }
}

As consultas complexas primeiro obtêm os IDs e genes dos pedidos do Elasticsearch e, em seguida, fazem fetch direto no banco de dados relacional usando o gene para acessar o shard correto.

3.2. Índices Globais (GSI)

Algumas plataformas de middleware, como o ShardingSphere, suportam índices globais secundários, que são mantidos automaticamente durante as operações de escrita.

-- Criação de um índice global em um cenário com ShardingSphere
CREATE SHARDING GLOBAL INDEX idx_comerciante ON pedidos(comerciante_id)
    USING HASH(comerciante_id) BUCKETS 128;

4. Processo de Migração de Dados

A migração de um sistema legado para uma arquitetura sharded é crítica e segue um processo cuidadoso:

  1. Escrita Dupla: Configurar a aplicação para escrever simultaneamente no sistema antigo e no novo. Mecanismos de compensação devem reverter falhas no novo sistema.
  2. Carga Inicial: Migrar dados históricos em lotes, usando SELECT com WHERE id > último_id_migrado para paginação eficiente.
  3. Reconciliação Incremental: Rodar jobs contínuos que comparam dados entre os dois sistemas e corrigem divergências.
  4. Rollout Gradual (Grayscale): Direcionar o tráfego de leitura para o novo sistema progressivamente, começando por 1% dos usuários, monitorando e aumentando gradualmente até 100%.

5. Armadilhas Comuns e Soluções

5.1. Hotspots (Concentração de Dados)

Em períodos de pico (e.g., Black Friday), um comerciante popular pode gerar todos os pedidos em um único shard. Uma solução é utilizar uma chave composta: (comerciante_id + usuario_id) % TOTAL_SHARDS.

5.2. Transações Distribuídas

Operações como a criação de um pedido (com atualização de estoque e pontos) exigem transações distribuídas. Uma abordagem pragmática é o uso de mensageria para consistência eventual:

@Transactional
public void criarPedido(Pedido pedido) {
    pedidoRepositorio.salvar(pedido); // Escrita no shard atual
    filaMensagens.enviar("evento.criacao.pedido", pedido); // Disparo assíncrono
}

// Consumidor do evento
public void processarCriacaoPedido(EventoPedido evento) {
    servicoEstoque.reservar(evento.getSkuId(), evento.getQuantidade());
    servicoFidelidade.creditarPontos(evento.getUsuarioId(), evento.getPontos());
}

5.3. Paginação em Consultas Cross-Shard

Consultar OFFSET 1000 em múltiplos shards é ineficiente. Alternativas incluem: filtrar por faixas de data (restringindo os shards consultados), usar um cursor baseado em timestamp ou ID, ou aceitar resultados aproximados para sumariazções globais.

6. Resultado Final da Arquitetura

Após a implementação completa das estratégias de sharding, as métricas de performance melhoram drasticamente:

Cenário Antes do Sharding Depois do Sharding
Consulta de pedidos por usuário Milhares de milissegundos Dezenas de milissegundos
Exportação de pedidos por comerciante Falha por timeout Concluída em segundos
Contagem global (com cache) Indisponível Poucos segundos (aproximada)

Princípios Finais:

  1. A escolha da sharding key é a decisão arquitetural mais impactante.
  2. O dimensionamento inicial deve considerar pelo menos 2 anos de crescimento previsto.
  3. Prefira junções simples em tabelas pequenas do que JOINs distribuídos complexos.
  4. Implemente monitoramento constante da taxa de distribuição dos shards para detectar desequilíbrios.

Tags: Sharding MySQL distributed-databases elasticsearch java

Publicado em 6-25 20:44