Может ли NoSQL база (например, MongoDB) подвиснуть на несколько секунд в Node.js приложении?

«Может ли NoSQL база (например, MongoDB) подвиснуть на несколько секунд в Node.js приложении?» — вопрос из категории Базы данных, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Да, MongoDB в Node.js приложениях действительно может испытывать задержки в несколько секунд. В моей практике это происходило по следующим причинам:

1. Проблемы с индексами:

// Проблемный запрос без индекса
const slowQuery = async () => {
  // COLLSCAN по 10M документов
  const users = await db.collection('users')
    .find({ 
      createdAt: { 
        $gte: new Date('2023-01-01'),
        $lte: new Date('2023-12-31')
      },
      status: 'active',
      'profile.city': 'Moscow'
    })
    .sort({ lastName: 1, firstName: 1 })
    .limit(100)
    .toArray(); // ~4.2 секунды
};

// Решение: составной индекс
await db.collection('users').createIndex({
  createdAt: 1,
  status: 1,
  'profile.city': 1,
  lastName: 1,
  firstName: 1
});
// Тот же запрос после индекса: ~120ms

2. Блокировки при операциях записи:

// Массовое обновление блокирует коллекцию
const bulkUpdate = async () => {
  const session = await mongoose.startSession();
  session.startTransaction();

  try {
    // Эта операция может заблокировать коллекцию на 2-3 секунды
    await User.updateMany(
      { plan: 'trial', trialEnds: { $lt: new Date() } },
      { $set: { plan: 'expired', active: false } },
      { session }
    );

    await session.commitTransaction();
  } catch (error) {
    await session.abortTransaction();
    throw error;
  } finally {
    session.endSession();
  }
};

3. Проблемы с памятью и дисковым I/O:

// Агрегация, превышающая лимит памяти
const heavyAggregation = async () => {
  const result = await db.orders.aggregate([
    { $match: { status: 'completed', date: { $gte: new Date('2023-01-01') } } },
    { $group: {
        _id: '$userId',
        totalAmount: { $sum: '$amount' },
        orderCount: { $sum: 1 },
        items: { $push: '$items' }
    }},
    { $sort: { totalAmount: -1 } },
    { $limit: 1000 }
  ], { 
    allowDiskUse: true, // Вынужденное использование диска
    maxTimeMS: 30000   // Таймаут 30 секунд
  }).toArray();
};

4. Репликация и шардинг:

// Запрос к secondary реплике с задержкой
const mongoose = require('mongoose');

// Настройка чтения с secondary
mongoose.connect('mongodb://primary,secondary1,secondary2/db', {
  replicaSet: 'rs0',
  readPreference: 'secondary', // Чтение с реплики
  maxStalenessSeconds: 120,    // Допустимая задержка репликации
});

// Если реплика отстает более чем на 120 секунд,
// запрос будет ждать или вернет ошибку
const data = await Model.find({}).read('secondary');

Мои решения для предотвращения зависаний:

  1. Мониторинг:
    
    // Использование mongodb-top и кастомных метрик
    const { MongoClient } = require('mongodb');

const client = new MongoClient(uri, { monitorCommands: true, });

client.on('commandStarted', (event) => { console.log(Query started: ${event.commandName}); });

client.on('commandSucceeded', (event) => { console.log(Query succeeded in ${event.duration}ms); // Отправка метрик в Prometheus if (event.duration > 1000) { console.warn('Slow query detected:', event.commandName); } });


2. **Оптимизация пула соединений:**
```javascript
// Настройка Mongoose
mongoose.connect(uri, {
  poolSize: 10,           // Максимум соединений
  maxPoolSize: 20,        // При нагрузке
  minPoolSize: 5,         // Минимум в пуле
  maxIdleTimeMS: 30000,   // Закрыть неиспользуемые через 30s
  waitQueueTimeoutMS: 5000 // Ожидание свободного соединения
});
  1. Ретри и fallback стратегии:
    
    const { retry } = require('async');

const queryWithRetry = async (queryFn, maxAttempts = 3) => { return retry({ times: maxAttempts, interval: (retryCount) => 100 * Math.pow(2, retryCount), errorFilter: (err) => { // Повторяем только при временных ошибках return err.code === 16500 || // TooManyRequests err.message.includes('connection pool') || err.name === 'MongoNetworkError'; } }, queryFn); };

// Использование const result = await queryWithRetry(async () => { return await db.collection('data').find({}).toArray(); });



**Выводы из production инцидентов:**
- Самые частые причины: отсутствие индексов + большие операции записи
- Регулярный анализ slow queries через `db.currentOp()` и `$indexStats`
- Автоматическое алертирование при 95-м перцентиле latency > 500ms