Шардирование: горизонтальное масштабирование

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

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

Зачем это на практике

Вертикальное масштабирование (взять сервер помощнее) рано или поздно упирается в потолок: самый большой диск конечен, а один primary физически не примет миллион записей в секунду. Шардирование снимает оба ограничения. Терабайты данных делятся между десятком шардов, и каждый отвечает только за свой диапазон — суммарно кластер хранит и обрабатывает в разы больше.

Но у шардирования высокая цена: сложность эксплуатации, дополнительные процессы, и — главное — необратимое решение про ключ шардирования. Поэтому правило: не шардируйте преждевременно. Сначала индексы, оптимизация запросов, replica set с чтением со вторичных. Шардирование — когда один replica set объективно не тянет объём или нагрузку.

Из чего состоит шардированный кластер

КомпонентРоль
shardхранит часть данных; сам по себе — отдельный replica set
config serversreplica set с метаданными кластера: карта чанков, кто где лежит
mongosмаршрутизатор; приложение подключается к нему, а не к шардам

Ключевая мысль: приложение видит mongos как обычную MongoDB. Оно шлёт запрос — mongos смотрит в метаданные и направляет его на нужный шард (или на все). Сами шарды — полноценные replica set, так что каждый шард ещё и отказоустойчив.

Ключ шардирования — самое важное решение

Shard key — это поле (или несколько полей) документа, по которому MongoDB решает, на какой шард положить документ. Данные режутся на чанки (chunks) — непрерывные диапазоны значений ключа (по умолчанию до 128 МБ). Балансировщик (balancer) в фоне следит, чтобы чанки были распределены по шардам равномерно, и при необходимости мигрирует их между шардами — онлайн, не останавливая кластер.

Хороший ключ шардирования обладает тремя свойствами:

  • Высокая кардинальность — много разных значений. Если значений мало (например, поле «страна» или булев флаг), чанки нельзя дробить дальше, они разрастаются в «jumbo» и застревают на одном шарде.
  • Низкая частота — ни одно значение не доминирует. Если 90% документов имеют status: "active", этот чанк перегрет.
  • Немонотонность записи — новые документы не валятся в один и тот же конец диапазона. Монотонный ключ (timestamp, ObjectId) отправляет все вставки в последний чанк → «горячий» шард, остальные простаивают.

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

Ranged vs hashed

Есть две стратегии распределения:

// включаем шардирование базы и коллекции
sh.enableSharding("shop")

// RANGED: чанки по диапазонам значений ключа
sh.shardCollection("shop.orders", { customerId: 1, orderDate: 1 })

// HASHED: распределяем по хэшу ключа
sh.shardCollection("shop.events", { deviceId: "hashed" })

Ranged sharding кладёт рядом близкие значения. Плюс: запросы по диапазону (orderDate за месяц) попадают в один-два чанка. Минус: монотонный ключ создаёт горячую точку записи.

Hashed sharding распределяет по хэшу — записи раскладываются равномерно даже при монотонном поле. Минус: запрос по диапазону превращается в scatter-gather (опрос всех шардов), потому что соседние значения разбросаны.

Частое практичное решение — составной ключ, например { customerId: 1, orderDate: 1 }: высокая кардинальность по клиенту даёт равномерность, а второе поле помогает диапазонным запросам внутри клиента.

Как это работает под капотом

Метаданные о том, какой диапазон ключа лежит на каком шарде, хранят config servers (тоже replica set — единый источник правды о топологии). mongos кэширует эту карту чанков. Когда приходит запрос, mongos смотрит, содержит ли он shard key. Если да — это targeted query, идёт точечно на нужные шарды. Если нет — broadcast/scatter-gather: запрос рассылается на все шарды, результаты собираются обратно (медленно и дорого).

Когда чанк перерастает лимит, MongoDB его разбивает (split) на два. Если шарды разбалансировались (на одном чанков заметно больше), балансировщик мигрирует чанки на менее загруженные шарды. Миграция идёт в фоне и не блокирует кластер. Именно поэтому плохой ключ так больно бьёт: при монотонном ключе все новые чанки рождаются на одном шарде, и балансировщик вечно тащит их прочь, тратя ресурсы на бесконечную перебалансировку.

Частые ошибки

  • Монотонный ключ при ranged-шардировании. Взяли _id (по умолчанию ObjectId растёт во времени) или timestamp — и все записи бьют в один шард. Лекарство: hashed-ключ или составной ключ с высококардинальным первым полем.
  • Низкая кардинальность. Ключ «страна», «город», булев флаг — мало значений, чанки не дробятся, появляются jumbo-чанки, застрявшие на шарде. Берите поле с тысячами и более уникальных значений.
  • Ключ не из частых запросов. Шардировали по userId, а ищете в основном по email — каждый такой поиск становится scatter-gather по всем шардам. Согласуйте ключ с реальными паттернами запросов.
  • Шардировать слишком рано. Сложность не окупается, пока один replica set справляется. Сначала индексы и оптимизация.
  • Считать, что ключ легко поменять. Исторически shard key был неизменяемым; современные версии умеют его менять/решардить, но это тяжёлая дорогая операция. Думайте над ключом заранее.

Итоги

  • Шардирование — горизонтальное масштабирование: данные делятся между шардами, когда один replica set не тянет объём или нагрузку.
  • Кластер состоит из шардов (каждый — replica set), config servers (метаданные) и mongos (роутер, точка входа для приложения).
  • Выбор shard key — самое важное и трудно обратимое решение: нужны высокая кардинальность, низкая частота, немонотонная запись и соответствие запросам.
  • Ranged — хорош для диапазонных запросов, но боится монотонных ключей; hashed — равномерная запись ценой scatter-gather по диапазонам.
  • Запрос с shard key уходит точечно; без него — рассылается на все шарды (scatter-gather).
Проверьте себя
1. Почему монотонно растущий shard key (например, timestamp или ObjectId) — плохой выбор для ranged-шардирования?
AОн занимает слишком много места на диске
BВсе новые вставки попадают в последний чанк, создавая горячий шард, пока остальные простаивают
CMongoDB вообще запрещает такие ключи
DОн ломает выборы primary внутри шардов
2. К какому компоненту шардированного кластера подключается приложение?
AНапрямую к каждому шарду
BК config servers
CК mongos (маршрутизатору)
DК арбитру
3. Запрос НЕ содержит поле shard key. Что сделает mongos?
AВернёт ошибку
BОтправит запрос на все шарды (scatter-gather) и соберёт результаты
CОтправит запрос только на config servers
DТочечно направит на один шард по _id