Что такое шардирование 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. Фундаментальные проблемы шардирования
- Scatter-Gather (Рассеивание-Сборка): Если запрос не содержит Shard Key (например,
SELECT * FROM orders WHERE status = 'SHIPPED'), координатор обязан отправить запрос на все шарды параллельно, дождаться всех ответов, объединить и отсортировать их в памяти. - Утрата целостности (No Cross-Shard Foreign Keys): База данных на Шарде 1 не может проверить внешний ключ
FOREIGN KEYк таблице на Шарде 2. Контроль связей переносится целиком в код приложения. - Межшардовые транзакции: Если требуется обновить данные на Шарде 1 и Шарде 2 атомарно, классический
BEGIN TRANSACTIONбессилен. Требуется протокол 2PC (XA) или паттерн Saga.
🔴 Senior Level
1. Генерация первичных ключей в шардированных БД (ID Generation)
В шардированной базе нельзя использовать AUTO_INCREMENT или SERIAL, так как на разных серверах неизбежно возникнут коллизии (на Шарде 1 появится id=42 и на Шарде 2 появится id=42).
Промышленные решения:
- Twitter Snowflake (64-битный ID):
[1 бит: знак] [41 бит: timestamp в мс] [10 бит: ID ноды/шарда] [12 бит: инкремент sequence]Плюсы: Идентификаторы глобально уникальны, упорядочены по времени (Time-ordered), помещаются в 64-битный
BIGINTи отлично индексируются в B-Tree. - UUIDv7: Новый стандарт UUID, содержащий 48-битный timestamp в старших битах. Не фрагментирует индексы B-Tree в отличие от полностью случайного UUIDv4.
- 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 (Миграция без простоя)
Процесс разделения переполненного шарда на живой продакшен-базе:
- Provisioning: Развертывание нового целевого шарда.
- Dual-Write: Приложение настраивается на запись изменений в старый и новый шарды одновременно.
- Backfill: Фоновый batch-процесс копирует исторические данные со старого шарда на новый.
- Catch-Up & Verification: Сверка контрольных сумм и применение логов изменений (CDC / Debezium).
- Read Switch: Переключение запросов на чтение на новый шард.
- Stop Dual-Write: Отключение записи в старый шард и освобождение дискового пространства.
4 Tricky Questions
1. Как решить проблему Hotspot / Celebrity Problem, когда один пользователь генерирует 80% нагрузки на свой шард?
Ответ:
Если ключ шардирования — user_id, а среди пользователей есть звезда с 50 миллионами подписчиков, шард с этой записью будет испытывать запредельную нагрузку (CPU, IOPS, сетевой трафик), в то время как остальные шарды простаивают.
Решения:
- Salted Shard Key: Для популярных сущностей ключ дополняется случайной солью:
key = user_id + "_" + random(1..10). Данные этого пользователя равномерно размазываются по 10 разным шардам. При чтении выполняется параллельный Scatter-Gather опрос этих 10 шардов. - Dedicated Shard: Выделение крупных тенантов / знаменитостей на персональные физические серверы с помощью Directory-based маршрутизации.
- Агрессивное кэширование: Вынос горячих данных на чтение в 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 работают так же, как обычные» (Они требуют выкачивания данных по сети на координатор и приводят к деградации производительности).