Тюнинг производительности MongoDB (WiredTiger, индексация)
MongoDB с WiredTiger как движком хранилища — по умолчанию выделяет 50% доступной RAM под кэш. На сервере с 32 ГБ это 16 ГБ кэша. Звучит щедро, но дьявол в деталях: неэффективные индексы, коллекции без нужных индексов, операции без hint вообще не используют кэш — они читают с диска страницу за страницей.
WiredTiger Cache: настройка
# /etc/mongod.conf
storage:
wiredTiger:
engineConfig:
# Явное указание размера кэша (по умолчанию: max(50% RAM - 1GB, 256MB))
cacheSizeGB: 12 # Для сервера 32 GB, оставляем ~20 GB для ОС и других процессов
# Журнал транзакций
journalCompressor: snappy # snappy быстрее zlib, чуть меньше сжатие
collectionConfig:
# Алгоритм сжатия данных
blockCompressor: snappy # snappy — баланс скорость/сжатие
indexConfig:
# Сжатие индексов
prefixCompression: true # экономит место в памяти и на диске
Мониторинг состояния кэша:
const cacheStats = db.serverStatus().wiredTiger.cache
printjson({
"cache_size_MB": Math.round(cacheStats["maximum bytes configured"] / 1024 / 1024),
"currently_in_cache_MB": Math.round(cacheStats["bytes currently in the cache"] / 1024 / 1024),
"dirty_bytes_MB": Math.round(cacheStats["tracked dirty bytes in the cache"] / 1024 / 1024),
"pages_read_from_disk": cacheStats["pages read into cache"],
"pages_evicted": cacheStats["pages evicted by application threads"],
// Если evicted > 0 — кэш под давлением, возможно нужно увеличить cacheSizeGB
})
Стратегия индексирования
Индексы — главный инструмент производительности. Без индекса — COLLSCAN, полный перебор каждого документа.
Правило ESR (Equality, Sort, Range) для составных индексов:
// Запрос: найти активные заказы пользователя, отсортировать по дате
db.orders.find({
user_id: ObjectId("..."), // Equality
status: "active" // Equality (второй)
}).sort({ created_at: -1 }) // Sort
// Правильный индекс: сначала equality-поля, потом sort
db.orders.createIndex(
{ user_id: 1, status: 1, created_at: -1 },
{ name: "idx_user_status_date", background: true }
)
// НЕПРАВИЛЬНО: range перед sort
// { user_id: 1, created_at: -1, status: 1 } — sort не использует индекс
Индексы для специфичных паттернов
// Мультиключевой индекс для массивов тегов
db.articles.createIndex({ tags: 1 })
// Документ: { tags: ["nodejs", "mongodb", "backend"] }
// Запрос: db.articles.find({ tags: "mongodb" }) — использует индекс
// Text индекс для полнотекстового поиска
db.articles.createIndex(
{ title: "text", body: "text" },
{ weights: { title: 10, body: 1 }, default_language: "russian" }
)
db.articles.find({ $text: { $search: "индексирование MongoDB" } })
// Sparse индекс — только документы с полем (экономит место)
db.users.createIndex(
{ phone: 1 },
{ sparse: true, unique: true }
)
// 2dsphere для геолокации
db.stores.createIndex({ location: "2dsphere" })
db.stores.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [37.618, 55.752] },
$maxDistance: 5000 // 5 км
}
}
})
Aggregation Pipeline: оптимизация
// Медленно: $match после $lookup
db.orders.aggregate([
{ $lookup: { from: "users", localField: "user_id", foreignField: "_id", as: "user" } },
{ $match: { "user.country": "RU" } } // ПЛОХО: lookup всей коллекции
])
// Быстро: $match как можно раньше в пайплайне
db.orders.aggregate([
{ $match: { status: "completed", created_at: { $gte: ISODate("2025-01-01") } } },
// ^ Использует индекс, фильтрует ДО lookup
{ $lookup: {
from: "users",
localField: "user_id",
foreignField: "_id",
as: "user",
pipeline: [
{ $match: { country: "RU" } }, // Фильтруем внутри lookup
{ $project: { name: 1, email: 1 } } // Только нужные поля
]
}},
{ $project: { _id: 1, total: 1, "user.name": 1 } }
])
// allowDiskUse для тяжёлых aggregation (если превышает 100 MB RAM)
db.orders.aggregate([...], { allowDiskUse: true })
Профайлер и анализ запросов
// Включить профайлер для медленных операций
db.setProfilingLevel(1, { slowms: 50 })
// Анализ результатов
db.system.profile.aggregate([
{ $match: { millis: { $gt: 50 } } },
{ $group: {
_id: "$command.filter",
count: { $sum: 1 },
avg_ms: { $avg: "$millis" },
max_ms: { $max: "$millis" }
}},
{ $sort: { avg_ms: -1 } },
{ $limit: 10 }
])
// Принудительный explain для подозрительного запроса
db.orders.find({ status: "pending" }).explain("allPlansExecution")
// "winningPlan" — выбранный план
// "rejectedPlans" — альтернативы, которые рассматривались
// "executionStats.totalDocsExamined" vs "nReturned" — selectivity индекса
Aggregation: $facet для параллельных пайплайнов
// Вместо нескольких отдельных запросов — один $facet
db.products.aggregate([
{ $match: { category: "electronics", price: { $gte: 1000 } } },
{ $facet: {
"total_count": [{ $count: "count" }],
"by_brand": [{ $group: { _id: "$brand", count: { $sum: 1 } } }, { $sort: { count: -1 } }],
"price_stats": [{ $group: { _id: null, min: { $min: "$price" }, max: { $max: "$price" }, avg: { $avg: "$price" } } }],
"paginated": [{ $sort: { price: 1 } }, { $skip: 0 }, { $limit: 20 }]
}}
])
// Один round-trip вместо четырёх запросов
Read Preference на Replica Set
// Аналитические запросы — на secondary, без нагрузки на primary
const client = new MongoClient(uri, {
readPreference: ReadPreference.SECONDARY_PREFERRED
})
// Или для конкретного запроса
db.orders.find({ status: "completed" })
.readPreference("secondaryPreferred")
.hint({ status: 1, created_at: -1 })
Шардирование: когда и как
Шардинг добавляет сложность — нужен mongos, config servers, выбор shard key. Имеет смысл при:
- Данных > 100–200 ГБ, которые не помещаются на один сервер
- Записи > 10 000 RPS, которые не справляется обработать один primary
sh.enableSharding("mydb")
// Ключ шардирования: высокая кардинальность, монотонный рост плохо
// Хорошо: хеш от user_id (равномерное распределение)
sh.shardCollection("mydb.events", { user_id: "hashed" })
// Плохо: монотонно растущий _id (все записи идут в один шард)
// sh.shardCollection("mydb.events", { _id: 1 }) // НЕ ДЕЛАТЬ
Практический чеклист тюнинга
-
cacheSizeGBустановлен явно исходя из RAM сервера - Все регулярные запросы покрыты индексами (проверка через profiler)
- Нет индексов с
accesses.ops == 0(удалить неиспользуемые) -
$matchстоит первым в aggregation pipeline - Аналитика читается с secondary
-
allowDiskUseвключён только там, где реально нужно - Размер oplog покрывает минимум 4–8 часов операций
- WiredTiger checkpoint interval = 60s (дефолт, обычно не менять)







