🏗️ Раздел 17 · Вопрос #10

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

Представьте многотомную энциклопедию:


🟢 Junior Level

Шардирование (Sharding / Горизонтальное секционирование) — это архитектурный метод разделения одной логической базы данных на несколько физически независимых серверов (шардов), где каждый сервер хранит только свою уникальную часть данных.

                        [Шардированная база данных]
                                     │
          ┌──────────────────────────┼──────────────────────────┐
          ▼                          ▼                          ▼
     [Шард 1]                   [Шард 2]                   [Шард 3]
Пользователи A–H           Пользователи I–P           Пользователи Q–Z
(Сервер 1: 30 млн)         (Сервер 2: 30 млн)         (Сервер 3: 30 млн)

Разница: Репликация vs Шардирование

  • Репликация (Read Replicas): Каждый сервер хранит 100% полную копию всей базы данных. Используется для повышения отказоустойчивости и распределения нагрузки на чтение (SELECT). Нагрузку на запись (INSERT / UPDATE) репликация не снижает.
  • Шардирование: Серверы хранят разные подмножества данных. Используется, когда общий объём базы данных или входящий поток записи физически не помещается на один (даже самый мощный) сервер.

Аналогия из жизни:

Представьте многотомную энциклопедию:

  • Если бы вся энциклопедия была в одной книге толщиной в 10 метров, ни один человек не смог бы её поднять или перевернуть страницу.
  • Разделив её на 26 отдельных томов (шардов), 26 человек могут одновременно читать и делать записи в разных томах, не создавая очередей и блокировок.

🟡 Middle Level

1. Выбор ключа шардирования (Shard Key) и стратегии маршрутизации

Shard Key (Ключ шардирования) — это поле сущности (например, user_id или tenant_id), по значению которого маршрутизатор определяет целевой физический сервер для выполнения SQL-запроса.

Стратегия Принцип работы Плюсы Минусы / Риски
Range-based (По диапазону) ID 1..1M -> Shard 1, 1M..2M -> Shard 2 Быстрые Range-запросы (BETWEEN date1 AND date2) 🔴 Hot Spotting: Все новые записи идут строго в последний шард!
Hash-based (По хешу ключа) shard_id = hash(user_id) % N 🟢 Идеально равномерное распределение нагрузки Запросы диапазонов требуют опроса всех $N$ шардов
Directory-based (Справочник) Внешняя lookup-таблица: tenant_123 -> Shard 4 Гибкость: VIP-клиентов можно изолировать на выделенные ноды Таблица маршрутизации становится SPOF и узким местом по I/O

2. Реализация динамической маршрутизации в Spring (AbstractRoutingDataSource)

В Java-приложениях клиентское шардирование часто настраивается через подмену источника данных:

public class ShardedRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // Контекст текущего потока определяет Shard Key (например, TenantContext)
        Long customerId = ShardContextHolder.getCustomerId();
        if (customerId == null) {
            return "shard_default";
        }
        // Хеширование: распределение по 4 шардам
        int shardIndex = Math.abs(customerId.hashCode() % 4);
        return "shard_" + shardIndex;
    }
}

3. Фундаментальные проблемы шардирования

  1. Scatter-Gather (Рассеивание-Сборка): Если запрос не содержит Shard Key (например, SELECT * FROM orders WHERE status = 'SHIPPED'), координатор обязан отправить запрос на все шарды параллельно, дождаться всех ответов, объединить и отсортировать их в памяти.
  2. Утрата целостности (No Cross-Shard Foreign Keys): База данных на Шарде 1 не может проверить внешний ключ FOREIGN KEY к таблице на Шарде 2. Контроль связей переносится целиком в код приложения.
  3. Межшардовые транзакции: Если требуется обновить данные на Шарде 1 и Шарде 2 атомарно, классический BEGIN TRANSACTION бессилен. Требуется протокол 2PC (XA) или паттерн Saga.

🔴 Senior Level

1. Генерация первичных ключей в шардированных БД (ID Generation)

В шардированной базе нельзя использовать AUTO_INCREMENT или SERIAL, так как на разных серверах неизбежно возникнут коллизии (на Шарде 1 появится id=42 и на Шарде 2 появится id=42).

Промышленные решения:

  1. Twitter Snowflake (64-битный ID):
    [1 бит: знак] [41 бит: timestamp в мс] [10 бит: ID ноды/шарда] [12 бит: инкремент sequence]
    

    Плюсы: Идентификаторы глобально уникальны, упорядочены по времени (Time-ordered), помещаются в 64-битный BIGINT и отлично индексируются в B-Tree.

  2. UUIDv7: Новый стандарт UUID, содержащий 48-битный timestamp в старших битах. Не фрагментирует индексы B-Tree в отличие от полностью случайного UUIDv4.
  3. Centralized Sequence Allocation: Выделение блоков ID пачками (Hi/Lo алгоритм) через Redis или выделенную таблицу в координаторе.

2. Consistent Hashing (Консистентное хеширование) и виртуальные ноды

При наивном хешировании hash(key) % N: При увеличении числа шардов с 3 до 4 старая формула перенаправит 75% всех существующих ключей на другие серверы. Придется остановить систему и переместить петабайты данных.

Consistent Hashing решает проблему:

                     Кольцо хешей [0 ... 2^32 - 1]
                                 [0]
                         .----------------.
                     .'     Node A (V1)     '.
                   .'                          '.
             Node C (V2)                     Node B (V1)
                   '.                          .'
                     '.     Node A (V2)      .'
                         '----------------'
  • Пространство хешей закольцовано от $0$ до $2^{32}-1$.
  • При добавлении нового шарда $N+1$ переезжает только $\frac{1}{N+1}$ часть данных исключительно от соседнего узла по кольцу.
  • Virtual Nodes (vnodes): Чтобы избежать неравномерного распределения («тяжелых» участков кольца), каждый физический сервер представлен на кольце десятками виртуальных точек (Node_A_1, Node_A_2, Node_A_3).

