MongoDB изначально проектировалась под распределённость: replica set появился раньше транзакций, шардинг — стандартный режим работы крупных кластеров. В этой статье — как считать, сколько уже занимает кластер, как устроена репликация, и как выбирать shard key, чтобы не пришлось делать resharding кварталами.
Примеры — на тех же двух коллекциях, что и в статье про ACID: category (3 документа) и product (7 документов, в реальности — миллионы).
Как считать размер кластера
Три уровня детализации.
База целиком:
db.stats(1024 * 1024 * 1024); // вывод в GB
// {
// db: "shop",
// collections: 8,
// objects: 142_530_891,
// dataSize: 41.2, // логический размер документов
// storageSize: 18.7, // сжатый, на диске (WiredTiger ~50% сжатие на JSON-подобных данных)
// indexSize: 6.4,
// totalSize: 25.1
// }
Одна коллекция:
db.product.stats(1024 * 1024 * 1024);
// {
// size: 12.3, // логический объём документов
// storageSize: 5.8, // на диске после WiredTiger-сжатия
// nindexes: 4,
// totalIndexSize: 2.1,
// avgObjSize: 132 // байт на документ — важно для прогноза
// }
По индексам:
db.product.aggregate([{ $indexStats: {} }]).forEach(s => {
print(s.name, s.accesses.ops);
});
// _id_ 12000000
// categoryId_1 8500000
// price_-1 145 ← редко используется, кандидат на удаление
«storageSize меньше size в 2× и более» — нормально для WiredTiger со snappy/zstd. «totalSize вашей базы догоняет RAM сервера» — пора задумываться о шардинге. Грубо: на одной машине комфортно живёт active dataset ≤ 70% от RAM. Когда индексы перестают помещаться — производительность падает на порядок.
Replica set — как устроена репликация
Минимум — три узла: один primary (принимает записи) и два secondary (реплицируют). Альтернатива — primary + secondary + arbiter (узел-голосователь без данных), но для production чаще берут три полноценных узла.
┌──────────┐
write → │ primary │ ───┐
└──────────┘ │ репликация через oplog
▲ │
│ ▼
┌──────────┐ ┌──────────┐
│secondary │ │secondary │
└──────────┘ └──────────┘
Oplog — capped collection в системной базе local, куда primary пишет каждую успешную операцию. Secondaries читают oplog и применяют у себя. Это аналог WAL в PostgreSQL, но логический (операции, а не страницы) — поэтому MongoDB не требует одинаковых версий или одинаковой компоновки данных между репликами.
Размер oplog'а определяет, как долго реплика может отстать. По умолчанию — 5% от свободного места на диске (но не больше 50 GB). Если secondary отстал больше, чем длина oplog'а — приходится делать initial sync с нуля.
Выбор primary
При сбое primary узлы голосуют. Побеждает кандидат с самым свежим oplog'ом и большинством голосов. Выбор занимает 10–30 секунд — на это время кластер недоступен на запись.
Что важно знать:
- Кворум нужен для записи: если из трёх узлов два пропали — оставшийся становится readonly. Это правильное поведение: иначе при разрыве сети получили бы split brain.
- Запись с
w: "majority"гарантированно переживёт failover. Сw: 1— нет. priorityв конфигурации replica set позволяет указать предпочтительный primary (например, в основном дата-центре).
Read preference
Куда драйвер посылает чтение. Пять режимов:
| Режим | Где читаем | Когда брать |
|---|---|---|
primary (default) | Только primary | Когда нужны самые свежие данные (баланс, заказ перед оплатой) |
primaryPreferred | Primary, если он жив; иначе secondary | Read-after-write критичные операции с fallback |
secondary | Только secondary | Аналитические запросы, отчёты — не нагружать primary |
secondaryPreferred | Secondary, primary только если все secondary упали | Read-heavy load, толерантный к stale данным |
nearest | Реплика с минимальной сетевой задержкой | Geo-distributed clusters |
db.product.find({ categoryId: 1 })
.readPref("secondaryPreferred", [{ region: "eu-west" }]);
// читаем из ближайшей реплики в Европе
Подводный камень: secondary может отставать. С readConcern: "majority" это в основном решается (читаем то, что подтверждено большинством), но stale-данные всё равно возможны. Для критичных read-after-write — использовать causal consistency.
Когда переходить на sharded cluster
Так же как и в PostgreSQL: когда одной машины уже не хватает. Конкретные триггеры:
- активный dataset не помещается в RAM, тяжёлые запросы вынуждают читать с диска;
- write throughput упирается в IOPS одной ноды;
- общий объём приближается к лимиту одного хранилища;
- нужно держать данные географически рядом с пользователями.
Партиционирования в стиле PostgreSQL внутри одной БД у MongoDB нет: следующий шаг масштабирования за пределы одной replica set — сразу sharded cluster.
Sharded cluster — что внутри
Четыре типа узлов:
┌─────────────────────┐
client→ │ mongos │ ←── 1+ роутера, stateless
│ (query router) │
└─────────────────────┘
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ Shard A │ │ Shard B │ ←── каждый шард = свой replica set
│(rs of 3) │ │(rs of 3) │
└──────────┘ └──────────┘
│ │
▼ ▼
┌───────────────────┐
│ Config Servers │ ←── 3 узла, хранят metadata
│ (replica set) │ (какой chunk на каком shard'е)
└───────────────────┘
- mongos — точка входа клиента. Принимает запросы, по shard key решает, на какой шард их отправить, агрегирует ответы. Stateless — масштабируется горизонтально.
- Config servers — replica set из 3 узлов с метаданными кластера (маршруты chunk → shard, версии). Без них кластер не работает.
- Shards — обычные replica set'ы с куском данных. Данные внутри коллекции разделены на chunks (по умолчанию 128 MB), которые распределены по шардам.
- Balancer — фоновый процесс, перераспределяющий chunks между шардами при дисбалансе. Запускается автоматически.
Стоимость минимального production-кластера: 3 (config) + 6 (два шарда × 3 реплики) + 2 (mongos с резервом) = 11 узлов. Это сильно больше одной replica set — поэтому шардинг включают только когда альтернатив нет.
Стратегии shard key
Ranged sharding
Документы распределены по диапазонам значений shard key. Если shard key — _id, и значения идут по возрастанию (ObjectId или BIGSERIAL), то новые документы концентрируются на последнем шарде — типичный антипаттерн «hot shard».
sh.shardCollection("shop.product", { _id: 1 }); // BAD на монотонном _id
Ranged подходит, когда:
- shard key равномерно распределён (например,
categoryIdпри равном объёме категорий); - запросы часто фильтруют по диапазону этого ключа (геолокация, время в исторических данных).
Hashed sharding
MongoDB сама хеширует значение shard key. Распределение равномерное, hot shard невозможен. Минус: запросы по диапазону значений идут на все шарды (scatter-gather).
sh.shardCollection("shop.product", { _id: "hashed" });
// _id монотонный — но после хеша распределение равномерно
Подходит для:
- ключей с монотонным ростом (
_id, timestamp); - запросов по точному значению (
find({ _id: ... })); - write-heavy нагрузки, когда главное — равномерно нагрузить шарды.
Zoned sharding
Прикрепляем диапазоны shard key к зонам (тегам шардов). Используется для geo-распределения и tenant-изоляции.
// Каждый шард получает тег региона
sh.addShardTag("shardEU", "EU");
sh.addShardTag("shardUS", "US");
// Документы с region="EU" идут в EU-шард
sh.addTagRange(
"shop.product",
{ region: "EU", productId: MinKey },
{ region: "EU", productId: MaxKey },
"EU"
);
Полезно, когда регуляторика требует физически держать данные клиента в его юрисдикции.
Compound shard key
Часто оптимум — составной ключ: первая часть даёт равномерность, вторая — locality для типичных запросов.
Для нашей product, если запросы обычно идут по categoryId и _id, а товаров по категориям сильно разное количество:
sh.shardCollection(
"shop.product",
{ categoryId: 1, _id: "hashed" }
);
В одной категории документы хешируются по _id (равномерно по шардам), но запросы find({ categoryId: 1 }) могут читать чуть меньше шардов, чем чистый hash по _id.
Как выбирать shard key
Пять критериев — выполнение всех обязательно:
- Высокая кардинальность — много разных значений. Shard key
genderс двумя значениями физически не позволит распределить по 10 шардам. - Равномерное распределение — иначе горячий шард.
userIdобычно ок,countryдля российской компании — плохо. - Ключ есть в большинстве запросов — иначе каждый
find()идёт на все шарды. - Низкая частота изменений — shard key менять можно с 4.4 (
refineCollectionShardKey,reshardCollectionс 5.0), но это операция уровня кварталов работы. - Targeted writes возможны — каждая запись попадает на один конкретный шард, без распределённой транзакции.
Антипример из нашего домена: шардировать product по _id (ObjectId), а category оставить нешардированной. Запросы с $lookup product → category бьют по всем шардам.
Лучше: шардируем обе коллекции по categoryId (с compound key для product). Тогда product и его category физически рядом — $lookup остаётся локальным.
Сравнение PostgreSQL и MongoDB
| PostgreSQL | MongoDB | |
|---|---|---|
| Партиционирование внутри инстанса | Есть (RANGE, LIST, HASH) | Нет — только sharded cluster |
| Минимальная репликация | Stream replication + 1 standby | Replica set из 3 узлов |
| Failover | Не автоматический (Patroni, Stolon) | Автоматический, 10–30 секунд |
| Шардинг | Через расширения (Citus) | Встроенный |
| Cross-shard JOIN | Через FDW, сложно | $lookup с автоматическим scatter-gather |
| Изменение ключа распределения | Очень дорого, обычно через миграцию | reshardCollection (5.0+), часы-дни |
Главное концептуальное отличие: в PostgreSQL партиционирование и шардирование — разные инструменты с разной стоимостью. В MongoDB шардинг сразу горизонтальный (между серверами) и сразу автоматический.
Что почитать дальше
- MongoDB: Replication и Sharding — официальная документация.
- ACID и согласованность в MongoDB — какие гарантии работают в sharded cluster.
- Моделирование документов — почему правильный schema design снижает потребность в шардинге.
- Партиционирование и шардирование в PostgreSQL — сравнение подходов.