← Назад к вопросам

Когда нужно использовать шардирование?

2.3 Middle🔥 181 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Шардирование БД: когда и как его использовать

Шардирование (Sharding) — это метод горизонтального масштабирования базы данных путем разделения данных между несколькими независимыми экземплярами. Это одна из самых сложных операций в системном дизайне и требует тщательного анализа.

Что такое шардирование

Шардирование делит данные между несколькими физическими базами данных (шардами) на основе ключа шардирования:

// Упрощенный пример
public class ShardRouter {
    private static final int SHARD_COUNT = 4;
    
    public int getShardId(long userId) {
        // Распределяем пользователей по 4 шардам
        return (int) (userId % SHARD_COUNT);
    }
    
    public Connection getConnection(long userId) {
        int shardId = getShardId(userId);
        
        // Пользователь 1 → shard 1 (db1.example.com)
        // Пользователь 2 → shard 2 (db2.example.com)
        // Пользователь 3 → shard 3 (db3.example.com)
        // Пользователь 4 → shard 0 (db4.example.com)
        
        return getShardConnection(shardId);
    }
}

Когда использовать шардирование

1. Масштабирование по объему данных превышает возможности одного сервера

// Когда одна БД больше не вмещает данные
public class DataScalingMetrics {
    // Сценарий: 10 млн пользователей, каждый занимает ~10 KB
    // Общий объем: 10M × 10KB = 100 GB
    
    // Если одна машина может вместить max 500 GB:
    // 100 GB / 500 GB = 0.2 → Еще не критично, просто больший RAM
    
    // Но через год: 100M пользователей × 10KB = 1 TB
    // 1 TB > 500 GB → Нужно шардирование
}

Признаки:

  • Размер БД приближается к лимитам одной машины
  • Резервная копия становится проблемой (слишком долго, слишком много места)
  • Невозможно купить еще одну машину (экономия?)

2. IOPS (операции ввода-вывода в секунду) достигает лимита одного сервера

// Когда одна БД не может обработать нагрузку
public class IOPSBottleneck {
    public static void main(String[] args) {
        // Каждый пользователь генерирует ~100 запросов в день
        // 1M активных пользователей = 100M запросов в день
        // = 100M / 86400 = ~1160 запросов в секунду
        
        // Современный SSD может: ~40,000 IOPS
        // Эффективно используется: ~30,000 IOPS (overhead на ОС)
        // С репликацией (3x): 30,000 / 3 = 10,000 эффективных IOPS
        // 1160 QPS все еще в порядке
        
        // Но для 10M активных пользователей:
        // 1160 × 10 = 11,600 QPS
        // 11,600 > 10,000 → Нужно шардирование
    }
}

Признаки:

  • CPU на базе постоянно на 80%+
  • Диск I/O утилизация максимальная
  • Query latency растет несмотря на оптимизацию
  • Кеширование (Redis) уже на максимуме

3. Сеть (network bandwidth) является bottleneck

// Когда объем передаваемых данных критичен
public class NetworkBottleneck {
    public static void main(String[] args) {
        // Каждый запрос к БД: ~5 KB ответа
        // 10,000 запросов в секунду = 50 MB/s
        
        // Network линк: 1 Gbps = 125 MB/s
        // Используется: 50 MB/s / 125 MB/s = 40%
        // Все еще в порядке
        
        // Но с репликацией (write → master + 2 replicas):
        // 50 MB/s × 3 = 150 MB/s
        // 150 MB/s > 125 MB/s → Сеть перегружена
    }
}

Стратегии шардирования

1. Range-based Sharding (по диапазонам)

public class RangeSharding {
    public int getShardId(long userId) {
        if (userId < 1_000_000) {
            return 0;  // Shard 0: user_ids 0 - 999,999
        } else if (userId < 2_000_000) {
            return 1;  // Shard 1: user_ids 1,000,000 - 1,999,999
        } else {
            return 2;  // Shard 2: user_ids 2,000,000+
        }
    }
}

// Плюсы:
// - Простота понимания
// - Легко добавлять новые шарды

// Минусы:
// - Hot spots (новые пользователи идут в последний шард)
// - Сложно переквалибровать (rebalance)

2. Hash-based Sharding (по хешу)

public class HashSharding {
    private static final int SHARD_COUNT = 4;
    
    public int getShardId(long userId) {
        return Math.abs(userId.hashCode()) % SHARD_COUNT;
    }
}

// Плюсы:
// - Равномерное распределение
// - Нет hot spots

// Минусы:
// - При добавлении шардов нужен rebalancing (дорого!)
// - Сложно масштабировать

3. Consistent Hashing (консистентное хеширование)

public class ConsistentHashSharding {
    private final NavigableMap<Integer, Integer> ring = new TreeMap<>();
    
