MongoDB и гибкие схемы данных
(Раздел: Технологии и Инфраструктура)
Краткое резюме
MongoDB — документно-ориентированное хранилище с гибкими схемами (BSON), быстрыми вставками, горизонтальным масштабированием и мощным Aggregation Pipeline. В iGaming оно отлично подходит для профилей игроков, гибких CRM-карточек, логов событий, телеметрии, материализованных проекций из стрима, каталогов игр и кешируемых представлений для фронтов. Для денежных инвариантов (кошельки/ledger) чаще остается SQL/CP-контур; MongoDB уместна как read-model и высокопроизводительное документное хранилище.
Где MongoDB дает максимум в iGaming
Профили и настройки игроков: переменные структуры (настройки локали, предпочтения, KYC-метаданные).
Каталоги контента/игр/провайдеров: быстрое чтение карточек, фильтры, тегирование, полнотекст.
События/телеметрия/журналы: высокие TPS, временные окна, TTL-хранение.
Материализованные представления (CQRS): быстрые экраны (лидерборды, последние действия, агрегаты).
Персонализация/фичи онлайн-ML: KV-паттерны в коллекциях, короткий TTL.
Принципы гибкой схемы: дисциплина вместо хаоса
MongoDB не «без схемы» — схема живет в коде и валидации.
Рекомендуется:1. Схема как контракт: JSON Schema Validation в коллекциях.
2. Версионирование документов полем `schemaVersion`.
3. Строгие обязательные поля (id, ключи поиска), «хвост» редких атрибутов — опционально.
4. Ограничение размерности массивов и вложенности (для индексов и RAM).
5. Миграции в фоне: апдейты по `schemaVersion`, шедулеры, бэк-филлы.
Пример: JSON Schema Validation
js db.createCollection("player_profiles", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["playerId", "createdAt", "schemaVersion"],
properties: {
playerId: { bsonType: "string" },
createdAt: { bsonType: "date" },
schemaVersion: { bsonType: "int", minimum: 1 },
locale: { bsonType: "string" },
kyc: {
bsonType: "object",
properties: {
status: { enum: ["pending", "verified", "rejected"] },
doc: { bsonType: "object" }
}
}
}
}
}
});
Модель данных и проектирование документов
Проектируйте «под запрос»: 1 экран/эндпойнт = 1 документ или небольшой набор документов.
Денормализация: включайте небольшие вложенные поддокументы (например, мини-карточки провайдеров игр).
- Встраивание — для тесно связанных и редко обновляемых фрагментов.
- Ссылки (`ref`) — при большом размере/частых апдейтах/повторном использовании.
- Ограничение размера: документ ≤ 16 МБ; крупные бинарники — GridFS/объектные хранилища.
- Аудит/метаданные: `createdAt`, `updatedAt`, `traceId`, `tenantId`, `idempotencyKey`.
Индексы: качество чтения и стабильность latency
Типы индексов и практики:- B-Tree (основной)
Compound: порядок полей соответствует частым предикатам и сортировкам.
Prefix-правило: для `(tenantId, playerId, createdAt)` работают префиксные варианты.
Сортировка: учитывайте `sort` в конце индекса (например, `createdAt: -1`).
js db.bets.createIndex(
{ tenantId: 1, playerId: 1, createdAt: -1 },
{ name: "idx_bets_tenant_player_created_desc" }
);
Partial / Sparse
Ускоряют частые подмножества (`status: "pending"`), уменьшают размер.
js db.withdrawals.createIndex(
{ playerId: 1, createdAt: -1 },
{ partialFilterExpression: { status: "pending" } }
);
TTL
Для телеметрии/логов/временных фичей — автоматическое истечение.
js db.events.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 });
Текстовый / autocomplete
`text` для полнотекста (ограничения по языкам); для автодополнения — `n-gram`/trigram через поля и regex-подходы или Atlas Search.
Антипаттерны индексов
Индекс «на все» → падение скорости записи.
Низкая кардинальность без partial → низкая селективность.
Дублирующие компаунды.
Индексировать поля внутри гигантских массивов без лимитов.
Aggregation Pipeline: быстрые экраны и отчеты
Используйте `$match` → `$sort` → `$limit` как ранние стадии; проектируйте индексы под `$match/$sort`.
`$lookup` для контролируемых джойнов (мягкие, в разумных объемах).
`$facet` для множественных метрик; `$unionWith` — объединение коллекций.
`$merge`/`$out` — материализация результатов в коллекции (read-models).
js db.bets.aggregate([
{ $match: { tenantId: "eu-1", playerId: "p123" } },
{ $sort: { createdAt: -1 } },
{ $limit: 100 },
{ $group: {
_id: "$playerId",
lastBets: { $push: { amount: "$amount", ts: "$createdAt", game: "$gameId" } },
totalAmount: { $sum: "$amount" }
} }
]);
Транзакции, согласованность и идемпотентность
Single-document atomic — бесплатная атомарность; сложные инварианты — думайте о разбиении по документам.
Multi-document transactions (ACID) — есть с реплика-сетами, но дороже по latency; применять точечно.
- `w: "majority"` для критичных записей (стоимость latency);
- `readConcern: "majority"` для согласованного чтения.
- Идемпотентность: уникальные ключи на `idempotencyKey`/`pspTx`, UPSERT-операции (`$setOnInsert`, `$inc`).
js db.wallet.updateOne(
{ playerId: "p123" },
{ $inc: { balanceCents: -5000 }, $set: { updatedAt: new Date() } },
{ upsert: true, writeConcern: { w: "majority" } }
);
Шардирование и выбор ключей
MongoDB шардирует по shard key. Выбор критичен:- Распределение нагрузки: ключ высокой кардинальности и равномерным распределением (например, `(tenantId, playerId)`).
- Избегайте монотонности: `createdAt` как единственный ключ → «горячий» шард.
- Hashed — ровнее распределяет записи.
- Ranged — лучше для диапазонных запросов, но следите за горячими хвостами.
- Зона-шардинг (tag ranges) для регуляторики/локализации (EU/LatAm/TR).
js sh.enableSharding("igaming");
db.bets.createIndex({ tenantId: 1, playerId: 1, _id: "hashed" });
sh.shardCollection("igaming.bets", { tenantId: 1, playerId: 1, _id: "hashed" });
Антипаттерны:
- Шард-ключ по низкой кардинальности (`status`) — перекос шардов.
- Частые `$lookup` между шардированными коллекциями без со-шардирования по одному ключу.
- Изменяемый shard key (менять сложно и дорого).
Реплика-сеты, чтения и политика read-after-write
Реплика-сет = HA и основа транзакций.
Read Preference:- `primary` для критичных read-after-write;
- `primaryPreferred`/`secondary` — для аналитики/не критичных.
- Read/Write concern согласовывайте с SLO и бюджетом latency.
Change Streams, CDC и интеграции
Change Streams: подписка на вставки/апдейты/удаления — удобно для:- синхронизации кэш-слоев (Redis),
- триггеров CRM/уведомлений,
- загрузки в OLAP (ClickHouse/Pinot),
- реактивных экранов.
- Outbox-паттерн: для критичных доменов публикуйте события в отдельную коллекцию, которую затем читает коннектор и транслирует в шину (Kafka). Это повышает предсказуемость интеграций.
Наблюдаемость и SLO
SLO: p99 чтения карточек ≤ 10–20 мс; вставки событий ≤ 20–40 мс; разница лейтенси между шардами в пределах X%; доступность ≥ 99.9%.
Метрики: оп-латентность, queue depth, % юмпсов на вторичные, cache/WT статистики, page faults, lock-waits, кол-во открытых курсоров/соединений.
Профилирование: `system.profile`, `explain("executionStats")`, блокировки коллекций/индексов.
Алерты: рост WT cache pressure, медленные операции, рост не попавших в индекс запросов, отставание вторичных, chunk migrations/балансер.
Производительность и тюнинг
WiredTiger Cache: по умолчанию ~50% RAM — валидируйте под профиль.
Compression: snappy/zstd для коллекций, zstd для журналов — баланс CPU/IO.
Batch-вставки и bulkWrite для телеметрии.
Projection (`{field:1}`) чтобы не тащить «толстые» документы.
Limit/Skip: избегайте больших `skip` → используйте пагинацию по курсору/по маркеру (`createdAt/_id`).
Capped коллекции для «кольцевых» логов.
Безопасность и комплаенс
Auth/RBAC: роли на коллекции/БД, минимально необходимые привилегии.
TLS в транзите, шифрование на диске (FLE/at-rest).
Политики PII: маскирование/псевдонимизация, отдельные коллекции для чувствительных полей.
Мульти-тенантность: префиксы/отдельные БД/коллекции, фильтры по `tenantId`, можно RLS-подобные слои в приложении.
Аудит: включайте аудит операций на критичных коллекциях.
Бэкапы, PITR и DR
Снимки (snapshots) томов + oplog-бэкапы для Point-in-Time Recovery.
Реплика-сет в другом регионе для DR; регулярные учения восстановления.
Контроль роста oplog под пики вставок (PSP вебхуки/турниры).
В шард-кластерах — согласованные бэкапы с config-сервером.
Интеграция с остальной архитектурой
CQRS: команды бьют по SQL (деньги), события → Materialized Views в MongoDB.
Event-Streaming: Kafka/Pulsar как шина, Mongo — sink/source через коннекторы и Change Streams.
Redis: рядом как слой ультра-низкой латентности (кэши/счетчики).
OLAP: выгрузка в ClickHouse/Pinot для длинных сканов и BI.
Чек-лист внедрения
1. Зафиксируйте домены: что идет в Mongo (гибко/высокий TPS/проекции), что остается в SQL.
2. Определите schema contracts: JSON Schema Validation, `schemaVersion`.
3. Спроектируйте индексы под реальные запросы; добавьте TTL для «шумных» данных.
4. Выберите shard key (высокая кардинальность, равномерность); при необходимости — zone-шардинг.
5. Настройте реплика-сет, Read/Write Concern под SLO; политика read-after-write.
6. Включите наблюдаемость и профилирование, алерты на индексы/WT cache/oplog.
7. Организуйте бэкапы + PITR, DR-кластер и регулярные учения.
8. Подключите Change Streams/Outbox для синхронизации кэшей и шин.
9. Ограничьте размер документов и вложенность; внедрите пагинацию по курсору.
10. Отдельные политики для PII/тенантов, шифрование, аудит.
Антипаттерны
«Без схемы» в проде: отсутствие валидации и версий → хаос.
Шард-ключ по времени/монотонный — горячий шард и нестабильная p99.
Джойны `$lookup` на огромных наборах без индексов/пагинации.
Использовать транзакции повсеместно — потери производительности.
Отсутствие TTL/ретенции для логов → рост объема и стоимости.
Хранить критично важные денежные инварианты только в Mongo без строгой идемпотентности.
Итоги
MongoDB — мощный инструмент для гибких доменов iGaming: профили, каталоги, телеметрия, проекции и персонализация. Ключ к успеху — схема-контракты и валидация, продуманная индексация, грамотно выбранный shard key, осознанные Read/Write Concern, Change Streams для интеграций и жесткая эксплуатационная дисциплина (наблюдаемость, бэкапы, DR). В сочетании с SQL-ядром и стриминговой шиной это дает платформе быстрые интерфейсы и устойчивость под турнирные пики.