Шардинг: горизонтальное разделение данных между узлами
Предпосылки: репликация (копии данных, failover, кворум — почему копии не решают проблему ёмкости).
← Репликация | CAP-теорема →
Платформа из сценария репликации выросла: 50 000 заказов в день, каталог на 10 миллионов товаров, primary упирается в диск и CPU. Добавление реплик не помогает — каждая хранит полный набор данных, а все записи идут на один узел. Репликация масштабирует чтение, но записи и ёмкость ограничены одним сервером.
Шардинг: разделить данные, а не копировать
Шардинг (от shard — «осколок») — разделение набора данных на части, каждая из которых живёт на своём узле. В отличие от репликации, где каждый узел хранит всё, при шардинге каждый узел хранит только свою часть.
┌──────────────────────────────┐
│ Router / App │
└──────┬───────┬───────┬───────┘
│ │ │
┌─────v─┐ ┌───v───┐ ┌─v─────┐
│Shard A│ │Shard B│ │Shard C│
│users │ │users │ │users │
│1–33% │ │34–66% │ │67–100%│
└───────┘ └───────┘ └───────┘Это решает две задачи: ёмкость — хранить больше данных, чем помещается на один узел, и пропускную способность записи — писать параллельно в разные шарды, если нагрузка распределена по ключу. Запрос приходит в приложение (или промежуточный proxy), которое вычисляет целевой шард по ключу и направляет запрос на нужный узел.
Цена шардинга
Шардинг — не бесплатное масштабирование. То, что раньше делалось «внутри одной базы», теперь пересекает границы узлов.
Распределённые запросы. Запрос, который не содержит shard key, должен опросить все шарды и агрегировать результаты (scatter-gather). «Покажи заказы пользователя 42» попадает в один шард. «Посчитай все заказы за сегодня» — при 10 шардах это 10 сетевых вызовов вместо одного, а итоговая latency определяется самым медленным шардом.
Утрата глобальных гарантий. Уникальность, внешние ключи, атомарность транзакций — всё это работает внутри одного шарда. Между шардами каждую гарантию нужно проектировать отдельно: распределённая транзакция требует координации (two-phase commit: координатор сначала спрашивает все узлы «готовы?», затем отдаёт команду «фиксируй»), которая замораживает строки на нескольких узлах до подтверждения; глобальная уникальность требует отдельного сервиса или схемы генерации ID.
Операционная сложность. Вместо одного сервера — десятки. Мониторинг, бэкапы, миграции схемы, обновление версий — всё умножается на количество шардов.
Shard key: главное решение в шардинге
Все три проблемы — scatter-gather, утрата гарантий, операционная сложность — определяются одним решением: как именно данные разделяются между шардами. Шардинг почти всегда строится вокруг shard key — значения, по которому запрос направляется на конкретный шард. Выбор shard key определяет, как распределяется нагрузка, какие запросы будут быстрыми, а какие — дорогими.
Платформа хранит заказы и чаще всего показывает пользователю «мои заказы». Shard key = user_id: любой запрос про конкретного пользователя попадает в один шард. user_id присутствует почти в каждом запросе — значит, большинство запросов обойдётся без scatter-gather.
orders(user_id, order_id, created_at, ...)
3 шарда, распределение через hash(user_id) % 3:
shard 0: hash(user_id) % 3 == 0
shard 1: hash(user_id) % 3 == 1
shard 2: hash(user_id) % 3 == 2
"Покажи заказы user_id=42" → shard(hash(42) % 3) → один шард
"Посчитай заказы за сегодня" → user_id нет → scatter по всем шардамХеширование (hash(user_id) % N вместо user_id % N) обеспечивает близкое к равномерному распределение даже если значения user_id не случайны (например, последовательные ID). Миллионы пользователей дают достаточно различных значений, чтобы нагрузка распределялась между шардами без перекосов. И user_id стабилен — пользователь не меняет свой ID, поэтому строки не нужно перемещать между шардами.
Но при неудачном выборе ключа эти свойства рушатся. Если шардировать по country_code в глобальном сервисе, где 70% пользователей из одной страны — один шард получит 70% нагрузки, остальные будут простаивать. А если шардировать по email, при смене адреса все заказы пользователя придётся физически переносить на другой шард.
Resharding: добавить узел — значит переместить данные
Платформа растёт, 3 шарда уже не справляются. Нужен четвёртый. Но hash(user_id) % 3 и hash(user_id) % 4 дают разные результаты для большинства ключей — значительная часть данных должна переехать на новые шарды.
Resharding — тяжёлая операция: нужно копировать данные, поддерживать корректную маршрутизацию во время миграции (запросы к перемещаемым данным должны находить их и на старом, и на новом месте) и не терять записи, которые приходят в процессе.
Consistent hashing минимизирует объём миграции: при добавлении узла перемещается только ~1/N данных вместо большинства. Подробнее этот механизм описан в контексте распределённого кэша.
Шардинг + репликация: два слоя одного решения
На практике шардинг и репликация не взаимоисключающие решения — это два слоя одной архитектуры. Шардинг решает ёмкость и запись, репликация внутри каждого шарда — доступность и масштабирование чтения.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Shard A │ │ Shard B │ │ Shard C │
│ primary │ │ primary │ │ primary │
│ replica 1 │ │ replica 1 │ │ replica 1 │
│ replica 2 │ │ replica 2 │ │ replica 2 │
└─────────────┘ └─────────────┘ └─────────────┘Платформа с 3 шардами, каждый реплицирован на 2 реплики: 9 узлов суммарно. Primary шарда принимает записи, реплики обслуживают чтения и готовы к failover. Потеря одного узла не влияет на систему — реплика берёт на себя роль primary для своего шарда.
Конкретные реализации этой схемы: шардирование в PostgreSQL (application-level routing, Citus, FDW) и Redis Cluster (16 384 слота, CRC16, MOVED/ASK).
Sources
- Kleppmann, M. Designing Data-Intensive Applications: Partitioning. https://dataintensive.net/
← Репликация | CAP-теорема →