Cloud e plataforma

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.

13/03/20269 min de leituraCloud
Estratégias de Sharding de Banco de Dados: Escalabilidade Horizontal para Sistemas em Produção

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:

AspectoReplicaçãoSharding
PropósitoAlta disponibilidade, escalabilidade de leituraEscalabilidade de escrita, distribuição de dados
DadosCópia completa em cada instânciaSubconjunto particionado em cada instância
Capacidade de escritaLimitada pelo primárioEscala com contagem de shards
Capacidade de leituraEscala com contagem de réplicasEscala com contagem de shards
ComplexidadeBaixaAlta
ConsistênciaForte (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'); // 3

Caracterí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); // 2

Caracterí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'); // 1

Caracterí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:

  1. Você otimizou seu banco de dados de instância única?
  • Índices devidamente ajustados?
  • Performance de query otimizada?
  • Caching implementado?
  • Réplicas de leitura implantadas?
  1. 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
  1. Qual é sua trajetória de crescimento de dados?
  • Crescimento previsível → Sharding baseado em range
  • Crescimento imprevisível → Sharding baseado em hash
  1. 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égiaMelhor ParaTrade-offs
Baseado em hashDados centrados em usuário, alta cardinalidadeDistribuição uniforme, queries de range difíceis
Baseado em rangeTime-series, analytics, crescimento previsívelQueries de range eficientes, hotspots potenciais
Baseado em diretórioRoteamento complexo, multi-tenantFlexibilidade máxima, dependência de diretório
GeográficoApps globais, requisitos de conformidadeOtimizaçã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

Leituras relacionadas