Sharding: particionamento horizontal de dados

1. Fundamentos do Sharding

Sharding é uma técnica de particionamento horizontal de dados que distribui registros de uma mesma tabela entre múltiplos bancos de dados independentes, chamados shards. Cada shard contém um subconjunto dos dados, mas todos compartilham o mesmo esquema de tabela. O objetivo principal é permitir que o sistema escale horizontalmente: ao invés de aumentar os recursos de um único servidor (escalonamento vertical), adicionamos mais servidores, cada um responsável por uma fração dos dados.

A diferença crucial entre sharding e particionamento vertical é que no particionamento vertical dividimos colunas de uma tabela entre diferentes bancos, enquanto no sharding dividimos linhas. O sharding torna-se necessário quando uma única instância de banco de dados não consegue mais lidar com o volume de dados ou a taxa de requisições, seja por limitações de armazenamento, CPU ou memória.

2. Estratégias de Distribuição de Dados

Existem três estratégias principais para decidir como distribuir os dados entre os shards:

Sharding baseado em hash: Aplica-se uma função hash à chave de shard (por exemplo, user_id) e o resultado determina o shard de destino. Exemplo:

shard_id = HASH(user_id) % NUMERO_DE_SHARDS

Vantagem: distribuição uniforme dos dados. Desvantagem: adicionar ou remover shards exige rehash de todos os dados.

Sharding baseado em intervalo: Os dados são particionados por faixas de valores da chave de shard. Exemplo:

Shard 1: user_id entre 1 e 10000
Shard 2: user_id entre 10001 e 20000
Shard 3: user_id entre 20001 e 30000

Vantagem: consultas por intervalo são eficientes. Desvantagem: pode criar hotspots se os dados não forem uniformemente distribuídos.

Sharding baseado em diretório: Um serviço de lookup mantém um mapeamento explícito entre chaves e shards. Exemplo:

Tabela de roteamento:
user_id 1-5000 -> shard A
user_id 5001-10000 -> shard B
user_id 10001-15000 -> shard C

Vantagem: flexibilidade total para rebalanceamento. Desvantagem: o diretório torna-se um ponto único de falha e gargalo de performance.

3. Implementação Técnica com SQL

Para implementar sharding em SQL, precisamos de uma camada de roteamento que direcione cada consulta ao shard correto. Considere um sistema de e-commerce com shards baseados em customer_id:

Criação de shards (exemplo conceitual):

-- Shard 1: clientes 1-10000
CREATE DATABASE shard1;
CREATE TABLE shard1.orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER,
    order_date DATE,
    total DECIMAL(10,2)
);

-- Shard 2: clientes 10001-20000
CREATE DATABASE shard2;
CREATE TABLE shard2.orders (
    order_id SERIAL PRIMARY KEY,
    customer_id INTEGER,
    order_date DATE,
    total DECIMAL(10,2)
);

Roteamento de consultas: O aplicativo ou middleware calcula o shard antes de executar a query:

function getShard(customer_id):
    if customer_id BETWEEN 1 AND 10000:
        return "shard1"
    else if customer_id BETWEEN 10001 AND 20000:
        return "shard2"

-- Consulta roteada
SELECT * FROM orders WHERE customer_id = 5000;
-- Roteia para shard1

Consultas cross-shard: Para buscar dados que podem estar em múltiplos shards, é necessário consultar todos e agregar os resultados:

-- Buscar todos os pedidos de clientes específicos
-- (pode exigir consulta a múltiplos shards)
SELECT * FROM shard1.orders WHERE customer_id IN (5000, 15000)
UNION ALL
SELECT * FROM shard2.orders WHERE customer_id IN (5000, 15000);

4. Desafios de Consistência e Transações

Transações distribuídas em ambientes shardizados são complexas. O protocolo de commit em duas fases (2PC) é uma abordagem, mas tem custo de performance e pode causar bloqueios. Exemplo simplificado:

-- Fase 1: Preparar
PREPARE TRANSACTION 'transacao_123' ON shard1;
PREPARE TRANSACTION 'transacao_123' ON shard2;

