Тюнінг продуктивності MongoDB (WiredTiger, індексація)

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Тюнінг продуктивності MongoDB (WiredTiger, індексація)
Складна
~2-3 робочих дні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Тюнінг продуктивності MongoDB (WiredTiger, індексація)

MongoDB з WiredTiger як рушієм сховища — за замовчуванням виділяє 50% доступної ОЗУ під кеш. На сервері з 32 ГБ це 16 ГБ кеша. Звучить щедро, але деталі важливі: неефективні індекси, колекції без необхідних індексів, операції без hint взагалі не використовують кеш — читають з диска сторінка за сторінкою.

WiredTiger Cache: налаштування

# /etc/mongod.conf
storage:
  wiredTiger:
    engineConfig:
      # Явне вказання розміру кеша (за замовчуванням: max(50% ОЗУ - 1GB, 256MB))
      cacheSizeGB: 12  # Для сервера з 32 ГБ, залишаємо ~20 ГБ для ОС та інших процесів

      # Журнал транзакцій
      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: "ukrainian" }
)
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": "UA" } }  // ПОГАНО: 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: "UA" } },  // Фільтруємо всередині lookup
            { $project: { name: 1, email: 1 } }  // Тільки потрібні поля
        ]
    }},
    { $project: { _id: 1, total: 1, "user.name": 1 } }
])

// allowDiskUse для важких aggregation (якщо перевищує 100 МБ ОЗУ)
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" — селективність індексу

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 })  // НЕ РОБИТИ