Escalando Bancos de Dados — Replicação, Sharding e o Perigo da Complexidade Prematura
Conforme uma aplicação cresce, chega um momento em que o banco de dados começa a se tornar um gargalo.
No início, a arquitetura costuma ser simples:
API
│
▼
DATABASE
A aplicação cresce:
API
│
▼
DATABASE
▲
│
100.000 Requests
O banco começa a receber:
- Mais leituras
- Mais escritas
- Mais conexões
- Mais consultas simultâneas
- Mais dados
- Consultas mais complexas
Em algum momento, simplesmente aumentar a capacidade da máquina pode deixar de ser suficiente.
Nesse ponto, existem duas estratégias fundamentais para escalar um banco de dados:
REPLICAÇÃO
│
└── Escalar LEITURAS
SHARDING
│
└── Escalar ESCRITAS e CAPACIDADE
A ideia simplificada é:
Replicação distribui cópias dos dados. Sharding distribui os próprios dados.
Essa diferença é fundamental.
1. O primeiro passo: escalar verticalmente
Antes de pensar em replicação ou sharding, normalmente começamos com:
Vertical Scaling
Também conhecido como:
Scale Up
Temos:
DATABASE
│
▼
┌───────────────────┐
│ CPU │
│ RAM │
│ SSD │
└───────────────────┘
Podemos aumentar:
CPU
RAM
IOPS
Storage
Por exemplo:
8 CPU
32 GB RAM
│
▼
16 CPU
64 GB RAM
│
▼
32 CPU
128 GB RAM
Essa estratégia é simples.
Não precisamos alterar a aplicação.
Não precisamos distribuir os dados.
Não precisamos sincronizar múltiplos bancos.
Por isso:
Sempre que possível, comece pelo simples.
O problema é que existe um limite físico e financeiro.
Uma máquina eventualmente pode ficar:
Cara demais
ou:
Insuficiente
É nesse momento que começamos a considerar:
Replicação
ou:
Sharding
2. Replicação
Replicação significa manter múltiplas cópias dos mesmos dados.
Imagine:
DATABASE
│
┌────────┼────────┐
▼ ▼ ▼
DB 1 DB 2 DB 3
Todos possuem os mesmos dados.
Por exemplo:
DB 1
User 123
Name = João
DB 2
User 123
Name = João
DB 3
User 123
Name = João
A vantagem é que podemos distribuir operações de leitura.
3. Primary e Replicas
Uma arquitetura muito comum é:
PRIMARY
│
┌──────────┴──────────┐
│ │
▼ ▼
REPLICA 1 REPLICA 2
A aplicação escreve no:
PRIMARY
E as réplicas recebem os dados:
PRIMARY
│
│ Replication
├──────────────► Replica 1
│
└──────────────► Replica 2
As leituras podem ser distribuídas:
LOAD BALANCER
│
┌────────┴────────┐
▼ ▼
Replica 1 Replica 2
Agora:
10.000 Reads
│
├── 5.000 → Replica 1
│
└── 5.000 → Replica 2
O Primary fica responsável principalmente pelas escritas.
4. Por que replicação escala leituras?
Imagine uma aplicação com:
100.000 operações
Dessas:
95.000 → READ
5.000 → WRITE
Temos uma proporção:
95% READ
5% WRITE
Nesse cenário, replicação é extremamente eficiente.
Podemos ter:
PRIMARY
│
┌───────┼───────┐
▼ ▼ ▼
R1 R2 R3
As leituras são distribuídas:
READ ──► R1
READ ──► R2
READ ──► R3
Enquanto:
WRITE ──► PRIMARY
Isso permite aumentar a capacidade de leitura adicionando novas réplicas.
5. O limite da replicação
Existe um problema importante.
As réplicas são cópias.
Portanto, alguém precisa atualizar todas elas.
Imagine:
PRIMARY
│
├──► Replica 1
├──► Replica 2
├──► Replica 3
└──► Replica 4
Agora temos:
1 Primary
4 Replicas
A quantidade de leituras que podemos suportar aumentou.
Mas as escritas ainda passam pelo:
PRIMARY
Se tivermos:
1.000.000 Writes
continuaremos tendo:
1.000.000 Writes
│
▼
PRIMARY
Adicionar réplicas não resolve esse gargalo.
Por isso:
Replicação é excelente para escalar leituras, mas não resolve diretamente o gargalo de escrita em um único Primary.
6. Replicação assíncrona
Uma estratégia comum é utilizar replicação assíncrona.
O fluxo é:
Application
│
▼
PRIMARY
│
│
├──────────► Replica 1
│
└──────────► Replica 2
O Primary confirma a escrita:
WRITE
│
▼
PRIMARY
│
▼
OK
E depois as réplicas recebem a atualização.
Isso reduz a latência de escrita.
Porém, existe um possível atraso:
PRIMARY
User = João
Replica
User = Pedro
Durante um curto período.
Esse fenômeno é conhecido como:
Replication Lag
7. Replication Lag
Imagine:
PRIMARY
User = João
Executamos:
UPDATE User
SET Name = 'Maria'
O Primary agora possui:
Maria
Mas a réplica ainda pode ter:
João
Durante alguns milissegundos:
PRIMARY → Maria
REPLICA → João
Isso pode gerar um problema:
Usuário atualiza seu nome
│
▼
WRITE → Primary
│
▼
"Maria"
│
▼
READ → Replica
│
▼
"João"
O usuário pode pensar:
"Minha alteração não foi salva."
Na realidade:
Database
│
├── Primary → Maria
│
└── Replica → João
A réplica ainda não sincronizou.
8. Read Your Own Writes
Uma solução comum para problemas de consistência é garantir que, após uma escrita, o usuário leia do Primary durante algum tempo.
Por exemplo:
WRITE
│
▼
PRIMARY
│
▼
READ
│
▼
PRIMARY
Depois:
READ
│
▼
Replica
Isso permite aproveitar as réplicas sem criar uma experiência inconsistente imediatamente após uma escrita.
9. Replicação e Alta Disponibilidade
Replicação também pode ajudar na disponibilidade.
Imagine:
PRIMARY
│
┌──────┴──────┐
▼ ▼
Replica 1 Replica 2
O Primary falha:
PRIMARY
X
O sistema pode promover uma réplica:
Replica 1
│
▼
NEW PRIMARY
Agora:
Replica 2
│
▼
Replica
O sistema volta a operar.
Isso é conhecido como:
Failover
Portanto, replicação pode fornecer:
Escalabilidade de leitura
+
Alta disponibilidade
Mas:
Alta disponibilidade não significa que a aplicação ficará automaticamente 100% disponível.
O processo de failover pode gerar:
- Pequeno downtime
- Perda de algumas transações em replicação assíncrona
- Reconexões
- Reeleição
- Reconfiguração da aplicação
10. Quando usar replicação?
Replicação é uma excelente escolha quando:
READS >>> WRITES
Por exemplo:
95% Reads
5% Writes
Também é útil quando precisamos de:
- Alta disponibilidade
- Failover
- Distribuição geográfica de leituras
- Redução de latência de leitura
Uma arquitetura poderia ser:
USERS
│
▼
APPLICATION
│
┌────────┴────────┐
│ │
WRITES READS
│ │
▼ ▼
PRIMARY READ REPLICAS
│
┌───────┼───────┐
▼ ▼ ▼
R1 R2 R3
11. Sharding
Agora imagine que o problema não são apenas as leituras.
Imagine que temos:
1.000.000 Writes/second
Temos:
PRIMARY
│
▼
1.000.000 Writes
Adicionar mais réplicas não resolve.
Precisamos dividir os dados.
Isso é:
Sharding
Sharding significa dividir um banco de dados em múltiplas partes.
Cada parte é chamada de:
Shard
Imagine:
DATABASE
│
┌─────────┼─────────┐
▼ ▼ ▼
SHARD 1 SHARD 2 SHARD 3
Cada shard contém uma parte dos dados.
Por exemplo:
Shard 1
Users 1 - 1.000.000
Shard 2
Users 1.000.001 - 2.000.000
Shard 3
Users 2.000.001 - 3.000.000
Agora as escritas podem ser distribuídas:
Write User 123
│
▼
Shard 1
Write User 1.500.000
│
▼
Shard 2
Write User 2.500.000
│
▼
Shard 3
Cada shard possui sua própria capacidade.
12. Sharding Horizontal
Sharding é uma forma de:
Horizontal Scaling
Também chamado:
Scale Out
Em vez de:
1 servidor enorme
temos:
3 servidores menores
Cada um processando uma parte dos dados.
DATABASE
│
┌──────┼──────┐
▼ ▼ ▼
Shard 1 Shard 2 Shard 3
A capacidade total aumenta.
13. Shard Key
Para saber em qual shard um dado deve ser armazenado, precisamos de uma:
Shard Key
Por exemplo:
user_id
Podemos utilizar:
hash(user_id) % 3
Resultado:
User 123
│
▼
Hash
│
▼
Shard 1
Outro:
User 456
│
▼
Hash
│
▼
Shard 2
A aplicação ou uma camada de roteamento determina onde cada dado está.
14. Range-Based Sharding
Uma estratégia é dividir por intervalos.
Por exemplo:
Shard 1
ID 1 - 1.000.000
Shard 2
ID 1.000.001 - 2.000.000
Shard 3
ID 2.000.001 - 3.000.000
É simples de entender.
Porém, pode criar um problema:
Hot Shard
Imagine que todos os novos usuários recebem IDs sequenciais.
As novas escritas sempre vão para:
Shard 3
Enquanto:
Shard 1
Shard 2
ficam praticamente ociosos.
Temos:
Shard 1 → 10%
Shard 2 → 10%
Shard 3 → 80%
Isso é uma distribuição ruim.
15. Hash-Based Sharding
Outra estratégia é usar hash.
hash(user_id)
│
▼
Shard escolhido
Por exemplo:
hash(123) % 3 → Shard 1
hash(456) % 3 → Shard 2
hash(789) % 3 → Shard 3
Isso geralmente distribui melhor os dados.
Porém, existe um problema.
Imagine:
3 Shards
E depois precisamos de:
5 Shards
Se o cálculo for:
hash(key) % 3
ao mudar para:
hash(key) % 5
muitos dados precisam mudar de shard.
Isso é:
Resharding
E pode ser extremamente complexo.
16. Consistent Hashing
Uma alternativa é utilizar:
Consistent Hashing
Em vez de:
hash(key) % N
temos um espaço de hash.
Imagine:
HASH RING
Shard A
│
┌─────┴─────┐
│ │
Shard C Shard B
As chaves são distribuídas pelo anel.
Quando adicionamos um shard:
Shard D
apenas parte das chaves precisa ser redistribuída.
Isso reduz o custo de:
Resharding
Consistent hashing é muito utilizado em sistemas distribuídos que precisam crescer dinamicamente.
17. O problema das consultas entre shards
Sharding traz uma complexidade enorme.
Imagine:
Users
Orders
Products
Podemos ter:
Shard 1 → Users A
Shard 2 → Users B
Shard 3 → Users C
Agora precisamos consultar:
SELECT *
FROM users
WHERE country = 'Brazil';
Se não sabemos em qual shard estão os dados:
Request
│
├──► Shard 1
├──► Shard 2
└──► Shard 3
Precisamos consultar todos.
Isso é conhecido como:
Scatter-Gather
Depois:
Shard 1 ──┐
Shard 2 ──┼──► Aggregator
Shard 3 ──┘
O resultado precisa ser combinado.
Isso aumenta:
- Latência
- Complexidade
- Uso de CPU
- Tráfego de rede
18. Joins entre shards
Imagine:
SELECT *
FROM users
JOIN orders
ON users.id = orders.user_id;
Se:
User
│
▼
Shard 1
e:
Orders
│
▼
Shard 3
O banco não consegue simplesmente fazer:
JOIN local
Agora precisamos realizar:
Shard 1
│
│ Network
▼
Shard 3
Ou:
Shard 1 ───┐
├──► Application
Shard 3 ───┘
A aplicação pode precisar combinar os dados.
Isso torna o sistema muito mais complexo.
19. Transações distribuídas
Imagine uma operação:
Transferir R$ 100
Precisamos atualizar:
Conta A → Shard 1
Conta B → Shard 2
Agora temos:
Shard 1
│
│ UPDATE
▼
Conta A - R$ 100
Shard 2
│
│ UPDATE
▼
Conta B + R$ 100
E se:
Shard 1 → SUCESSO
Shard 2 → FALHA
Temos:
Conta A → -R$ 100
Conta B → Não recebeu
Agora precisamos de mecanismos de:
- Distributed Transactions
- Two-Phase Commit
- Saga Pattern
- Compensating Transactions
A complexidade aumenta significativamente.
20. Por que sharding é tão complexo?
Quando fazemos sharding, precisamos resolver:
Routing
Shard Key
Resharding
Cross-Shard Queries
Cross-Shard Joins
Distributed Transactions
Hot Shards
Operational Complexity
Por isso:
Sharding não deve ser a primeira solução para um banco que está ficando lento.
21. O grande erro: Sharding prematuro
Um dos maiores erros em arquitetura é introduzir sharding antes de realmente precisar.
Imagine uma aplicação com:
10.000 usuários
E alguém decide:
Vamos criar 10 shards!
Arquitetura:
Application
│
┌──────┼──────┐
▼ ▼ ▼
S1 S2 S3
...
S10
Agora temos:
- 10 bancos
- 10 monitoramentos
- 10 backups
- 10 migrations
- 10 conexões
- Routing
- Resharding
- Queries distribuídas
Tudo isso para um sistema que talvez pudesse funcionar perfeitamente com:
1 Database
O resultado é:
Complexidade ↑
Custo ↑
Bugs ↑
Operação ↑
Sem necessariamente existir:
Performance ↑
22. A arquitetura mais simples possível
Uma progressão saudável pode ser:
ETAPA 1
Application
│
▼
Database
Depois:
ETAPA 2
Application
│
▼
Bigger Database
Depois:
ETAPA 3
Application
│
▼
Primary
│
├──► Replica 1
└──► Replica 2
Depois:
ETAPA 4
Application
│
▼
Primary
│
├──► Replica 1
├──► Replica 2
└──► Replica 3
E somente quando necessário:
ETAPA 5
Application
│
▼
Shard Router
│
├──► Shard 1
├──► Shard 2
└──► Shard 3
Essa progressão evita complexidade prematura.
23. A ordem correta para escalar
Antes de fazer sharding, devemos analisar outras alternativas.
Uma progressão comum:
1. Otimizar queries
│
▼
2. Criar índices
│
▼
3. Corrigir N+1 queries
│
▼
4. Aumentar hardware
│
▼
5. Adicionar cache
│
▼
6. Replicar leituras
│
▼
7. Separar workloads
│
▼
8. Sharding
A ordem exata depende do sistema, mas a ideia é:
Não introduza a solução mais complexa antes de eliminar os problemas mais simples.
24. Otimização antes da distribuição
Imagine uma consulta:
SELECT *
FROM users
WHERE email = 'user@email.com';
Sem índice:
Database
│
▼
Scan 10.000.000 rows
Criamos:
INDEX email
Agora:
Database
│
▼
Index Lookup
│
▼
1 row
O problema foi resolvido sem:
Replicação
e sem:
Sharding
Por isso:
O primeiro passo para escalar um banco é descobrir o que realmente está lento.
25. Replicação vs Sharding
CaracterísticaReplicaçãoShardingDadosDuplicadosDivididosPrincipal objetivoEscalar leiturasEscalar capacidade/escritasAlta disponibilidadeSimPode ajudarRead ScalingExcelentePossívelWrite ScalingLimitadoExcelenteComplexidadeModeradaAltaCross-shard queriesNãoProblemaReshardingNãoSimDistributed TransactionsNormalmente nãoPode ser necessárioOperaçãoMais simplesMais complexa
A diferença fundamental:
REPLICAÇÃO
Mesmo dado
│
├──► DB 1
├──► DB 2
└──► DB 3
Enquanto:
SHARDING
Dados diferentes
│
├──► Shard 1
├──► Shard 2
└──► Shard 3
26. Podemos combinar os dois
Sistemas grandes frequentemente utilizam:
Sharding + Replicação
Por exemplo:
APPLICATION
│
SHARD ROUTER
│
┌──────────────┼──────────────┐
▼ ▼ ▼
SHARD 1 SHARD 2 SHARD 3
│ │ │
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
▼ ▼ ▼ ▼ ▼ ▼
Primary Replica Primary Replica Primary Replica
Agora:
Sharding
│
└── Distribui dados e escritas
Replication
│
└── Distribui leituras e aumenta disponibilidade
Essa combinação permite escalar grandes volumes.
Mas também combina as complexidades.
27. Como saber quando usar replicação?
Pergunte:
Meu problema é principalmente READ?
Se:
SIM
Considere:
Read Replicas
Se temos:
100 Reads
10 Writes
Replicação provavelmente é uma ótima solução.
Também considere replicação quando precisa de:
Failover
Alta disponibilidade
Leituras geograficamente distribuídas
28. Como saber quando usar sharding?
Pergunte:
Meu problema é WRITE?
E:
Meu Primary chegou ao limite?
E:
A máquina não consegue crescer verticalmente?
E:
O volume total de dados excede a capacidade de uma máquina?
Se a resposta for sim para vários desses pontos:
Sharding
pode ser necessário.
29. O papel das métricas
Nunca devemos decidir por:
"Meu sistema está crescendo."
Devemos observar métricas.
Por exemplo:
CPU
RAM
Disk I/O
IOPS
Connections
Query Latency
Transactions/sec
Reads/sec
Writes/sec
Replication Lag
Storage
Imagine:
CPU = 30%
RAM = 40%
Writes = 10.000/s
Talvez não precisemos de sharding.
Agora:
CPU = 100%
RAM = 95%
Disk I/O = 100%
Writes = 200.000/s
Talvez seja necessário escalar.
A decisão deve ser baseada em:
MÉTRICAS
│
▼
GARGALO
│
▼
SOLUÇÃO
Nunca:
Crescimento
│
▼
Sharding
30. Uma árvore de decisão
Podemos pensar assim:
Banco está lento?
│
▼
Identificar gargalo
│
├── Query lenta
│ │
│ ▼
│ Otimizar
│
├── Falta de índice
│ │
│ ▼
│ Criar índice
│
├── Memória/CPU
│ │
│ ▼
│ Scale Up
│
├── Muitas leituras
│ │
│ ▼
│ Read Replicas
│
└── Muitas escritas
│
▼
Sharding?
│
▼
Só se necessário
31. O principal insight
A melhor arquitetura não é:
A mais distribuída
É:
A mais simples
que atende aos requisitos
Se um banco único suporta:
10 milhões de usuários
não existe motivo técnico para utilizar:
100 shards
A arquitetura deve evoluir conforme os gargalos aparecem.
32. Resumo
Replicação
Database
│
├──► Replica 1
├──► Replica 2
└──► Replica 3
Ideal para:
READ SCALING
Também pode ajudar em:
Alta disponibilidade
Failover
Sharding
Database
│
├──► Shard 1
├──► Shard 2
└──► Shard 3
Ideal para:
WRITE SCALING
E para:
Capacidade total
Replicação
Mesmo dado
Duplicado
Sharding
Dados diferentes
Distribuídos
Conclusão
Quando um banco de dados começa a atingir seus limites, existem duas estratégias fundamentais:
REPLICAÇÃO
e:
SHARDING
Replicação é normalmente o primeiro passo para sistemas com grande volume de leituras:
PRIMARY
│
┌────────┴────────┐
▼ ▼
Replica 1 Replica 2
Ela permite distribuir leituras e melhorar a disponibilidade, mas não elimina o gargalo de escrita no Primary.
Quando o problema está na capacidade de escrita ou no tamanho total dos dados, podemos considerar sharding:
ROUTER
│
┌─────────┼─────────┐
▼ ▼ ▼
SHARD 1 SHARD 2 SHARD 3
Porém, o custo é muito maior.
Sharding introduz:
- Roteamento
- Escolha de Shard Key
- Resharding
- Hot Shards
- Scatter-Gather
- Cross-Shard Joins
- Transações distribuídas
- Maior complexidade operacional
Por isso, um dos maiores erros de arquitetura é fazer:
"Minha aplicação está crescendo"
│
▼
"Vamos fazer sharding"
O caminho correto é:
Medir
│
▼
Identificar o gargalo
│
▼
Otimizar
│
▼
Scale Up
│
▼
Cache
│
▼
Read Replicas
│
▼
Sharding
Não existe medalha por usar a arquitetura mais complexa.
O objetivo de um bom arquiteto é encontrar o menor nível de complexidade capaz de resolver o problema atual.
A regra mental pode ser resumida assim:
BANCO
│
▼
Qual é o gargalo?
│
┌───────────┴───────────┐
▼ ▼
LEITURA ESCRITA
│ │
▼ ▼
REPLICAÇÃO SHARDING
│ │
▼ ▼
Read Scaling Write Scaling
E a regra mais importante:
Não faça sharding porque você imagina que um dia precisará dele. Faça sharding quando as métricas mostrarem que você realmente precisa dele.
Até lá, mantenha o sistema simples.
Porque, em System Design, complexidade também é um custo de infraestrutura.