-- Fase 2: Commit (se todos prepararam com sucesso)
COMMIT PREPARED 'transacao_123' ON shard1;
COMMIT PREPARED 'transacao_123' ON shard2;

A escolha entre consistência eventual e consistência forte depende do caso de uso. Sistemas financeiros geralmente exigem consistência forte, enquanto redes sociais podem tolerar consistência eventual.

Para tratamento de falhas, é comum usar réplicas de shards e mecanismos de failover automático.

5. Operações de Manutenção e Rebalanceamento

Adicionar um novo shard requer redistribuir dados dos shards existentes. Exemplo de rebalanceamento com sharding por hash:

-- Antes: 3 shards, função hash % 3
-- Depois: 4 shards, função hash % 4
-- Dados precisam ser migrados

-- Para cada registro, calcular novo shard e mover
UPDATE shard_old SET shard_id = HASH(user_id) % 4;
INSERT INTO shard_new SELECT * FROM shard_old WHERE novo_shard = 4;
DELETE FROM shard_old WHERE novo_shard = 4;

O rebalanceamento tem impacto significativo no desempenho. Técnicas como "double writing" (escrever no shard antigo e novo simultaneamente) ou "virtual shards" (shards lógicos que mapeiam para físicos) podem minimizar o downtime.

6. Ferramentas e Abordagens no PostgreSQL

No PostgreSQL, o sharding distribuído pode ser implementado com extensões como Citus (agora parte da Microsoft). Citus distribui tabelas automaticamente entre workers:

-- Habilitar Citus
SELECT create_distributed_table('orders', 'customer_id');

-- Citus gerencia o roteamento automaticamente
INSERT INTO orders VALUES (1, 5000, '2024-01-01', 100.00);
-- Citus decide em qual worker colocar baseado em customer_id

O particionamento nativo do PostgreSQL (table partitioning) é uma alternativa mais simples, mas não oferece distribuição entre servidores:

CREATE TABLE orders (
    order_id SERIAL,
    customer_id INTEGER,
    order_date DATE,
    total DECIMAL(10,2)
) PARTITION BY RANGE (customer_id);

CREATE TABLE orders_1_10000 PARTITION OF orders
    FOR VALUES FROM (1) TO (10001);

CREATE TABLE orders_10001_20000 PARTITION OF orders
    FOR VALUES FROM (10001) TO (20001);

A principal diferença: particionamento nativo mantém todos os dados no mesmo servidor, enquanto sharding com Citus distribui entre servidores distintos.

7. Monitoramento e Observabilidade em Shards

Em ambientes shardizados, métricas por shard são essenciais:

-- Monitorar tamanho dos shards
SELECT 'shard1' as shard, COUNT(*) as total_orders, 
       SUM(total) as receita_total FROM shard1.orders
UNION ALL
SELECT 'shard2', COUNT(*), SUM(total) FROM shard2.orders;

Slow query logs devem ser coletados de cada shard individualmente. Alertas para desbalanceamento podem ser configurados quando a diferença de tamanho entre shards excede um threshold (ex: 20%).

8. Boas Práticas e Anti-Padrões

Boas práticas:
- Escolha uma chave de shard com alta cardinalidade e distribuição uniforme (ex: user_id, customer_id)
- Mantenha dados relacionados no mesmo shard (co-location) para evitar joins cross-shard
- Use sharding apenas quando o volume de dados realmente justificar (geralmente acima de 1TB ou milhões de transações/dia)

Anti-padrões:
- Usar chaves de shard com baixa cardinalidade (ex: status, gender) — causa hotspots
- Realizar joins frequentes entre shards — extremamente ineficiente
- Aplicar sharding prematuramente — aumenta complexidade sem benefício real

Quando evitar sharding: sistemas com volume de dados pequeno (abaixo de 100GB), requisitos de consistência forte com transações distribuídas complexas, ou quando a aplicação não pode ser modificada para suportar roteamento de shards.

Referências