trilha

Tecnologias/02 - Bancos de Dados/02 - Não Relacionais/01 - MongoDB6 min

Sharding por Dentro

Conceito

Sharding é a razão de existir do MongoDB em muitos projetos: é o único dos bancos desta trilha em que escala horizontal de escrita vem na caixa. No Postgres, MySQL e Oracle, isso é projeto (Sharding, Federation, Citus, Vitess) ou opção paga.

A topologia

              app  →  mongos (roteador, sem estado — pode ter vários)
                        │
        ┌───────────────┼───────────────┐
        │               │               │
    shard A         shard B         shard C          ← cada um é um REPLICA SET
   (rs: P+2S)      (rs: P+2S)      (rs: P+2S)
        └───────────────┴───────────────┘
                        │
                 config servers (replica set)
              ← guarda o MAPA: qual faixa vive em qual shard

Três papéis, e cada um falha de um jeito diferente:

  • mongos — roteia. Sem estado, mas cacheia o mapa de rotas; um mapa velho causa erro de rota (o mongos se corrige buscando a versão nova).
  • Config servers — a fonte da verdade do mapa. Se caírem, o cluster fica somente leitura de metadados: não há balanceamento nem split, e escritas seguem no que já está mapeado.
  • Shards — cada um um replica set completo (3. Transações, Replica Set e Durabilidade).

Chunks e o balanceador

O espaço da shard key é dividido em chunks (faixas contíguas). O balanceador (rodando nos config servers desde a 6.0) move chunks entre shards para equilibrar.

shard key = cliente_id (ranged)

chunk 1: [minKey  .. 1000)   → shard A
chunk 2: [1000    .. 5000)   → shard B
chunk 3: [5000    .. maxKey) → shard C

Quando um chunk cresce além de chunkSize (padrão 128 MB), ele é dividido. A partir da 6.0/7.0 o critério de balanceamento passou a ser o tamanho de dado por shard, não a contagem de chunks — o que corrigiu um desequilíbrio histórico.

A shard key: a decisão mais importante e mais difícil de reverter

Uma shard key precisa de três propriedades ao mesmo tempo:

Propriedade Por quê Se falhar
Alta cardinalidade define o número máximo de chunks poucos chunks → não distribui
Baixa frequência nenhum valor deve concentrar o volume hot shard
Não monotônica valores crescentes vão sempre para o último chunk hot shard na escrita

O erro clássico é a chave monotônica:

// ❌ timestamp ou ObjectId: TODA escrita nova cai no chunk de maxKey → um shard só
sh.shardCollection("app.eventos", { criado_em: 1 })

// ✅ hashed: distribui uniformemente (perde consulta por faixa)
sh.shardCollection("app.eventos", { _id: "hashed" })

// ✅✅ composta: distribui E mantém localidade útil por dispositivo
sh.shardCollection("app.eventos", { sensor_id: 1, criado_em: 1 })

A última é a melhor forma geral: um prefixo de alta cardinalidade e boa distribuição, com um sufixo que dá localidade (os eventos do mesmo sensor ficam juntos, ordenados).

Ranged vs hashed

Ranged Hashed
Distribuição pode desequilibrar uniforme
Consulta por faixa eficiente (vai a 1 shard) scatter-gather (vai a todos)
Chave monotônica hot shard resolvido
Localidade de dado preservada destruída

Targeted vs scatter-gather: a única métrica que importa

// ✅ TARGETED: a shard key está no filtro → 1 shard responde
db.eventos.find({ sensor_id: "s-42", criado_em: { $gte: ontem } })

// ❌ SCATTER-GATHER: sem a shard key → TODOS os shards são consultados,
//    e o mongos precisa mesclar (e ordenar) os resultados
db.eventos.find({ tipo: "alerta" })

Num cluster de 10 shards, uma consulta scatter-gather faz 10 consultas e uma mesclagem. Se a maior parte do seu tráfego for scatter-gather, o sharding piorou a latência — você adicionou salto de rede e coordenação sem reduzir trabalho por nó.

O teste que decide se vale shardar: a shard key candidata aparece no filtro das consultas mais frequentes? Se não, escolha outra chave — ou não sharde.

Resharding: a saída que existe (e dói)

  • 5.0+: reshardCollection reescreve a coleção com uma chave nova.
  • 7.0+: mais rápido e com menos indisponibilidade.
  • Ainda assim: é uma operação longa, que precisa de espaço em disco extra, e com janela de bloqueio de escrita no final.

Antes disso havia apenas refineCollectionShardKey (que só acrescenta sufixos à chave existente). Ou seja: hoje é possível corrigir uma escolha ruim — mas continua sendo uma operação que ninguém quer fazer com o sistema em produção.

Trade-offs

