Ответ
Да, 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');
Мои решения для предотвращения зависаний:
- Мониторинг:
// Использование 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 // Ожидание свободного соединения
});
- Ретри и 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