Автор оригинала: Vlad Mihalcea.
В своих предыдущих постах я говорил о пакетном импорте и готовая производительность MongoDB . Тем временем MongoDB был удостоен награды СУБД 2013 года , поэтому я решил предложить более тщательный анализ ее использования в реальной жизни.
Поскольку теория лучше понимается в прагматическом контексте, я сначала представлю вам наши требования к виртуальному проекту.
Вступление
Наш виртуальный проект имеет следующие требования:
- в нем должны храниться значимые события времени, представленные в виде(t)
- он должен агрегировать минимальные, максимальные, средние и подсчитывать записи по:
- секунды в минуту
- минуты в час
- часы в сутках
- дней в году
- агрегирование секунд в минуту рассчитывается в режиме реального времени (поэтому оно должно быть очень быстрым).
- все остальные агрегации вычисляются с помощью пакетного процессора (поэтому они должны быть относительно быстрыми)
Модель данных
Я предложу два варианта моделирования данных, каждый из которых имеет свои плюсы и минусы.
В первой версии используется автоматически назначенный по умолчанию MongoDB “_id” , и это упрощает вставки, так как мы можем делать это пакетами, не опасаясь столкновения меток времени. Если каждую миллисекунду записывается 10 значений, то в итоге у нас будет 10 отдельных документов. В этом посте будет обсуждаться этот вариант модели данных.
Вторая версия использует количество миллисекунд с момента эпохи в качестве поля “_id” , а значения хранятся внутри массива “значения” . Если каждую миллисекунду записывается 10 значений, то в итоге у нас будет один отдельный документ с 10 записями в массиве “значения” . Будущий пост будет посвящен этой компактной модели данных.
Вставка данных
Как и в моем предыдущем посте, я буду использовать 50 миллионов документов для тестирования логики агрегирования. Я выбрал этот номер, потому что я тестирую на своем товарном ПК . В вышеупомянутом посте мне удалось вставить более 80000 документов в секунду. На этот раз я применю более реалистичный подход и начну с создания коллекции и индексов до вставки данных.
MongoDB shell version: 2.4.6 connecting to: random > db.dropDatabase() { "dropped" : "random", "ok" : 1 } > db.createCollection("randomData"); { "ok" : 1 } > db.randomData.ensureIndex({"created_on" : 1}); > db.randomData.getIndexes() [ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "random.randomData", "name" : "_id_" }, { "v" : 1, "key" : { "created_on" : 1 }, "ns" : "random.randomData", "name" : "created_on_1" } ]
Теперь пришло время вставить 50 миллионов документов.
mongo random --eval "var arg1=50000000;arg2=1" create_random.js ... Job#1 inserted 49900000 documents. Job#1 inserted 50000000 in 2852.56s
На этот раз нам удалось импортировать 17500 документов в секунду. При таких темпах нам потребуется 550B записей в год, что более чем достаточно для нашего варианта использования.
Сжатие данных
Во-первых, нам нужно проанализировать статистику нашей коллекции, и для этого нам нужно использовать статистику команда:
db.randomData.stats() { "ns" : "random.randomData", "count" : 50000000, "size" : 3200000096, "avgObjSize" : 64.00000192, "storageSize" : 5297451008, "numExtents" : 23, "nindexes" : 2, "lastExtentSize" : 1378918400, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 3497651920, "indexSizes" : { "_id_" : 1623442912, "created_on_1" : 1874209008 }, "ok" : 1 }
Текущий размер индекса составляет почти 3,5 ГБ, и это почти половина моей доступной оперативной памяти. К счастью, MongoDB поставляется с командой compact , которую мы можем использовать для дефрагментации ваших данных. Это занимает много времени, особенно потому, что у нас большой общий размер индекса.
db.randomData.runCommand("compact"); Compacting took 1523.085s
Давайте посмотрим, сколько места мы сэкономили за счет уплотнения:
db.randomData.stats() { "ns" : "random.randomData", "count" : 50000000, "size" : 3200000032, "avgObjSize" : 64.00000064, "storageSize" : 4415811584, "numExtents" : 24, "nindexes" : 2, "lastExtentSize" : 1149206528, "paddingFactor" : 1, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 2717890448, "indexSizes" : { "_id_" : 1460021024, "created_on_1" : 1257869424 }, "ok" : 1 }
Мы освободили почти 800 МБ данных, и это будет удобно для наших операций агрегирования с большим объемом оперативной памяти.
Объяснение логики агрегирования
Все четыре сводных отчета похожи, так как они отличаются только:
- интервал времени выбора
- группа по детализации по времени
Поэтому мы можем начать с первого отчета, который агрегирует значения по секундам. Мы будем использовать метод объяснить , чтобы получить представление о нашей внутренней работе агрегации.
load(pwd() + "/../../util/date_util.js"); var minDate = new Date(Date.UTC(2012, 1, 10, 11, 25, 30)); var maxDate = new Date(Date.UTC(2012, 1, 10, 11, 25, 35)); var result = db.randomData.runCommand('aggregate', { pipeline: [ { $match: { "created_on" : { $gte: minDate, $lt : maxDate } } }, { $project: { _id : 0, created_on : 1, value : 1 } }, { $group: { "_id": { "year" : { $year : "$created_on" }, "dayOfYear" : { $dayOfYear : "$created_on" }, "hour" : { $hour : "$created_on" }, "minute" : { $minute : "$created_on" }, "second" : { $second : "$created_on" }, }, "count": { $sum: 1 }, "avg": { $avg: "$value" }, "min": { $min: "$value" }, "max": { $max: "$value" } } }, { $sort: { "_id.year" : 1, "_id.dayOfYear" : 1, "_id.hour" : 1, "_id.minute" : 1, "_id.second" : 1 } } ], explain: true}); printjson(result);
Что приводит к следующему результату
{ "serverPipeline" : [ { "query" : { "created_on" : { "$gte" : ISODate("2012-02-10T11:25:30Z"), "$lt" : ISODate("2012-02-10T11:25:35Z") } }, "projection" : { "created_on" : 1, "value" : 1, "_id" : 0 }, "cursor" : { "cursor" : "BtreeCursor created_on_1", "isMultiKey" : false, "n" : 5, "nscannedObjects" : 5, "nscanned" : 5, "nscannedObjectsAllPlans" : 5, "nscannedAllPlans" : 5, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 0, "nChunkSkips" : 0, "millis" : 0, "indexBounds" : { "created_on" : [ [ ISODate("2012-02-10T11:25:30Z"), ISODate("2012-02-10T11:25:35Z") ] ] }, "allPlans" : [ { "cursor" : "BtreeCursor created_on_1", "n" : 5, "nscannedObjects" : 5, "nscanned" : 5, "indexBounds" : { "created_on" : [ [ ISODate("2012-02-10T11:25:30Z"), ISODate("2012-02-10T11:25:35Z") ] ] } } ], "oldPlan" : { "cursor" : "BtreeCursor created_on_1", "indexBounds" : { "created_on" : [ [ ISODate("2012-02-10T11:25:30Z"), ISODate("2012-02-10T11:25:35Z") ] ] } }, "server" : "VLAD:27017" } }, { "$project" : { "_id" : false, "created_on" : true, "value" : true } }, { "$group" : { "_id" : { "year" : { "$year" : [ "$created_on" ] }, "dayOfYear" : { "$dayOfYear" : [ "$created_on" ] }, "hour" : { "$hour" : [ "$created_on" ] }, "minute" : { "$minute" : [ "$created_on" ] }, "second" : { "$second" : [ "$created_on" ] } }, "count" : { "$sum" : { "$const" : 1 } }, "avg" : { "$avg" : "$value" }, "min" : { "$min" : "$value" }, "max" : { "$max" : "$value" } } }, { "$sort" : { "sortKey" : { "_id.year" : 1, "_id.dayOfYear" : 1, "_id.hour" : 1, "_id.minute" : 1, "_id.second" : 1 } } } ], "ok" : 1 }
Структура агрегации использует шаблон проектирования труб и фильтра , и наш конвейер состоит из следующих операций:
- Совпадение : Эта операция похожа на предложение WHERE SQL, и это первое, которое мы используем с тех пор, как мы используем наш индекс “created_on” (например, это подтверждается результатами объяснения: “курсор”: “BtreeCursor created_on_1” ,). Мы не используем индекс покрытия (например, “Только индекс”: false ), потому что это было бы излишним для нашей настройки 8 ГБ оперативной памяти.
- Проект : Эта операция похожа на предложение SELECT SQL и используется для удаления поля “_id” из нашего рабочего набора (что бесполезно для нашей логики отчетности).
- Группа : Эта операция похожа на предложение GROUP BY SQL, и она выполняет все вычисления в памяти. Вот почему мы отфильтровали рабочий набор перед его группировкой.
- Сортировка : Эта операция похожа на предложение ORDER BY SQL, и мы используем ее для сортировки результатов в хронологическом порядке.
Сценарий базовой агрегации
Поскольку наши четыре отчета похожи, мы можем сгруппировать всю логику в одном сценарии:
function printResult(dataSet) { dataSet.result.forEach(function(document) { printjson(document); }); } function aggregateData(fromDate, toDate, groupDeltaMillis, enablePrintResult) { print("Aggregating from " + fromDate + " to " + toDate); var start = new Date(); var groupBy = { "year" : { $year : "$created_on" }, "dayOfYear" : { $dayOfYear : "$created_on" } }; var sortBy = { "_id.year" : 1, "_id.dayOfYear" : 1 }; var appendSeconds = false; var appendMinutes = false; var appendHours = false; switch(groupDeltaMillis) { case ONE_SECOND_MILLIS : appendSeconds = true; case ONE_MINUTE_MILLIS : appendMinutes = true; case ONE_HOUR_MILLIS : appendHours = true; } if(appendHours) { groupBy["hour"] = { $hour : "$created_on" }; sortBy["_id.hour"] = 1; } if(appendMinutes) { groupBy["minute"] = { $minute : "$created_on" }; sortBy["_id.minute"] = 1; } if(appendSeconds) { groupBy["second"] = { $second : "$created_on" }; sortBy["_id.second"] = 1; } var pipeline = [ { $match: { "created_on" : { $gte: fromDate, $lt : toDate } } }, { $project: { _id : 0, created_on : 1, value : 1 } }, { $group: { "_id": groupBy, "count": { $sum: 1 }, "avg": { $avg: "$value" }, "min": { $min: "$value" }, "max": { $max: "$value" } } }, { $sort: sortBy } ]; var dataSet = db.randomData.aggregate(pipeline); var aggregationDuration = (new Date().getTime() - start.getTime())/1000; print("Aggregation took:" + aggregationDuration + "s"); if(dataSet.result != null && dataSet.result.length > 0) { print("Fetched :" + dataSet.result.length + " documents."); if(enablePrintResult) { printResult(dataSet); } } var aggregationAndFetchDuration = (new Date().getTime() - start.getTime())/1000; if(enablePrintResult) { print("Aggregation and fetch took:" + aggregationAndFetchDuration + "s"); } return { aggregationDuration : aggregationDuration, aggregationAndFetchDuration : aggregationAndFetchDuration }; }
Время для получения результатов
Давайте протестируем первые три отчета, используя следующий сценарий:
load(pwd() + "/../../util/date_util.js"); load(pwd() + "/aggregate_base_report.js"); var deltas = [ { matchDeltaMillis: ONE_MINUTE_MILLIS, groupDeltaMillis: ONE_SECOND_MILLIS, description: "Aggregate all seconds in a minute" }, { matchDeltaMillis: ONE_HOUR_MILLIS, groupDeltaMillis: ONE_MINUTE_MILLIS, description: "Aggregate all minutes in an hour" }, { matchDeltaMillis: ONE_DAY_MILLIS, groupDeltaMillis: ONE_HOUR_MILLIS, description: "Aggregate all hours in a day" } ]; var testFromDate = new Date(Date.UTC(2012, 5, 10, 11, 25, 59)); deltas.forEach(function(delta) { print('Aggregating ' + description); var timeInterval = calibrateTimeInterval(testFromDate, delta.matchDeltaMillis); var fromDate = timeInterval.fromDate; var toDate = timeInterval.toDate; aggregateData(fromDate, toDate, delta.groupDeltaMillis, true); });
Это дает нам следующие результаты:
MongoDB shell version: 2.4.6 connecting to: random Aggregating Aggregate all seconds in a minute Aggregating from Sun Jun 10 2012 14:25:00 GMT+0300 (GTB Daylight Time) to Sun Jun 10 2012 14:26:00 GMT+0300 (GTB Daylight Time) Fetched :45 documents. { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 25, "second" : 0 }, "count" : 1, "avg" : 0.4924355132970959, "min" : 0.4924355132970959, "max" : 0.4924355132970959 } { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 25, "second" : 1 }, "count" : 1, "avg" : 0.10043778014369309, "min" : 0.10043778014369309, "max" : 0.10043778014369309 } ... { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 25, "second" : 59 }, "count" : 1, "avg" : 0.16304525500163436, "min" : 0.16304525500163436, "max" : 0.16304525500163436 } Aggregating from Sun Jun 10 2012 14:00:00 GMT+0300 (GTB Daylight Time) to Sun Jun 10 2012 15:00:00 GMT+0300 (GTB Daylight Time) Fetched :60 documents. { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 0 }, "count" : 98, "avg" : 0.4758610369979727, "min" : 0.004005654249340296, "max" : 0.9938081130385399 } { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 1 }, "count" : 100, "avg" : 0.5217278444720432, "min" : 0.003654648782685399, "max" : 0.9981840122491121 } ... { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 11, "minute" : 59 }, "count" : 92, "avg" : 0.5401836506308705, "min" : 0.01764239347539842, "max" : 0.9997266652062535 } Aggregating Aggregate all hours in a day Aggregating from Sun Jun 10 2012 03:00:00 GMT+0300 (GTB Daylight Time) to Mon Jun 11 2012 03:00:00 GMT+0300 (GTB Daylight Time) Fetched :24 documents. { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 0 }, "count" : 5727, "avg" : 0.4975644027204364, "min" : 0.00020139524713158607, "max" : 0.9997993060387671 } { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 1 }, "count" : 5799, "avg" : 0.49519448930962623, "min" : 0.00011728447861969471, "max" : 0.9999530822969973 } ... { "_id" : { "year" : 2012, "dayOfYear" : 162, "hour" : 23 }, "count" : 5598, "avg" : 0.49947314951339256, "min" : 0.00009276834316551685, "max" : 0.9999523421283811 }
Следите за обновлениями, в моем следующем посте я покажу вам, как оптимизировать эти запросы агрегирования.
Код доступен на GitHub .