Sharding só se paga acima de um limiar. Ele adiciona mongos, config servers, scatter-gather, transações distribuídas mais caras e uma decisão irreversível-na-prática. Um replica set num servidor grande resolve muito mais do que a maioria imagina. Sharde por um destes motivos concretos:

  1. O working set não cabe na RAM do maior servidor viável (4. WiredTiger e Armazenamento).
  2. A vazão de escrita passou do que um primário aguenta.
  3. Requisito de localidade geográfica (zone sharding para residência de dados).

Transação distribuída em cluster sharded é bem mais cara. Ela precisa de coordenação em duas fases entre shards. Se o desenho exige transações que cruzam shards com frequência, a shard key está errada (ela deveria manter o agregado junto) — ou o modelo deveria ser relacional (4. MongoDB vs Relacional).

Índice único fica limitado. Numa coleção sharded, um índice unique só é possível se contiver a shard key. Unicidade global em outro campo (e-mail, CPF) exige uma coleção auxiliar com aquele campo como shard key — trabalho manual, que um relacional daria de graça.

O trade-off central: o sharding do MongoDB troca um limite físico (um servidor) por um compromisso de modelagem (a shard key). Ele é a razão de escolher MongoDB para escala de escrita e, escolhido errado, é a razão de o cluster ser mais lento que a máquina única que existia antes.

Exemplo prático

Preparar e verificar

// habilitar e shardar
sh.enableSharding("app")
db.eventos.createIndex({ sensor_id: 1, criado_em: 1 })   // o índice DEVE existir antes
sh.shardCollection("app.eventos", { sensor_id: 1, criado_em: 1 })

// o estado do cluster
sh.status()

// distribuição real por shard — o número que revela hot shard
db.eventos.getShardDistribution()
// Shard shard-a: 4.2 GB (34%)   1_204_133 docs
// Shard shard-b: 4.0 GB (33%)   1_198_004 docs
// Shard shard-c: 4.1 GB (33%)   1_201_876 docs      ← equilibrado 🎉

// a consulta é targeted?
db.eventos.find({ sensor_id: "s-42" }).explain()
//   → "shards": { "shard-b": {...} }                  ← 1 shard: targeted ✅
db.eventos.find({ tipo: "alerta" }).explain()
//   → SHARD_MERGE com os 3 shards                     ← scatter-gather ⚠️

Zone sharding: residência de dados

// LGPD/GDPR: dado de cliente europeu fica em hardware europeu
sh.addShardToZone("shard-eu-1", "EU")
sh.addShardToZone("shard-br-1", "BR")

sh.updateZoneKeyRange("app.clientes",
  { regiao: "EU", cliente_id: MinKey }, { regiao: "EU", cliente_id: MaxKey }, "EU")
sh.updateZoneKeyRange("app.clientes",
  { regiao: "BR", cliente_id: MinKey }, { regiao: "BR", cliente_id: MaxKey }, "BR")

É um caso em que o sharding se justifica sem necessidade de escala: o requisito é legal, não de capacidade.

Como isso falha em produção

Sintoma Causa
Um shard com 90% da escrita shard key monotônica (timestamp/ObjectId)
Um shard com 90% da leitura shard key de baixa frequência (um tenant gigante)
Toda query lenta após shardar tráfego dominado por scatter-gather
jumbo chunk que não move chunk maior que o limite, com uma única shard key dentro (chave de cardinalidade baixa)
Balanceador nunca termina janela de balanceamento curta, ou I/O saturado
duplicate key que não deveria índice unique não pode ser global sem a shard key
Transação distribuída lenta/abortando agregado espalhado entre shards
Cluster "sem metadados" config servers indisponíveis

Aprofundamento aplicado

Distribuição de dados e distribuição de tráfego

Uma coleção pode ter bytes bem distribuídos e quase todas as requisições atingir um único cliente. Balancear armazenamento não divide automaticamente uma chave lógica muito disputada. Uma chave hashed distribui valores diferentes, mas o mesmo valor continua recebendo o mesmo resultado de hash. Referência: escolha de shard key.

Cenário: um cliente concentra 60% das escritas. Acrescentar uma dimensão à chave pode espalhar esse cliente, mas consultar todos os seus pedidos passa a tocar mais partes. O projeto troca localidade de leitura por distribuição de escrita.

Plano de validação

Liste consultas principais, campos de filtro, cardinalidade da chave e distribuição de volume por cliente. Depois verifique quantos shards cada consulta alcança e como a latência muda quando uma das partes fica lenta.

Exercício: compare (clienteId) e uma chave que subdivide pedidos do cliente. Resultado esperado: identifique qual desenho direciona uma leitura pontual, qual espalha um cliente grande e quais consultas exigem juntar resultados de várias partes.

Relacionado


Parte de MOC MongoDB · Sharding (docs)

Buscar

Busca por título, seção e texto das notas