MongoDB document database deep dive: replica set with oplog (1 primary + N secondaries with automatic Raft-like failover), sharded cluster (mongos routers + config server replica set + 3 shard replica sets), writeConcern levels (1, majority, all), readPreference (primary, secondary, nearest), multi-document transactions (4.0+), and change streams (CDC built-in). Four scenarios: write with writeConcern majority, primary failover with election, sharded query routing (targeted vs scatter-gather), and change stream tailing oplog. Includes ADR comparing MongoDB vs PostgreSQL JSONB vs DynamoDB for evolving-schema document workloads.
MongoDB — это document database на WiredTiger storage engine с replica set и опциональным sharding. Появилась в 2009 как ответ на боль реляционных БД при evolving schemas, JSON-like документах и горизонтальном масштабировании "из коробки". Сегодня — самая распространённая document DB в продакшене, со зрелой экосистемой драйверов и managed-сервисом (Atlas).
Главная ценность — схема живёт в приложении, а не в БД. Добавление поля не требует ALTER TABLE на 500 GB таблицу с двухчасовой блокировкой: обновляешь validator или просто пишешь новое поле, существующие документы остаются как есть. Это критично на ранних стадиях продукта.
Вторая ценность — встроенный путь к шардингу. Replica set из 3 нод даёт durability и failover; когда упрёшься в потолок (5-10K writes/s на современном железе), переключаешь на sharded cluster через mongos + config-rs + shards без переписывания приложения. Драйверы прозрачно роутят запросы.
Третья ценность — change streams (CDC из коробки). db.collection.watch() отдаёт events с resumeToken для resilience. Не нужен Debezium + Kafka — для многих use-cases встроенного механизма хватает.
Что MongoDB не делает хорошо: cross-document JOINы ($lookup медленный и плохо масштабируется), сложные многодокументные транзакции (multi-doc txn есть с 4.0, но дороже single-doc на порядок), strict ACID на финансовые потоки без понимания writeConcern/readConcern (Jepsen неоднократно ловил баги). Если evolving schema исчезла, нужны JOINы и transactions — выбирай PostgreSQL.
Держи в голове три уровня:
Уровень 1 — нода (mongod + WiredTiger). Процесс с B-tree на диске, snapshot MVCC, journal (WAL, group commit ~50ms), checkpoint каждые 60s или 2GB лога. Cache = 50% RAM минус 1 GB по умолчанию; working set обязан помещаться в cache, иначе page faults убивают latency. Один документ — atomic update даже при nested изменениях.
Уровень 2 — replica set (1 primary + N secondaries). Все writes идут на primary, который пишет в локальный oplog (capped collection в local.oplog.rs). Secondaries делают pull-tail oplog'a и применяют изменения у себя. Failover — Raft-like election за ~10s, побеждает secondary с самым свежим oplog (highest lastApplied). Без шардинга 90% продакшен-кластеров живут вечно на replica set из 3 нод.
Уровень 3 — sharded cluster (mongos + config-rs + shards). mongos — stateless router, config-rs — replica set с метаданными о chunks, shards — несколько replica sets. Данные режутся по shard key на chunks (default 128 MB), balancer фоном двигает chunks. Запрос с shard key летит в один shard (targeted, ~3ms); без shard key — во все (scatter-gather, latency = max(shards)).
Knobs, которые меняют поведение системы:
w:1 (только primary, fire-and-forget стиль, потеря при failover), majority (default с 5.0, durable), w:N, all. Latency vs durability.primary (consistent), secondary (stale read, не используй без понимания), nearest (latency optimized). Read scalability vs consistency.local, majority (видим только то, что прошло majority commit), linearizable (дорого, write на каждый read для leader confirmation), snapshot (для transactions).Эта тройка (writeConcern, readPreference, readConcern) — твой основной dial между performance и consistency. См. cap-theorem и pacelc-theorem: MongoDB по умолчанию PC/EC (consistent при partition, consistent в нормальной работе), но конфигурируется в обе стороны.
Shard key — критичное архитектурное решение. Monotonic key (timestamp, ObjectId, sequential ID) → все новые writes падают на последний chunk → hot shard. Hashed shard key → равномерное распределение, но плохо для range queries. Compound ({tenantId: 1, _id: 1}) — лучшее для multi-tenant. С 5.0 ключ можно менять (refineCollectionShardKey, reshardCollection), но это дорогая операция.
Диаграмма показывает полный sharded cluster: app tier (writer + reader), два stateless mongos router'а, config-rs из трёх нод, и три shard'а — каждый replica set из 1 primary + 2 secondaries.
Writes идут по цепочке app-writer → mongos → config-rs (lookup) → shard primary → secondaries (oplog stream) → ack обратно. mongos кеширует shard map и обращается к config-rs только при stale-cache.
Reads аналогичны: с shard key → один shard (targeted); без shard key → mongos рассылает во все shards параллельно (scatter-gather) и мерджит результаты.
Oplog replication — edges от primary к secondaries — асинхронный pull-stream, на который ждём при writeConcern: majority.
1. Write w (durable). App пишет insertOne с writeConcern: majority. mongos лукапит shard map в config-rs, роутит на нужного primary. Primary применяет write в WiredTiger, пишет journal + oplog entry, и параллельно streamит к двум secondaries. Как только secondary-a подтверждает (1 primary + 1 sec = majority из 3), primary отвечает mongos, mongos — клиенту. Latency +10-20ms vs w:1, но write durable при любом одиночном failover. Это default с 5.0.
2. Failover (Raft-like election). Primary крашится (OOM, network partition, kernel panic). Secondaries видят heartbeat timeout >10s, начинают election — обмениваются term + lastApplied, побеждает тот, у кого свежее oplog. Promotion ~10s typical, ~30s worst-case. Driver с retryWrites=true хранит lsid + txnNumber snapshot и прозрачно retries на новом primary — никаких дубликатов. Это работает только для idempotent ops (single-document writes); для multi-doc transactions нужен ручной retry с TransientTransactionError обработкой.
3. Targeted vs scatter-gather query. Запрос find({_id: ObjectId("...")}) имеет shard key → mongos маппит hashed(_id) в chunk и отправляет в один shard, B-tree lookup ~1ms, total ~3ms. Запрос find({status: "pending"}) НЕ имеет shard key → mongos шлёт во все три shards параллельно, каждый сканит свой кусок, mongos мерджит и сортирует. Latency = max(всех shards) + merge overhead (~50-100ms). При росте числа shards растёт tail latency — это фундаментальная цена шардинга при отсутствии shard key в query.
4. Change stream (CDC built-in). Клиент вызывает db.orders.watch([{$match: {operationType: "insert"}}]). mongos открывает change stream cursor на каждом shard primary; каждый primary tail'ит свой oplog и стримит matching events обратно. Когда происходит insert, primary применяет write и сразу отправляет change event клиенту с resumeToken. Если consumer крашится — продолжает с сохранённого resumeToken. Это полноценный CDC без отдельной инфры типа Debezium/Kafka, и используется для real-time analytics, search index sync (Elasticsearch), cache invalidation.
На диаграмме на ноде mongos-1 лежит ADR-001 — MongoDB vs Postgres JSONB vs DynamoDB для document workload.
Контекст: команда на старте product catalog, ~50 GB, evolving schema (товары разных категорий с разным набором атрибутов), нужен full-text search по названию и фасеты по brand/price, ожидаемый рост до 1-2 TB и 5-10K reads/s. Backend на Node.js, нет dedicated DBA.
Решение — MongoDB replica set (3 ноды) с writeConcern: majority и readPreference: primaryPreferred. Причины:
ALTER — добавление поля = обновление application validator, не миграция БД на 50 GB{category, attrs.color, price} быстрее, чем GIN на JSONB при growing dataset{tenantId, _id} compound shard key, без переписывания приложенияОтвергнутые альтернативы:
Consequences: writes 3-8K/s на m6i.xlarge, p99 read 5-15ms, simple ops через Atlas. Минусы: $lookup медленный → нужно денормализовать; aggregation pipeline учится тяжело; забыли writeConcern: majority до 5.0 = риск потери writes на failover. Триггер на пересмотр: cross-document money flow появится → Postgres; evolving schema исчезнет и нужны JOINы → Postgres; >50K writes/s на одной коллекции → шардируем или переходим на Cassandra/Scylla.
Note: Stripe не использует MongoDB — это known Postgres-only shop с Vitess для шардирования. См. case
payment-systemи Stripe engineering blogs.
Historical note: Discord использовал MongoDB исторически, но мигрировал на Cassandra (а затем добавил ScyllaDB) для chat messages при росте до триллионов записей. Полезный пример того, когда MongoDB перестаёт подходить — extremely write-heavy time-series-like workload с равномерным распределением.
writeConcern: w:1 для критичных данных (до 5.0 был default). Write возвращает ack как только primary применил; если primary крашится до oplog replication — write теряется навсегда. Всегда majority для money/orders/auth.hashed или compound с tenantId prefix.$jsonSchema validators поля расползаются (userId, user_id, UserId в одной коллекции). Задавай validators даже на "временные" коллекции.$push без bound. Document grows → fragmentation → 16 MB limit hit. Используй capped arrays ($push: {$each, $slice: -1000}) или выноси в отдельную коллекцию.$lookup для сложных JOIN'ов. N+1 в дисгайзе. Либо денормализуй, либо признай, что нужен Postgres.writeConcern: majority + readConcern: majority.RECOVERING → full initial sync (часы на TB). Алерт обязателен: window < 2× peak lag.$lookup плохо масштабируется. Для analytics — Postgres/ClickHouse; для warehouse — Snowflake/BigQuery.Docs (точные ссылки):
Jepsen analyses (читай как survival guide):
Книги:
Engineering blogs:
Связанные концепты: