Що таке шардування 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 працюють так само, як звичайні» (Вони вимагають викачування даних мережею на координатор і призводять до деградації продуктивності).