    public ConsistentHashSharding(List<Integer> shardIds) {
        for (int shardId : shardIds) {
            // Каждый шард имеет несколько виртуальных узлов
            for (int i = 0; i < 150; i++) {
                ring.put(hash(shardId + "-" + i), shardId);
            }
        }
    }
    
    public int getShardId(long userId) {
        int hash = hash(userId);
        SortedMap<Integer, Integer> tailMap = ring.tailMap(hash);
        int nodeHash = tailMap.isEmpty() ? ring.firstKey() : tailMap.firstKey();
        return ring.get(nodeHash);
    }
    
    private int hash(Object key) {
        return Math.abs(key.hashCode());
    }
}

// Плюсы:
// - При добавлении/удалении шарда нужен rebalancing только подмножества данных
// - Хорошо работает с динамическим масштабированием

// Минусы:
// - Сложность реализации
// - Нужно кешировать ring на клиентах

Практический пример в Java

@Service
public class ShardedUserService {
    private final ShardConnectionPool connectionPool;
    private final ShardRouter router;
    
    public User getUser(long userId) {
        int shardId = router.getShardId(userId);
        Connection conn = connectionPool.getConnection(shardId);
        
        String sql = "SELECT * FROM users WHERE id = ?";
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setLong(1, userId);
        
        ResultSet rs = stmt.executeQuery();
        if (rs.next()) {
            return mapToUser(rs);
        }
        return null;
    }
    
    public void createUser(User user) {
        int shardId = router.getShardId(user.getId());
        Connection conn = connectionPool.getConnection(shardId);
        
        String sql = "INSERT INTO users (id, name, email) VALUES (?, ?, ?)";
        PreparedStatement stmt = conn.prepareStatement(sql);
        stmt.setLong(1, user.getId());
        stmt.setString(2, user.getName());
        stmt.setString(3, user.getEmail());
        
        stmt.executeUpdate();
    }
}

Проблемы шардирования и как их решать

1. Мульти-шардовые запросы (Scatter-Gather)

// ❌ Проблема: нельзя просто SELECT * FROM users ORDER BY created_at
// Нужно опросить все шарды и собрать результаты

public List<User> getAllUsersSortedByCreatedDate() {
    List<User> allUsers = new ArrayList<>();
    
    for (int shardId = 0; shardId < SHARD_COUNT; shardId++) {
        Connection conn = connectionPool.getConnection(shardId);
        String sql = "SELECT * FROM users ORDER BY created_at";
        PreparedStatement stmt = conn.prepareStatement(sql);
        
        ResultSet rs = stmt.executeQuery();
        while (rs.next()) {
            allUsers.add(mapToUser(rs));
        }
    }
    
    // Сортируем на клиенте
    allUsers.sort((u1, u2) -> u2.getCreatedAt().compareTo(u1.getCreatedAt()));
    return allUsers;
}

// Это сложно и медленно

2. Изменение ключа шардирования (rebalancing)

// ❌ Слишком дорого менять ключ шардирования
// Например: был по user_id, хотим по tenant_id

// ✅ Решение: Double-Write Syndrome
public void migrateShardingKey() {
    // 1. Добавляем новую таблицу с новым ключом шардирования
    // 2. Пишем в обе таблицы параллельно
    // 3. Мигрируем все исторические данные
    // 4. Переключаем читают на новую таблицу
    // 5. Удаляем старую таблицу
}

Когда НЕ использовать шардирование

  • ❌ Для маленьких проектов (< 100 GB данных)
  • ❌ Если можно решить проблему вертикальным масштабированием
  • ❌ Если нет четкого ключа шардирования
  • ❌ Если часто нужны мульти-шардовые запросы

Альтернативы шардированию

// 1. Вертикальное масштабирование (больше CPU/RAM)
ReplaceOldServer(old_16_core, new_128_core);

// 2. Кеширование (Redis, Memcached)
Cache<userId, User> cache = new RedisCache<>();

// 3. Архивирование старых данных
archiveDataOlderThan(LocalDate.now().minusYears(1));

// 4. Чтение реплик (read replicas)
Connection readConn = connectionPool.getReadReplica();

Заключение

Используй шардирование когда:

  1. Объем данных одной БД превышает 500GB-1TB
  2. IOPS нагрузка достигает лимитов одного сервера (обычно 30,000+)
  3. Сетевая полоса полностью занята
  4. Вертикальное масштабирование исчерпано
  5. Есть естественный ключ для распределения (user_id, tenant_id)

Помни: Шардирование — это последний вариант масштабирования. Сначала оптимизируй запросы, добавь кеширование, затем рассмотри read replicas. Только потом переходи к шардированию.

Когда нужно использовать шардирование? | PrepBro