Производительность агрегирования MongoDB медленнее, чем миллионы документов
фон
Наша система носит носитель и очень надежна, она тестировалась на нагрузку для обработки 5000 транзакций в секунду, и для каждой транзакции документ вставляется в единую коллекцию MongoDB (без обновлений или запросов в этом приложении, только).
Это составляет ~ 700 млн. Документов в день, что является нашим эталоном.
Развертывание MongoDB еще не очерчено, у нас есть 1x replicaset с 1 master и 2 slaves, все из которых являются типами m2.2xlarge экземпляров на ec2.
Каждый экземпляр поддерживается 1ТБ RAID0-полосой, состоящей из 8 томов (без PIOPS).
Мы используем родной драйвер node -mongodb с собственным синтаксическим анализатором BSON С++ для оптимальной производительности записи и попытались соответствующим образом моделировать структуру документа.
Примечание
- Документы крошечные (120 байт)
- Документ включает в себя "ведро времени" (h [наш], d [ay], m [onth], y [ear]) вместе с полем "t [ime]"
- У нас есть индекс в коллекции для запроса с помощью "c [ustomer]" и "a", который является очень случайным, но не уникальным тегом
- Мы рассмотрели разделение данных на отдельные коллекции, хотя в этом примере все данные горячие.
- Мы также изучаем предварительную агрегацию, хотя это невозможно сделать в реальном времени.
требование
- Для отчетности нам нужно рассчитать количество уникальных "а" тегов в месяц вместе с их итогами клиентом за любой данный период.
- Отчет занимает около 60 секунд для запуска образца (полной коллекции) документов 9.5MM, хранящихся в течение 2 часов. Подробности ниже:
Документ
{
_id: ObjectID(),
a: ‘string’,
b: ‘string’,
c: ‘string’ or <int>,
g: ‘string’ or <not_exist>,
t: ISODate(),
h: <int>,
d: <int>,
m: <int>,
y: <int>
}
Индекс
col.ensureIndex({ c: 1, a: 1, y: 1, m: 1, d: 1, h: 1 });
запрос агрегации
col.aggregate([
{ $match: { c: 'customer_1', y: 2013, m: 11 } },
{ $group: { _id: { c: '$c', y: '$y', m: '$m' }, a: { $addToSet: '$a' }, t: { $sum: 1 } } },
{ $unwind: '$a' },
{ $group: { _id: { c: '$_id.c', y: '$_id.y', m: '$_id.m', t: '$t' }, a: { $sum: 1 } } },
{ $sort: { '_id.m': 1 } },
{
$project: {
_id: 0,
c: '$_id.c',
y: '$_id.y',
m: '$_id.m',
a: 1,
t: '$_id.t'
}
},
{ $group: { _id: { c: '$c', y: '$y' }, monthly: { $push: { m: '$m', a: '$a', t: '$t' } } } },
{ $sort: { '_id.y': 1 } },
{
$project: {
_id: 0,
c: '$_id.c',
y: '$_id.y',
monthly: 1
}
},
{ $group: { _id: { c: '$c' }, yearly: { $push: { y: '$y', monthly: '$monthly' } } } },
{ $sort: { '_id.c': 1 } },
{
$project: {
_id: 0,
c: '$_id.c',
yearly: 1
}
}
]);
результат агрегации
[
{
"yearly": [
{
"y": 2013,
"monthly": [
{
"m": 11,
"a": 3465652,
"t": 9844935
}
]
}
],
"c": "customer_1"
}
]
63181ms
Объяснение агрегации
{
"cursor" : "BtreeCursor c_1_a_1_y_1_m_1_d_1_h_1",
"isMultiKey" : false,
"n" : 9844935,
"nscannedObjects" : 0,
"nscanned" : 9844935,
"nscannedObjectsAllPlans" : 101,
"nscannedAllPlans" : 9845036,
"scanAndOrder" : false,
"indexOnly" : true,
"nYields" : 27,
"nChunkSkips" : 0,
"millis" : 32039,
"indexBounds" : {
"c" : [ [ "customer_1", "customer_1" ] ],
"a" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ],
"y" : [ [ 2013, 2013 ] ],
"m" : [ [ 11, 11 ] ],
"d" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ],
"h" : [ [ { "$minElement" : 1 }, { "$maxElement" : 1 } ] ]
}
}
вопросы
-
Учитывая высокую частоту вставок и нашу потребность в выполнении запросов агрегирования рангов с течением времени. Является ли практика времени хорошей практикой, учитывая, что приложение может вставлять 30MM-документы за один час?
-
Мы понимали, что MongoDB может запрашивать миллиарды документов за считанные секунды:
- Несомненно, наш запрос агрегирования по документам 9.5MM может вернуться через 1сек или около того?
- Используем ли мы правильную технику для достижения этого, а если нет, то где мы должны сосредоточить наши усилия на получении результатов отчета почти мгновенно?
- Возможно ли это без осколков на этом этапе?
-
Будет ли MapReduce (параллельно) лучше альтернативой?
Ответы
Ответ 1
Вы задаетесь вопросом, почему ваша агрегировка занимает так много времени. Помимо пунктов, сделанных @Avish в его ответе (вы делаете некоторые ненужные шаги), вам также нужно учитывать ваши физические ресурсы и время, в которое идет время.
Вот часть вашего объяснения:
"n" : 9844935,
"nscannedObjects" : 0,
"nscanned" : 9844935,
"scanAndOrder" : false,
"indexOnly" : true,
"nYields" : 27,
"millis" : 32039,
Следует отметить, что агрегация заняла 32 секунды (не 50), ей никогда не приходилось извлекать один документ, поскольку он получил всю информацию из самого индекса. Это не нужно было делать в памяти. Но он должен был дать 27 раз... Почему? Есть две причины, по которым возникают процессы чтения: один - когда есть ожидание записи (авторы имеют приоритет, а длинные чтения должны уступать им), либо произошла ошибка страницы - все операции должны выполняться, когда любые данные, к которым они пытаются получить доступ не находится в ОЗУ (это значит, что процесс не блокирует других от выполнения работы, пока они ожидают, что их данные будут загружены в ОЗУ).
Вопросы, которые приходят на ум, были: был ли холод DB? Соответствуют ли индексы в ОЗУ? Были ли записи в то же время, что читатели должны были бороться?
Я бы проверял, что индекс может поместиться в ОЗУ, запустите команду "touch" , чтобы убедиться, что она находится в ОЗУ, упростить мою агрегацию конвейер не делать ненужной работы, а затем запускать его снова, пару раз подряд и посмотреть, как выглядят тайминги.
Ответ 2
Я не понимаю, почему вам нужно $unwind
значения a
и почему вам нужно группировать по сумме в любой точке. Это также кажется ошибкой, так как для каждого разворотного значения a
вы выведете то же значение t
, рассчитанное для всего времени.
Насколько я понимаю, ваш запрос должен выглядеть так:
col.aggregate([
// Pre-filter
{ $match: { /* ... */ } },
// Pre-sort to aid in grouping
{ $sort: { c: 1, y: 1, m: 1, a: 1 },
// Group by month, customer and `a` to find unique `a` values and their totals
{ $group: {
_id: { c: '$c', y: '$y', m: '$m', a: '$a' },
t: { $sum: 1 }
}
},
// Not sure if another sort is required at this point, I'd assume MongoDB
// is smart enough to understand we're grouping by a subset of the original
// grouping key
// Group by month and customer to count unique `a` values and grand total
{ $group: {
_id: { c: '$_id.c', y: '$_id.y', m: '$_id.m' },
a: { sum: 1 }, // number of unique `a` values in group
t: { sum: '$t' } // rolled-up total of all `a`-totals in group
},
// You can tack on further groupings by year and customer here,
// although I believe these would be better done in the UI layer
]);
В принципе, начало конвейера с разматыванием и повторной группировкой, а также промежуточные сорта могут замедлить вас. Посмотрите, работает ли эта версия лучше, и попытайтесь добавить сортировки между группировками, если они помогут.
Ответ 3
Я бы предложил попробовать индекс на y, m и d (год, месяц, дата?) в этом порядке, поскольку они известны как int, а не текущие, которые c могут быть int или строку. Поскольку данные основаны на времени, может также иметь смысл.