Шардирование, партиционирование и согласованное хеширование

Когда данные не влезают на одну машину, их режут на части и раскладывают по узлам.

Шардирование (sharding) — разбиение данных на части (шарды) и распределение их по разным серверам, чтобы каждый хранил и обслуживал только свою долю.

Зачем это нужно

Один сервер ограничен по диску, памяти и пропускной способности. Когда данных терабайты, а запросов тысячи в секунду, их делят между узлами. Каждый шард — это самостоятельная база со своей долей данных. Запрос идёт только на нужный шард, а не на все.

Стратегии разбиения

СтратегияКак делимМинус
По диапазону (range)id 1–1М → шард A, 1М–2М → Bперекос: новые записи бьют в один шард
По хешу ключаshard = hash(key) % Nпри смене N переезжает почти всё
По справочнику (directory)таблица «ключ → шард»справочник — новая точка отказа

Выбор ключа шардирования

Ключ должен равномерно распределять нагрузку и совпадать с тем, по чему вы чаще всего ищете. Шардирование ленты по user_id хорошо, если запросы всегда «дай данные пользователя X». Плохой ключ порождает «горячий» шард — узел, на который приходится непропорционально много трафика (например, шард со знаменитостью-миллионником).

Проблема наивного хеша

Формула shard = hash(key) % N кажется удобной, но у неё фатальный изъян: при изменении числа узлов N (добавили или убрали сервер) меняется почти у всех ключей. Это значит массовый переезд данных и обвал кэша.

Было 4 узла:  hash(key) % 4
Стало 5 узлов: hash(key) % 5
→ ~80% ключей сменили узел = катастрофа

Согласованное хеширование (consistent hashing)

Решение — представить пространство хешей как кольцо (например, 0 … 2³²−1). И ключи, и узлы хешируются в точки на кольце. Ключ принадлежит первому узлу по часовой стрелке от своей точки.

Кольцо хешей (по часовой стрелке):

   [Node A]----key1----[Node B]----key2----[Node C]----key3----(назад к A)

key1 → ближайший по часовой = Node B
key3 → ближайший по часовой = Node A (через замыкание кольца)

Магия в том, что при добавлении узла переезжает только часть ключей между новым узлом и его соседом, а не все. При удалении — аналогично: ключи ушедшего узла достаются соседу, остальные не трогаются.

Событиеhash % NConsistent hashing
Добавили узелпереезжает ~все ключипереезжает ~1/N ключей
Убрали узелпереезжает ~все ключипереезжают только ключи этого узла

Виртуальные узлы

Если каждый узел — одна точка на кольце, распределение получается неравномерным. Поэтому каждый физический узел представляют сотнями виртуальных точек на кольце. Тогда нагрузка размазывается ровно, а при падении узла его ключи расходятся по многим соседям, а не падают на одного. Этот приём используют Cassandra, DynamoDB, многие распределённые кэши.

Итог

  • Шардирование делит данные по узлам; ключ должен распределять нагрузку равномерно.
  • Наивный hash % N при смене числа узлов переселяет почти все ключи.
  • Consistent hashing переселяет лишь ~1/N ключей; виртуальные узлы выравнивают нагрузку.
Проверьте себя
1. В чём главная проблема формулы shard = hash(key) % N?
AОна слишком медленная
BПри изменении N (числа узлов) почти все ключи меняют узел — массовый переезд данных
CОна не работает со строками
DОна даёт неравномерное распределение всегда
2. Что такое «горячий» шард?
AШард, который физически перегревается
BУзел, на который приходится непропорционально много трафика из-за неудачного ключа
CШард с самыми новыми данными
DРезервная копия шарда
3. Сколько примерно ключей переезжает при добавлении узла в схеме consistent hashing?
Aпочти все ключи
Bоколо 1/N ключей — только часть между новым узлом и соседом
Cровно половина
Dни одного
4. Зачем в consistent hashing вводят виртуальные узлы?
AЧтобы скрыть реальные адреса серверов
BЧтобы распределение по кольцу было равномерным, а нагрузка упавшего узла расходилась по многим соседям
CЧтобы ускорить хеш-функцию
DЧтобы уменьшить число физических серверов
Поддержать проект