Estratégias de Sharding de Banco de Dados: Escalabilidade Horizontal para Sistemas em Produção
Quando uma única instância de banco de dados não consegue mais lidar com a carga, o sharding se torna o caminho a seguir. Compreender padrões de sharding, trade-offs e implicações operacionais evita becos sem saída arquiteturais.
Resumo executivo
Quando uma única instância de banco de dados não consegue mais lidar com a carga, o sharding se torna o caminho a seguir. Compreender padrões de sharding, trade-offs e implicações operacionais evita becos sem saída arquiteturais.
Ultima atualizacao: 13/03/2026
Introdução: Quando o escalamento vertical bate na parede
Toda aplicação em crescimento eventualmente encontra os limites de uma única instância de banco de dados. Escalamento vertical—adicionar CPU, memória ou armazenamento a uma instância existente—torna-se progressivamente mais caro e eventualmente impossível. Nesse ponto, escalamento horizontal através de sharding de banco de dados torna-se necessário.
Sharding divide um banco de dados grande em peças menores e mais gerenciáveis chamadas shards, distribuídas através de múltiplas instâncias de banco de dados. Cada shard contém um subconjunto dos dados, e a aplicação roteia queries para o shard apropriado.
A decisão de shard é significativa. Ela introduz complexidade arquitetural, overhead operacional e novos modos de falha. Mas para aplicações intensivas em dados com alto throughput de escrita ou grandes conjuntos de dados, o sharding é frequentemente o único caminho para escala sustentável.
Fundamentos de sharding
Sharding vs replicação
Sharding é fundamentalmente diferente de replicação:
| Aspecto | Replicação | Sharding |
|---|---|---|
| Propósito | Alta disponibilidade, escalabilidade de leitura | Escalabilidade de escrita, distribuição de dados |
| Dados | Cópia completa em cada instância | Subconjunto particionado em cada instância |
| Capacidade de escrita | Limitada pelo primário | Escala com contagem de shards |
| Capacidade de leitura | Escala com contagem de réplicas | Escala com contagem de shards |
| Complexidade | Baixa | Alta |
| Consistência | Forte (com configuração apropriada) | Eventual (entre shards) |
Replicação copia dados para alta disponibilidade e escalabilidade de leitura. Sharding distribui dados para escalabilidade de escrita e distribuição de dados.
Quando considerar sharding
Indicadores de que o sharding pode ser necessário:
- Throughput de escrita excede capacidade de instância única: Banco de dados é limitado por I/O ou CPU durante workloads pesadas de escrita
- Conjunto de dados excede limites de armazenamento: Instância única não consegue armazenar todos os dados (comum em aplicações SaaS armazenando conteúdo gerado por usuário)
- Performance degrada com crescimento de dados: Latência de query aumenta conforme o tamanho dos dados cresce
- Backup e restore se tornam impraticáveis: Backups completos de banco de dados demoram muito
- Custo de escalamento vertical torna-se proibitivo: Atualizar para instâncias maiores não é mais economicamente viável
"Escale antes de precisar" não se aplica ao sharding. Shard quando você tem evidência clara de que escalamento de instância única é insuficiente, não como otimização prematura.
Arquiteturas de sharding
Sharding gerenciado por aplicação
A camada de aplicação determina qual shard deve receber cada query. Esta é a abordagem mais comum e oferece máxima flexibilidade.
typescript// Sharding em nível de aplicação
class ShardedDatabase {
private shards: Map<number, DatabaseConnection> = new Map();
private shardCount: number;
constructor(shardConfigs: ShardConfig[]) {
this.shardCount = shardConfigs.length;
shardConfigs.forEach(config => {
this.shards.set(config.shardId, this.createConnection(config));
});
}
// Sharding baseado em hash
getShardId(key: string): number {
const hash = this.hashFunction(key);
return hash % this.shardCount;
}
async getUser(userId: string): Promise<User> {
const shardId = this.getShardId(userId);
const shard = this.shards.get(shardId);
return shard.query('SELECT * FROM users WHERE id = ?', [userId]);
}
async createUser(user: User): Promise<User> {
const shardId = this.getShardId(user.id);
const shard = this.shards.get(shardId);
return shard.insert('users', user);
}
// Sharding baseado em range
async getOrdersByDateRange(startDate: Date, endDate: Date): Promise<Order[]> {
const results: Order[] = [];
// Query apenas shards relevantes
const relevantShards = this.getShardsForDateRange(startDate, endDate);
for (const shardId of relevantShards) {
const shard = this.shards.get(shardId);
const shardResults = await shard.query(
'SELECT * FROM orders WHERE created_at BETWEEN ? AND ?',
[startDate, endDate]
);
results.push(...shardResults);
}
return results;
}
private hashFunction(key: string): number {
// Função hash simples - em produção use crypto ou murmurhash
let hash = 0;
for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Converte para inteiro de 32-bit
}
return Math.abs(hash);
}
private getShardsForDateRange(startDate: Date, endDate: Date): number[] {
// Implementação depende da estratégia de sharding por data
// Exemplo: Shard por mês
const startMonth = startDate.getMonth();
const endMonth = endDate.getMonth();
const shards = [];
for (let month = startMonth; month <= endMonth; month++) {
shards.push(month % this.shardCount);
}
return [...new Set(shards)]; // Remove duplicatas
}
private createConnection(config: ShardConfig): DatabaseConnection {
// Cria conexão de banco de dados baseada na configuração
return new DatabaseConnection(config);
}
}
interface ShardConfig {
shardId: number;
host: string;
port: number;
database: string;
username: string;
password: string;
}Vantagens:
- Flexibilidade máxima em estratégia de sharding
- Não requer funcionalidades especializadas de banco de dados
- Pode otimizar para padrões específicos de acesso
Desvantagens:
- Complexidade de aplicação aumenta
- Todas as aplicações devem implementar lógica de sharding
- Queries entre shards requerem orquestração manual
Sharding gerenciado por banco de dados
Alguns bancos oferecem funcionalidades de sharding integradas que abstraem a complexidade da camada de aplicação.
MongoDB Sharded Clusters:
javascript// Configuração de sharding MongoDB
sh.addShard("shard1.example.com:27017")
sh.addShard("shard2.example.com:27017")
sh.addShard("shard3.example.com:27017")
// Habilita sharding em um banco de dados
sh.enableSharding("myapp")
// Shard uma coleção com chave de shard hash
sh.shardCollection("myapp.users", { "user_id": "hashed" })
// Shard uma coleção com chave de shard range
sh.shardCollection("myapp.orders", { "created_at": 1 })PostgreSQL Citus Extension:
sql-- Criar tabela distribuída
SELECT create_distributed_table('users', 'user_id');
-- Definir método de distribuição
SELECT alter_distributed_table('users', 'distribution_method' => 'hash');
-- Criar tabela de referência (replicada para todos os shards)
SELECT create_reference_table('countries');
-- Criar tabela co-localizada
SELECT create_distributed_table('orders', 'user_id');
SELECT alter_distributed_table('orders', 'colocate_with' => 'users');Vantagens:
- Complexidade de aplicação reduzida
- Banco de dados roteia queries
- Re-balanceamento e escalamento integrados
Desvantagens:
- Menos flexibilidade em estratégia de sharding
- Vendor lock-in para banco específico
- Pode exigir expertise especializada
Estratégias de sharding
Sharding baseado em hash
Dados são distribuídos baseados em uma função hash aplicada a uma chave de shard. Isso fornece distribuição uniforme mas torna queries de range ineficientes.
typescriptclass HashBasedSharding {
private shardCount: number;
constructor(shardCount: number) {
this.shardCount = shardCount;
}
getShard(key: string): number {
const hash = this.crc32(key);
return hash % this.shardCount;
}
// Implementação CRC32
private crc32(str: string): number {
// Implementação da função hash CRC32
// Em produção, use uma biblioteca bem testada
let crc = 0 ^ -1;
for (let i = 0; i < str.length; i++) {
crc = (crc >>> 8) ^ crc32Table[(crc ^ str.charCodeAt(i)) & 0xff];
}
return (crc ^ -1) >>> 0;
}
}
// Uso
const sharding = new HashBasedSharding(8);
const userShard = sharding.getShard('user_12345'); // 3Características:
- Distribuição uniforme de dados entre shards
- Sem hotspots (assumindo boa função hash)
- Queries entre shards difíceis
- Re-sharding requer migração significativa de dados
Melhor para:
- Dados centrados em usuário (usuários, perfis, configurações)
- Chaves de alta cardinalidade
- Queries por ponto baseadas em chave de shard
- Workloads pesadas em escrita
Sharding baseado em range
Dados são distribuídos baseados em ranges de valores de chave de shard. Isso habilita queries de range eficientes mas pode criar hotspots.
typescriptclass RangeBasedSharding {
private ranges: Array<{min: number, max: number, shardId: number}>;
constructor(ranges: Array<{min: number, max: number, shardId: number}>) {
// Ordena ranges por valor mínimo
this.ranges = ranges.sort((a, b) => a.min - b.min);
}
getShard(key: number): number {
for (const range of this.ranges) {
if (key >= range.min && key <= range.max) {
return range.shardId;
}
}
throw new Error(`Nenhum shard encontrado para chave: ${key}`);
}
addShard(min: number, max: number, shardId: number) {
this.ranges.push({ min, max, shardId });
this.ranges.sort((a, b) => a.min - b.min);
}
}
// Uso: Shard usuários por range de ID
const sharding = new RangeBasedSharding([
{ min: 1, max: 1000000, shardId: 0 },
{ min: 1000001, max: 2000000, shardId: 1 },
{ min: 2000001, max: 3000000, shardId: 2 },
{ min: 3000001, max: 4000000, shardId: 3 },
]);
const userShard = sharding.getShard(2500500); // 2Características:
- Queries de range eficientes dentro do shard
- Agrupamento natural de dados
- Pode criar hotspots (distribuição não uniforme)
- Re-sharding divide ranges existentes
Melhor para:
- Dados time-series (logs, métricas, eventos)
- Dados acessados por ranges (datas, IDs)
- Workloads de analytics
- Aplicações com padrões de crescimento de dados previsíveis
Sharding baseado em diretório
Um serviço de lookup mantém mapeamento entre chaves e shards. Isso fornece máxima flexibilidade mas adiciona complexidade operacional.
typescriptclass DirectoryBasedSharding {
private directory: Map<string, number> = new Map();
private shards: Map<number, DatabaseConnection> = new Map();
constructor() {
this.loadDirectory();
}
async getShard(key: string): Promise<DatabaseConnection> {
const shardId = this.directory.get(key);
if (!shardId) {
throw new Error(`Nenhum mapeamento de shard encontrado para chave: ${key}`);
}
const shard = this.shards.get(shardId);
if (!shardId) {
throw new Error(`Shard ${shardId} não encontrado`);
}
return shard;
}
async assignKeyToShard(key: string, shardId: number): Promise<void> {
this.directory.set(key, shardId);
await this.saveDirectory();
}
async migrateKey(key: string, fromShardId: number, toShardId: number): Promise<void> {
const fromShard = this.shards.get(fromShardId);
const toShard = this.shards.get(toShardId);
// Copia dados para novo shard
const data = await fromShard.query('SELECT * FROM data WHERE key = ?', [key]);
await toShard.insert('data', data);
// Atualiza diretório
this.directory.set(key, toShardId);
await this.saveDirectory();
// Deleta do shard antigo (após confirmação)
await fromShard.query('DELETE FROM data WHERE key = ?', [key]);
}
private async loadDirectory(): Promise<void> {
// Carrega diretório de armazenamento (Redis, banco de dados, etc.)
const entries = await this.loadFromStorage();
entries.forEach(([key, shardId]) => {
this.directory.set(key, shardId);
});
}
private async saveDirectory(): Promise<void> {
// Salva diretório de armazenamento
const entries = Array.from(this.directory.entries());
await this.saveToStorage(entries);
}
}Características:
- Flexibilidade máxima no posicionamento de dados
- Pode implementar regras complexas de roteamento
- Adiciona serviço de diretório como dependência
- Diretório se torna bottleneck de performance
Melhor para:
- Requisitos complexos de roteamento
- Aplicações multi-tenant com posicionamento customizado de tenant
- Distribuição geográfica de dados
- A/B testing de estratégias de sharding
Sharding geográfico
Dados são distribuídos baseados em localização geográfica, tipicamente para conformidade ou otimização de latência.
typescriptclass GeoBasedSharding {
private regionMapping: Map<string, string> = new Map(); // país -> região
private shardMapping: Map<string, number> = new Map(); // região -> shard
constructor() {
this.initializeRegionMapping();
this.initializeShardMapping();
}
getShard(userId: string, userCountry: string): number {
const region = this.regionMapping.get(userCountry);
if (!region) {
return this.getShardMapping().get('default') || 0;
}
const shardId = this.shardMapping.get(region);
if (shardId === undefined) {
return this.getShardMapping().get('default') || 0;
}
return shardId;
}
private initializeRegionMapping(): void {
this.regionMapping.set('US', 'us-east-1');
this.regionMapping.set('BR', 'sa-east-1');
this.regionMapping.set('DE', 'eu-central-1');
this.regionMapping.set('JP', 'ap-northeast-1');
// ... mais mapeamentos
}
private initializeShardMapping(): void {
this.shardMapping.set('us-east-1', 0);
this.shardMapping.set('sa-east-1', 1);
this.shardMapping.set('eu-central-1', 2);
this.shardMapping.set('ap-northeast-1', 3);
this.shardMapping.set('default', 0);
}
private getShardMapping(): Map<string, number> {
return this.shardMapping;
}
}
// Uso
const sharding = new GeoBasedSharding();
const userShard = sharding.getShard('user_123', 'BR'); // 1Características:
- Otimiza para latência
- Suporta requisitos de residência de dados
- Requer rastreamento de localização de usuário
- Pode criar tamanhos de shard não uniformes
Melhor para:
- Aplicações globais
- Requisitos de conformidade (GDPR, soberania de dados)
- Aplicações sensíveis à latência
- Implantações multi-região
Queries entre shards e operações de join
Sharding introduz complexidade para queries que abrangem múltiplos shards.
Transações distribuídas
typescriptclass DistributedTransaction {
private shards: Map<number, DatabaseConnection>;
private transactions: Map<number, any> = new Map();
async execute(query: string, shardIds: number[]): Promise<void> {
// Inicia transação em todos os shards participantes
for (const shardId of shardIds) {
const shard = this.shards.get(shardId);
const tx = await shard.beginTransaction();
this.transactions.set(shardId, tx);
}
try {
// Executa query em todos os shards
const results = await Promise.all(
shardIds.map(async (shardId) => {
const tx = this.transactions.get(shardId);
return tx.execute(query);
})
);
// Commit todas as transações
await Promise.all(
shardIds.map(async (shardId) => {
const tx = this.transactions.get(shardId);
return tx.commit();
})
);
return results;
} catch (error) {
// Rollback todas as transações
await Promise.all(
shardIds.map(async (shardId) => {
const tx = this.transactions.get(shardId);
try {
await tx.rollback();
} catch (rollbackError) {
console.error(`Rollback falhou no shard ${shardId}:`, rollbackError);
}
})
);
throw error;
}
}
}Joins em nível de aplicação
typescriptasync function getOrdersWithUserSharded(orderId: string): Promise<any> {
// Obtém order do shard de orders
const orderShardId = getShardIdForOrders(orderId);
const order = await shards.get(orderShardId).query(
'SELECT * FROM orders WHERE id = ?',
[orderId]
);
// Obtém user do shard de users
const userShardId = getShardIdForUsers(order.user_id);
const user = await shards.get(userShardId).query(
'SELECT * FROM users WHERE id = ?',
[order.user_id]
);
return { ...order, user };
}
async function getRecentOrdersForUser(userId: string, limit: number = 10): Promise<any[]> {
// Query shard de orders
const orderShardId = getShardIdForOrdersByUser(userId);
const orders = await shards.get(orderShardId).query(
'SELECT * FROM orders WHERE user_id = ? ORDER BY created_at DESC LIMIT ?',
[userId, limit]
);
return orders;
}Estratégias de desnormalização
Para minimizar queries entre shards, desnormalize dados:
typescript// Armazena dados redundantes para evitar joins entre shards
interface Order {
id: string;
user_id: string;
// Dados de usuário desnormalizados
user_name: string;
user_email: string;
total: number;
created_at: Date;
}
async function createOrderWithDenormalizedUser(orderData: OrderData, userId: string): Promise<Order> {
// Busca user do shard de users
const userShardId = getShardIdForUsers(userId);
const user = await shards.get(userShardId).query(
'SELECT id, name, email FROM users WHERE id = ?',
[userId]
);
// Cria order com dados de usuário desnormalizados
const orderShardId = getShardIdForOrders(orderData.id);
const order = await shards.get(orderShardId).insert('orders', {
id: orderData.id,
user_id: userId,
user_name: user.name,
user_email: user.email,
total: orderData.total,
created_at: new Date()
});
return order;
}Estratégias de re-sharding
Hash consistente
Hash consistente minimiza movimento de dados ao adicionar ou remover shards:
typescriptclass ConsistentHash {
private ring: Map<number, string> = new Map(); // hash -> shard
private sortedHashes: number[] = [];
private virtualNodes: number = 150; // Nós virtuais por shard físico
addShard(shardId: string): void {
// Adiciona nós virtuais para balanceamento de carga
for (let i = 0; i < this.virtualNodes; i++) {
const virtualNodeKey = `${shardId}:${i}`;
const hash = this.hashFunction(virtualNodeKey);
this.ring.set(hash, shardId);
this.sortedHashes.push(hash);
}
this.sortedHashes.sort((a, b) => a - b);
}
removeShard(shardId: string): void {
// Remove todos os nós virtuais para este shard
for (let i = 0; i < this.virtualNodes; i++) {
const virtualNodeKey = `${shardId}:${i}`;
const hash = this.hashFunction(virtualNodeKey);
this.ring.delete(hash);
const index = this.sortedHashes.indexOf(hash);
if (index > -1) {
this.sortedHashes.splice(index, 1);
}
}
}
getShard(key: string): string {
const hash = this.hashFunction(key);
// Encontra primeiro nó no ring com hash >= hash da chave
const index = this.findFirstNodeGreaterOrEqual(hash);
// Wrap-around se necessário
const nodeHash = this.sortedHashes[index % this.sortedHashes.length];
return this.ring.get(nodeHash) || '';
}
private findFirstNodeGreaterOrEqual(hash: number): number {
let left = 0;
let right = this.sortedHashes.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (this.sortedHashes[mid] >= hash) {
if (mid === 0 || this.sortedHashes[mid - 1] < hash) {
return mid;
}
right = mid - 1;
} else {
left = mid + 1;
}
}
return 0;
}
private hashFunction(key: string): number {
// Use uma boa função hash (MD5, SHA-1, ou murmurhash)
let hash = 0;
for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash);
}
}Estratégia de migração com escrita dupla
Gradualmente migra dados para novos shards:
typescriptclass DualWriteMigrator {
private sourceShard: DatabaseConnection;
private targetShard: DatabaseConnection;
private migrationComplete: boolean = false;
async migrateKey(key: string): Promise<void> {
// 1. Inicia escritas duplas
await this.enableDualWrites(key);
// 2. Backfill dados existentes
await this.backfillData(key);
// 3. Verifica consistência de dados
const isConsistent = await this.verifyConsistency(key);
if (isConsistent) {
// 4. Troca alvo de leitura
await this.switchReadTarget(key);
// 5. Para escritas no source
await this.disableSourceWrites(key);
}
}
async enableDualWrites(key: string): Promise<void> {
// Escreve em ambos source e target
this.sourceShard.insert('data', { key, value: 'write_enabled' });
this.targetShard.insert('data', { key, value: 'write_enabled' });
}
async backfillData(key: string): Promise<void> {
// Copia todos os dados existentes
const data = await this.sourceShard.query('SELECT * FROM data WHERE key = ?', [key]);
await this.targetShard.insert('data', data);
}
async verifyConsistency(key: string): Promise<boolean> {
const sourceData = await this.sourceShard.query('SELECT * FROM data WHERE key = ?', [key]);
const targetData = await this.targetShard.query('SELECT * FROM data WHERE key = ?', [key]);
return JSON.stringify(sourceData) === JSON.stringify(targetData);
}
async switchReadTarget(key: string): Promise<void> {
// Atualiza roteamento para ler do target
await this.updateRouting(key, 'target');
}
async disableSourceWrites(key: string): Promise<void> {
// Para de escrever no shard source
await this.sourceShard.query('UPDATE data SET write_enabled = false WHERE key = ?', [key]);
}
async updateRouting(key: string, target: string): Promise<void> {
// Atualiza configuração de roteamento
// Isso pode estar em config store, banco de dados, ou service discovery
}
}Considerações operacionais
Monitoramento e observabilidade
Sistemas shardados requerem monitoramento abrangente:
typescriptclass ShardedDatabaseMonitor {
private shards: Map<number, DatabaseConnection>;
private metrics: Map<number, ShardMetrics> = new Map();
async collectMetrics(): Promise<ShardedSystemMetrics> {
const shardMetrics = await Promise.all(
Array.from(this.shards.entries()).map(async ([shardId, shard]) => {
const metrics = await this.collectShardMetrics(shardId, shard);
this.metrics.set(shardId, metrics);
return { shardId, metrics };
})
);
return {
shards: shardMetrics,
system: this.calculateSystemMetrics(shardMetrics)
};
}
private async collectShardMetrics(shardId: number, shard: DatabaseConnection): Promise<ShardMetrics> {
const [
connectionCount,
queryLatency,
dataSize,
writeThroughput,
readThroughput
] = await Promise.all([
shard.getConnectionCount(),
shard.getAverageQueryLatency(),
shard.getDataSize(),
shard.getWriteThroughput(),
shard.getReadThroughput()
]);
return {
shardId,
connectionCount,
queryLatency,
dataSize,
writeThroughput,
readThroughput,
healthStatus: this.evaluateHealthStatus({
connectionCount,
queryLatency,
dataSize,
writeThroughput,
readThroughput
})
};
}
private evaluateHealthStatus(metrics: Partial<ShardMetrics>): string {
if (metrics.queryLatency > 1000) return 'degradado';
if (metrics.connectionCount > 10000) return 'degradado';
return 'saudável';
}
private calculateSystemMetrics(shardMetrics: Array<{shardId: number, metrics: ShardMetrics}>): SystemMetrics {
const totalConnections = shardMetrics.reduce((sum, { metrics }) => sum + metrics.connectionCount, 0);
const avgLatency = shardMetrics.reduce((sum, { metrics }) => sum + metrics.queryLatency, 0) / shardMetrics.length;
const totalDataSize = shardMetrics.reduce((sum, { metrics }) => sum + metrics.dataSize, 0);
return {
totalConnections,
averageLatency: avgLatency,
totalDataSize,
shardCount: shardMetrics.length,
healthyShards: shardMetrics.filter(({ metrics }) => metrics.healthStatus === 'saudável').length
};
}
}
interface ShardMetrics {
shardId: number;
connectionCount: number;
queryLatency: number;
dataSize: number;
writeThroughput: number;
readThroughput: number;
healthStatus: string;
}
interface SystemMetrics {
totalConnections: number;
averageLatency: number;
totalDataSize: number;
shardCount: number;
healthyShards: number;
}Backup e recuperação de desastre
Sharding complica backup e recuperação:
typescriptclass ShardedBackupStrategy {
private shards: Map<number, DatabaseConnection>;
async createConsistentBackup(backupId: string): Promise<void> {
// 1. Congela escritas em todos os shards
await this.freezeAllShards();
try {
// 2. Cria backup de cada shard
const backups = await Promise.all(
Array.from(this.shards.entries()).map(async ([shardId, shard]) => {
return this.createShardBackup(shardId, shard, backupId);
})
);
// 3. Registra metadados de backup
await this.recordBackupMetadata(backupId, backups);
console.log(`Backup ${backupId} concluído com sucesso`);
} finally {
// 4. Retoma escritas
await this.resumeAllShards();
}
}
private async createShardBackup(shardId: number, shard: DatabaseConnection, backupId: string): Promise<BackupMetadata> {
const timestamp = Date.now();
const backupLocation = `backups/${backupId}/shard_${shardId}_${timestamp}.sql`;
await shard.dump(backupLocation);
const checksum = await this.calculateChecksum(backupLocation);
return {
shardId,
backupId,
timestamp,
location: backupLocation,
checksum,
dataSize: await this.getFileSize(backupLocation)
};
}
async restoreFromBackup(backupId: string): Promise<void> {
const backupMetadata = await this.getBackupMetadata(backupId);
for (const shardBackup of backupMetadata.shards) {
await this.restoreShardBackup(shardBackup);
await this.verifyShardBackup(shardBackup);
}
console.log(`Restore do backup ${backupId} concluído com sucesso`);
}
private async verifyShardBackup(shardBackup: BackupMetadata): Promise<void> {
const actualChecksum = await this.calculateChecksum(shardBackup.location);
if (actualChecksum !== shardBackup.checksum) {
throw new Error(`Checksum mismatch para shard ${shardBackup.shardId}`);
}
}
}Tratamento de falhas e resiliência
typescriptclass ShardedDatabaseResilience {
private shards: Map<number, DatabaseConnection>;
private retryPolicy: RetryPolicy;
private circuitBreaker: CircuitBreaker;
async queryWithRetry(query: string, params: any[], shardId: number): Promise<any> {
return this.retryPolicy.execute(async () => {
if (this.circuitBreaker.isOpen(shardId)) {
throw new Error(`Circuit breaker do shard ${shardId} está aberto`);
}
try {
const result = await this.executeQuery(query, params, shardId);
this.circuitBreaker.recordSuccess(shardId);
return result;
} catch (error) {
this.circuitBreaker.recordFailure(shardId);
throw error;
}
});
}
async executeQuery(query: string, params: any[], shardId: number): Promise<any> {
const shard = this.shards.get(shardId);
if (!shard) {
throw new Error(`Shard ${shardId} não encontrado`);
}
return await shard.query(query, params);
}
async failoverToReplica(shardId: number): Promise<DatabaseConnection> {
const primaryShard = this.shards.get(shardId);
const replicaShardId = this.findAvailableReplica(shardId);
if (!replicaShardId) {
throw new Error(`Nenhuma réplica disponível para shard ${shardId}`);
}
// Redireciona tráfego para réplica
await this.redirectTraffic(shardId, replicaShardId);
return this.shards.get(replicaShardId);
}
private findAvailableReplica(shardId: number): number | null {
// Implementação para encontrar réplica disponível
// Isso pode ser baseado em health checks, load, ou proximidade geográfica
return null;
}
private async redirectTraffic(fromShardId: number, toShardId: number): Promise<void> {
// Atualiza roteamento para redirecionar tráfego
console.log(`Redirecionando tráfego do shard ${fromShardId} para ${toShardId}`);
}
}Framework de decisão
Avalie necessidade de sharding
Perguntas antes de shard:
- Você otimizou seu banco de dados de instância única?
- Índices devidamente ajustados?
- Performance de query otimizada?
- Caching implementado?
- Réplicas de leitura implantadas?
- Qual é sua restrição primária de escalabilidade?
- Throughput de escrita → Sharding ajuda
- Throughput de leitura → Replicação pode ser suficiente
- Capacidade de armazenamento → Sharding ou armazenamento de objetos
- Qual é sua trajetória de crescimento de dados?
- Crescimento previsível → Sharding baseado em range
- Crescimento imprevisível → Sharding baseado em hash
- Quais são seus padrões de query?
- Principalmente queries por ponto → Sharding baseado em hash
- Queries de range importantes → Sharding baseado em range
- Joins complexos → Considere otimização em nível de aplicação
Escolha estratégia de sharding
| Estratégia | Melhor Para | Trade-offs |
|---|---|---|
| Baseado em hash | Dados centrados em usuário, alta cardinalidade | Distribuição uniforme, queries de range difíceis |
| Baseado em range | Time-series, analytics, crescimento previsível | Queries de range eficientes, hotspots potenciais |
| Baseado em diretório | Roteamento complexo, multi-tenant | Flexibilidade máxima, dependência de diretório |
| Geográfico | Apps globais, requisitos de conformidade | Otimização de latência, tamanhos de shard não uniformes |
Conclusão
Sharding de banco de dados é uma técnica poderosa para escalar aplicações intensivas em dados, mas introduz complexidade significativa. A decisão de shard deve ser baseada em evidência clara de que escalamento de instância única é insuficiente, não como otimização prematura.
Sharding bem-sucedido requer planejamento cuidadoso, monitoramento contínuo e práticas operacionais robustas. Comece com a estratégia de sharding mais simples que atenda suas necessidades, e evolua conforme os requisitos mudam. O objetivo não é alcançar pureza arquitetural—é construir um sistema que escala de forma previsível permanecendo mantível.
Pergunta prática de fechamento: Qual é o gargalo de escalabilidade primário em sua implantação atual de banco de dados, e qual seria o impacto de uma arquitetura shardada nas capacidades operacionais de sua equipe?
Planejando uma estratégia de escalabilidade de banco de dados e precisa de orientação especializada sobre arquitetura de sharding e implementação? Fale com especialistas da Imperialis sobre projetar uma estratégia de sharding que combine com a escala, requisitos e capacidades de sua equipe.
Fontes
- Database Sharding: A Comprehensive Guide — Cockroach Labs
- MongoDB Sharding Documentation — documentação MongoDB
- PostgreSQL Citus Documentation — documentação Citus
- Distributed Systems: Principles and Paradigms — Tanenbaum & Van Steen
- Designing Data-Intensive Applications — Martin Kleppmann