3. Zero-Downtime Resharding (Миграция без простоя)

Процесс разделения переполненного шарда на живой продакшен-базе:

  1. Provisioning: Развертывание нового целевого шарда.
  2. Dual-Write: Приложение настраивается на запись изменений в старый и новый шарды одновременно.
  3. Backfill: Фоновый batch-процесс копирует исторические данные со старого шарда на новый.
  4. Catch-Up & Verification: Сверка контрольных сумм и применение логов изменений (CDC / Debezium).
  5. Read Switch: Переключение запросов на чтение на новый шард.
  6. Stop Dual-Write: Отключение записи в старый шард и освобождение дискового пространства.

4 Tricky Questions

1. Как решить проблему Hotspot / Celebrity Problem, когда один пользователь генерирует 80% нагрузки на свой шард?

Ответ: Если ключ шардирования — user_id, а среди пользователей есть звезда с 50 миллионами подписчиков, шард с этой записью будет испытывать запредельную нагрузку (CPU, IOPS, сетевой трафик), в то время как остальные шарды простаивают. Решения:

  1. Salted Shard Key: Для популярных сущностей ключ дополняется случайной солью: key = user_id + "_" + random(1..10). Данные этого пользователя равномерно размазываются по 10 разным шардам. При чтении выполняется параллельный Scatter-Gather опрос этих 10 шардов.
  2. Dedicated Shard: Выделение крупных тенантов / знаменитостей на персональные физические серверы с помощью Directory-based маршрутизации.
  3. Агрессивное кэширование: Вынос горячих данных на чтение в Redis/Memcached.

2. В чем опасность глубокой пагинации (ORDER BY created_at LIMIT 10 OFFSET 100000) в шардированной СУБД?

Ответ: При запросе без Shard Key координатор должен опросить все шарды. Чтобы вернуть страницу со смещением 100 000, координатор обязан запросить с каждого из $N$ шардов по $100\,010$ записей! При 10 шардах координатор выгрузит по сети более 1 000 000 строк, отсортирует их все в своей оперативной памяти, отбросит первые 100 000 и отдаст клиенту 10 штук. Это гарантированно вызовет OutOfMemoryError или огромный latency spike. Решение: Использование Keyset Pagination (Seek Method) вместо OFFSET: WHERE created_at < :last_seen_timestamp AND id < :last_seen_id ORDER BY created_at DESC LIMIT 10.

3. Почему в шардированных базах данных крайне сложно реализовать глобальные уникальные ограничения (UNIQUE constraint) не по Shard Key?

Ответ: Реляционная СУБД проверяет UNIQUE через локальный B-Tree индекс на конкретном сервере. Если таблица шардирована по user_id, а уникальным должно быть поле email:

  • Чтобы проверить, свободен ли email = "test@domain.com" при регистрации, СУБД на Шарде 1 не знает, есть ли такая строка на Шарде 2.
  • Пришлось бы делать распределенную блокировку всех шардов перед каждой вставкой. Решение: Создание отдельного сервиса/таблицы сопоставления (Lookup Index): email -> user_id, шардированного по hash(email).

4. В чем разница между шардированием на уровне приложения (Client-Side) и на уровне СУБД (Distributed SQL / NewSQL)?

Ответ:

  • Шардирование на уровне приложения (ShardingSphere, Hibernate, Custom Router): Приложение само знает про шарды и управляет пулами соединений. СУБД — обычные независимые инстансы PostgreSQL/MySQL. Плюсы: полный контроль, дешевле. Минусы: сложный код приложения, отсутствие поддержки кросс-шардовых транзакций и перебалансировки из коробки.
  • Шардирование на уровне СУБД (CockroachDB, YugabyteDB, Vitess, Citus): Для приложения база выглядит как единый монолитный сервер. СУБД сама распределяет шарды (Ranges/Tablets) по нодам с помощью распределенного консенсуса (Raft/Paxos), автоматически делает ребалансировку и распределенные транзакции. Минусы: более высокие требования к ресурсам и сетевому latency между нодами.

🎯 Шпаргалка для интервью

Главное:

  • Шардирование — горизонтальное деление данных по строкам на независимые серверы для масштабирования записи и объема данных.
  • Shard Key — поле, определяющее адрес сервера (требуется высокая кардинальность и равномерное распределение).
  • Стратегии: Hash-based (самая популярная и равномерная), Range-based (риск Hot Spots), Directory-based (максимальная гибкость).
  • Consistent Hashing защищает от необходимости переносить всю базу при добавлении новых серверов.
  • ID в шардированных системах: Twitter Snowflake или UUIDv7 (автоинкремент базы непригоден из-за коллизий).
  • Главные проблемы: Scatter-Gather запросы, потеря внешних ключей (FK), сложность межшардовых транзакций и пагинации.

Красные флаги (Чего говорить НЕЛЬЗЯ):

  • ❌ «Шардирование нужно подключать в проект с первого дня» (Шардирование внедряют только при исчерпании лимитов вертикального масштабирования, от 1–2 ТБ или сотен тысяч TPS).
  • ❌ «Для шардированной базы мы используем автоинкрементные ID в таблицах» (Это приведет к дубликатам ID на разных шардах).
  • ❌ «Межшардовые JOIN работают так же, как обычные» (Они требуют выкачивания данных по сети на координатор и приводят к деградации производительности).

Связанные темы: