Шардирование: горизонтальное масштабирование
Replica set спасает от сбоев, но не от роста: когда данные перестают помещаться на одну машину, а поток записей упирается в один primary, нужно шардирование — данные распределяют по нескольким серверам.
Шардирование (sharding) — горизонтальное масштабирование: набор данных делится на части и раскладывается по нескольким серверам-шардам. Каждый шард хранит лишь свой кусок данных и обрабатывает лишь свою долю запросов.
Зачем это на практике
Вертикальное масштабирование (взять сервер помощнее) рано или поздно упирается в потолок: самый большой диск конечен, а один primary физически не примет миллион записей в секунду. Шардирование снимает оба ограничения. Терабайты данных делятся между десятком шардов, и каждый отвечает только за свой диапазон — суммарно кластер хранит и обрабатывает в разы больше.
Но у шардирования высокая цена: сложность эксплуатации, дополнительные процессы, и — главное — необратимое решение про ключ шардирования. Поэтому правило: не шардируйте преждевременно. Сначала индексы, оптимизация запросов, replica set с чтением со вторичных. Шардирование — когда один replica set объективно не тянет объём или нагрузку.
Из чего состоит шардированный кластер
| Компонент | Роль |
shard | хранит часть данных; сам по себе — отдельный replica set |
config servers | replica 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).