→Защитите свою базу данных от будущих пожаров, чтобы избежать масштабных потерь капитала на этапе Серии А
→
Защитите свою базу данных от будущих пожаров, чтобы избежать масштабных потерь капитала на этапе Серии А
Ответственность: Ниже приведенное является вымышленным исследованием случаев, используемым для коммуникации лучших практик для проектирования схем MongoDB, настройки производительности и оптимизации затрат.
→Disclaimer: The following is a fictional case study used to communicate best practices for MongoDB schema design, performance tuning, and cost optimization
День, когда Билл стал ядерным
Призыв прошел через2:17. .
Atlas caused another unsolicited production cluster scale-up that resulted in an M60Машины с ежемесячной стоимостью$15kСовет хотел узнать, почему ожог увеличился на 20% в то время как M60 служит дорогостоящим$15 k/monthсистемы .
I opened the profiler:
db.system.profile.aggregate([
{ $match: { millis: { $gt: 100 } } },
{ $group: {
_id: { op: "$op", ns: "$ns", query: "$command.filter" },
n: { $sum: 1 },
avgMs: { $avg: "$millis" }
}},
{ $sort: { n: -1 } }, { $limit: 10 }
]).pretty();
Каждый виджет Dashboard требовал, чтобы нарушитель вытащил в общей сложности1.7 GBЧистое количество использования памяти создало горные вершины в графике, которые напоминают Эверест.
The M30 servers now operate with one of these clusters. The solution did not entail an increase in shards. Three common flaws known as Формы преступленийсуществовало в кодовой базе до ликвидации.
Расследование криминальной сцены
2.1 N + 1 Запрос Цунами
Это признается анти-образцом — при заказе одного набора заказов требуется выполнение N отдельных запросов для получения строк заказов.
// Incorrect: Orders + 1 000 extra queries
const orders = await db.orders.find({ userId }).toArray();
for (const o of orders) {
o.lines = await db.orderLines.find({ orderId: o._id }).toArray();
}
Hidden taxes
→ Compute | → 1 000 cursors = 1 000 context switches | →
Хранилище I/O | →
1 000 index walks + 1 000 doc deserialisations |
Сеть | →
Each round‑trip eats ~1 ms RTT + TLS handshake overhead |
Compute
Метр
Почему она шипает
вычислить
Compute
1000 курсоров = 1000 контекстных переключателей
Storage I/O
1 000 индексных ходов + 1 000 дезериализаций doc
Network
Каждый круг съедает ~1 мс RTT + TLS рукопожатие над головой
Каждый круг съедает ~1 мс RTT + TLS рукопожатие над головой
Refactor (4 lines):
// Success: Single round‑trip, 1 read unit per order
db.orders.aggregate([
{ $match: { userId } },
{ $lookup: {
from: "orderLines",
localField: "_id",
foreignField: "orderId",
as: "lines"
}},
{ $project: { lines: 1, total: 1, ts: 1 } }
]);
Задержка p95 снизилась с 2300 мс до 160 мс.
2 300 мс160 мсAtlas read‑ops: 101 → 1.Это 99% скидка — никакого кода купона не требуется.
2.2 Неограниченный запрос Firehose
«Но мы должны показать полную историю кликов!»
«Но мы должны показать полную историю кликов!»
Конечно — просто не в одном курсоре.
// Failure: Streams 30 months of data through the API gateway
db.events.find({ userId }).toArray();
Fix: hard-cap партию и проект только поля, которые вы делаете.
db.events.find(
{ userId, ts: { $gte: ISODate(new Date() - 1000*60*60*24*30) } },
{ _id: 0, ts: 1, page: 1, ref: 1 } // projection
).sort({ ts: -1 }).limit(1_000);
Тогда пусть монго очистится позади вас:
// 90‑day sliding window
db.events.createIndex({ ts: 1 }, { expireAfterSeconds: 60*60*24*90 });
Один клиент финтех сократил свой счет за хранение на 72% за ночь, просто добавив TTL.
→Один клиент финтех сократил свой счет за хранение на 72% за ночь, просто добавив TTL.
2.2 Jumbo-Document Money Pit
Mongo покрывает документы на 16 МБ, но все, что превышает 256 КБ, уже является красным флагом.
{
"_id": "...",
"type": "invoice",
"customer": { /* 700 kB */ },
"pdf": BinData(0,"..."), // 4 MB binary
"history": [ /* 1 200 delta rows */ ],
"ts": ISODate()
}
Why it hurts
- Весь документ настраивается, даже если вы прочтете одно поле.
- WiredTiger не может хранить столько документов на страницу → более низкое соотношение ударов кэша. →
- Записи в индексе огромны → фильтр пропускает → больше дисков ищет. →
Solution: →schema‑by‑access‑pattern: →
graph TD
Invoice[(invoices<br/><2 kB)] -->|ref| Hist[history<br/><1 kB * N]
Invoice -->|ref| Bin[pdf‑store (S3/GridFS)]
Мета-фактуры для небольших счетов остаются горячими; BLOBS в S3 стоит $0.023/GB-месяц вместо NAND-класса SSD Atlas.
→Мета-фактуры для небольших счетов остаются горячими; BLOBS в S3 стоит $0.023/GB-месяц вместо NAND-класса SSD Atlas.
Четыре еще формы преступлений, в которых вы, вероятно, виноваты
- →
- Индекс низкой кардинальности ({тип: 1, тс: -1 }) — перенаправить его на { userId: 1, тс: -1 }. →
- $regex начинается с неиндексированного поля — сканирования строк из ада.
- findOneAndUpdate queue—document‐level locking bottleneck; используйте Redis/Kafka. →
- скачивание + большая страничка с отклонением — Mongo должен считать каждый пропущенный doc; переключитесь на курсоры диапазона (ts, _id). →
4 · Cost Anatomy 101
«Но Атлас говорит, что чтение дешево!»
→«Но Атлас говорит, что чтение дешево!»
Давайте сделаем математику.
Метрический
Метрический
ценность
Единая стоимость
Месячные затраты
Читать (3 к/с)
Читать (3 к/с)
7.8 Б
$0.09 / М
$702
Письменность (150 с)
380 м
$ 0,225 / м
86 долларов
$86
Передача данных
1.5 ТБ
1.5 ТБ
$ 0,25 / ГБ
$375
Объем памяти (2 Тб)
Объем памяти (2 Тб)
$ 0,24 / ГБ
$480
Всего в целом:$1,643.
Apply the fixes:
- →
- Читает упал на 70% → $210 →
- Перевод падает на 80% → $75 →
- Storage falls 60 % → $192 →
Новый счет: $ 564. Это один инженер среднего уровня или взлетно-посадочная полоса до четвертого квартала — вы выбираете.
→New bill: $564. That’s one mid‑level engineer илиПуть до Q4 — вы выбираете.
48-часовой спасательный спринт (Battle-tested Timeline)
Снижение/замена индексов низкой кардинальности.
Disk shrinks; cache hits ↑.
Час
Акция
инструмент
выиграл
выиграл
0 — 2
Обратите внимание на профиль (slowms = 50
)
Mongo shell
Шелл Mongo
Surface top 10 slow ops.
2‐6
2‐6
Перепишите N + 1 в $lookup.
VS Code + Jest tests
VS Code + Jest тесты
На 90 процентов меньше читают.
6‑10
Добавить прогнозы и ограничить неограниченные находки.
Добавить прогнозы &limit
Неограниченные находки.
Огненный лазер
API layer
RAM стабильный; API 4x быстрее.
RAM steady; API 4× faster.
10 — 16
10 — 16
Break jumbo docs → metas + GridFS/S3.
Разбивайте Jumbo Docs → Metas + GridFS/S3.
Сценарий ETL
Рабочий набор устанавливается в RAM.
Working set fits in RAM.
Drop/replace low‑cardinality indexes.
Disk shrinks; cache hits ↑.
16 - 22
Снижение/замена индексов низкой кардинальности.
Снижение/замена индексов низкой кардинальности.
компас
Disk shrinks; cache hits ↑.
Disk shrinks; cache hits ↑.
22‑30
22‑30
22‑30
Create TTLs, month‑partition cold data, enable Online Archive.
Create TTLs, month‑partition cold data, enable Online Archive.
Атлас УИ
Атлас УИ
60 % storage saved.
Сохранено 60 % хранилища.
Add Grafana panels: cache hit %, scan:ix ratio, eviction rate.
30 — 36
Add Grafana panels: cache hit %, scan:ix ratio, eviction rate.
Add Grafana panels: cache hit %, scan:ix ratio, eviction rate.
Prometheus
Visual early warnings.
Visual early warnings.
36‑48
36‑48
36‑48
Тест на загрузку с K6
Тест на загрузку с K6
k6 + Atlas metrics
k6 + атлас метрики
Confirm p95 < 150 ms @ 2× load.
Confirm p95 < 150 ms @ 2× load.
Self‑Audit Checklist—Pin It Above Your Desk
-
Largest doc ÷ median > 10? → Refactor.
→ -
Cursor returns > 1 000 docs? → Paginate.
→ -
TTL on every event/store table? (Yes/No)
→ - Любой индекс, где кардинальность < 10 %? → Drop/re‐order.
-
Profiler slowops > 1 % total ops? → Optimize or cache.
If primary cache hits remain under 90% it is wise to separate collections or add additional RAM memory post fixes.
Place the checklist on your laptop with adhesive glue after laminating it for printing.
Почему Shape Beats индексы
MongoDB’s query planner does a cost‑based search across candidate plans. The cost vector includes:
workUnits = ixScans + fetches + sorts + #docs returned
Индексы только снижаютixScans
Плохая форма инфляцииfetchesиsorts, which often dominate. Example:
db.logs.find(
{ ts: { $gte: start, $lt: end }, level: "error" }
).sort({ level: 1, ts: -1 });
Индекс{ level: 1, ts: -1 }
Планер не помогает избегать поимки каждого документа, когда он добавляет предикат к не упомянутому полю массива в ваших проекциях. Нетто-результат: 20 k поимки на 200 ударов.
Live Metrics You Should Be Watching (Графана PromQL)
# WiredTiger cache hit ratio
(rate(wiredtiger_blockmanager_blocks_read[1m]) /
(rate(wiredtiger_blockmanager_blocks_read[1m]) +
rate(wiredtiger_blockmanager_blocks_read_from_cache[1m]))
) < 0.10
Alert if > 10 % misses for 5 m.
# Docs scanned vs returned
rate(mongodb_ssm_metrics_documents[1m]{state="scanned"}) /
rate(mongodb_ssm_metrics_documents[1m]{state="returned"}) > 100
If you scan 100× more docs than you return, you’re burning money.
→Если вы сканируете в 100 раз больше документов, чем возвращаете, вы сжигаете деньги.
Hands‑On: Thin‑Slice Migration Script
Нужно взломать 1‐TBevents
collection into clicks
, views
, logins
без задержки времени? используйтеdouble‑write / backfill pattern.
// 1. Add trigger
const changeStream = db.events.watch([], { fullDocument: 'updateLookup' });
changeStream.on('change', ev => {
const dest = db[`${ev.fullDocument.type}s`];
dest.insertOne({ ...ev.fullDocument });
});
// 2. Backfill historical in chunks
let lastId = ObjectId("000000...");
while (true) {
const batch = db.events.find({_id: {$gt: lastId}}).sort({_id: 1}).limit(10_000);
if (!batch.hasNext()) break;
const docs = batch.toArray();
docs.forEach(d => db[`${d.type}s`].insertOne(d));
lastId = docs[docs.length - 1]._id;
}
Zero downtime, minimal extra storage (thanks to TTL), everyone sleeps.
Когда ShardingIs the Answer
Согласно правилу большого пальца вы должны разорвать только в том случае, еслиoneиз этих условий происходит после оптимизации вашей базы данных:
- →
- Система работает с рабочим набором, представляющим более 80 процентов оперативной памяти независимо от скорости производительности кэша. →
- Система генерирует более 15 тысяч операций в секунду в своей пиковой производительности при использовании одного основного сервера.
-
Your main priorities should be maintaining sub-70-millisecond multi-region latency because high AWS billing costs do not represent your critical concern.
→
The decision should be simple when the conditions do not match these rules.
Case Study Wrap‑Up
До этого
After
Δ
Отпечатки рук
120 Гб
Читать / Sec
600 GB
−71 %
До этого
After
Δ
Метрический
Metric
До этого
До этого
После
After
Δ
Δ
Отпечатки рук
120 Гб
Отпечатки рук
Отпечатки рук
120 Гб
120 Гб
36 Гб
−70 %
−70 %
Читать / Sec
Читать / Sec
Читать / Sec
6 700
6 700
900
−86 %
−71 %
Теплое хранение (Hot Storage)
Теплое хранение (Hot Storage)
2.1 ТБ
2.1 ТБ
600 GB
600 GB
−71 %
−71 %
p95 latency
1.9 С
−92 %
p95 latency
P95 Латентность
1.9 s
1.9 С
140 мс
−92 %
−92 %
Стоимость атласа / mo.
$15 284
$3 210
−79 %
Нет фрагментов, нет большого замораживания кода, просто безжалостная хирургическая операция.
→No shards, no major code freeze, just ruthless shape surgery.
Оригинальное название: Debt vs. Death Spiral
Требование быстрого доставки является обязательным, но поддержание некачественной работы относится к добровольному накоплению долгов. Поставщик облачных услуг взимает с вас сложные проценты, которые накапливаются с годовой процентной ставкой 1000 % на ваш неоплаченный долг. Мы изучили кредитные карты MongoDB с высоким процентом, поскольку они представляют собой пять видов преступлений, которые мы изучали.
You need to open the profiler to work with the $lookup
пистоны при добавлении пыли TTL, а затем разверните свой проект в строгом режиме. Ваша доска и ваша команда разработчиков и ваш pager в 02:17 получат качественный отдых.
Продолжайте переработка вашего кода, пока не произойдет следующий инцидент автоскалирования.