Автор оригинала: Vlad Mihalcea.
Вступление
Это третья часть нашего руководства по временным рядам MongoDB, и в этом посте будет подчеркнута важность моделирования данных. Возможно, вы захотите ознакомиться с первой частью этой серии, чтобы ознакомиться с требованиями к нашему виртуальному проекту, а во второй части рассказывается об общих методах оптимизации.
Когда вы впервые начнете использовать MongoDB, вы сразу заметите, что это модель данных без схемы. Но отсутствие схемы не означает пропуск надлежащего моделирования данных (удовлетворяющего бизнесу вашего приложения и требованиям к производительности). В отличие от базы данных SQL, модель документов NoSQL больше ориентирована на запросы, чем на нормализацию данных. Вот почему ваш дизайн не будет завершен, если в нем не будут учтены ваши шаблоны запросов к данным.
Новая модель данных
Наше предыдущее событие было смоделировано следующим образом:
{ "_id" : ObjectId("52cb898bed4bd6c24ae06a9e"), "created_on" : ISODate("2012-11-02T01:23:54.010Z") "value" : 0.19186609564349055 }
Мы пришли к выводу, что ObjectId работает против нас, поскольку его размер индекса составляет около 1,4 ГБ, а наша логика агрегирования данных его вообще не использует. Единственное истинное преимущество для его наличия заключается в возможности использования объемных вставок .
Предыдущее решение использовало поле даты для хранения метки времени создания события. Это повлияло на логику группировки агрегации, которая привела к следующей структуре:
"_id" : { "year" : { "$year" : [ "$created_on" ] }, "dayOfYear" : { "$dayOfYear" : [ "$created_on" ] }, "hour" : { "$hour" : [ "$created_on" ] }, "minute" : { "$minute" : [ "$created_on" ] }, "second" : { "$second" : [ "$created_on" ] } }
Эта группа _id требует некоторой прикладной логики для получения надлежащих данных JSON. Мы также можем изменить поле created_on Дата на числовое значение, представляющее количество миллисекунд с эпохи Unix. Это может стать нашим новым документом _id (который все равно индексируется по умолчанию).
Вот как будет выглядеть наша новая структура документов:
{ "_id" : 1346895603146, "values" : [ 0.3992688732687384 ] } { "_id" : 1348436178673, "values" : [ 0.7518879524432123, 0.0017396819312125444 ] }
Теперь мы можем легко извлечь ссылку на метку времени (указывающую на текущую секунду, минуту, час или день) из метки времени Unix.
Итак, если текущая метка времени 1346895603146 (Чт, 06 сентября 2012 01:40:03 146 мс по Гринвичу), мы можем извлечь:
- текущий второй момент времени [Чт, 06 сентября 2012 01:40:03 GMT]: 1346895603000 = (1346895603146 – (1346895603146 % 1000))
- текущая минута времени [Чт, 06 Сентябрь 2012 01:40:00 GMT]: 1346895600000 = (1346895603146 – (1346895603146 % (60 * 1000)))
- текущий часовой момент времени [Чт, 06 Сентябрь 2012 01:00:00 GMT]: 1346893200000 = (1346895603146 – (1346895603146 % (60 * 60 * 1000)))
- время текущего дня [Чт, 06 Сентябрь 2012 00:00:00 GMT]: 1346889600000= (1346895603146 – (1346895603146 % (24 * 60 * 60 * 1000)))
Алгоритм довольно прост, и мы можем использовать его при вычислении идентификатора группы агрегации.
Эта новая модель данных позволяет нам иметь по одному документу на метку времени. Каждое событие времени добавляет новое значение в массив “значения”, поэтому два события, происходящие в одно и то же мгновение, будут использовать один и тот же документ MongoDB.
Вставка тестовых данных
Все эти изменения требуют изменения сценария импорта, который мы использовали ранее . На этот раз мы не можем использовать пакетную вставку, и мы воспользуемся более реальным подходом. На этот раз мы будем использовать не пакетированный расстроенный , как в следующем сценарии:
var minDate = new Date(2012, 0, 1, 0, 0, 0, 0); var maxDate = new Date(2013, 0, 1, 0, 0, 0, 0); var delta = maxDate.getTime() - minDate.getTime(); var job_id = arg2; var documentNumber = arg1; var batchNumber = 5 * 1000; var job_name = 'Job#' + job_id var start = new Date(); var index = 0; while(index < documentNumber) { var date = new Date(minDate.getTime() + Math.random() * delta); var value = Math.random(); db.randomData.update( { _id: date.getTime() }, { $push: { values: value } }, true ); index++; if(index % 100000 == 0) { print(job_name + ' inserted ' + index + ' documents.'); } } print(job_name + ' inserted ' + documentNumber + ' in ' + (new Date() - start)/1000.0 + 's');
Теперь пришло время вставить 50 миллионов документов.
Job#1 inserted 49900000 documents. Job#1 inserted 50000000 documents. Job#1 inserted 50000000 in 4265.45s
Вставка 50 млн записей выполняется медленнее, чем в предыдущей версии, но мы все равно можем получать 10 тыс. вставок в секунду без какой-либо оптимизации записи. Для целей этого теста мы будем считать, что 10 событий в миллисекунду достаточно, учитывая, что при такой скорости у нас в конечном итоге будет 315 миллиардов документов в год.
Сжатие данных
Теперь давайте проверим статистику новой коллекции:
db.randomData.stats(); { "ns" : "random.randomData", "count" : 49709803, "size" : 2190722612, "avgObjSize" : 44.070233229449734, "storageSize" : 3582234624, "numExtents" : 24, "nindexes" : 1, "lastExtentSize" : 931495936, "paddingFactor" : 1.0000000000429572, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 1853270272, "indexSizes" : { "_id_" : 1853270272 }, "ok" : 1 }
Размер документа уменьшился с 64 до 44 байт, и на этот раз у нас есть только один индекс. Мы можем еще больше уменьшить размер коллекции, если используем команду compact .
db.randomData.runCommand("compact"); { "ns" : "random.randomData", "count" : 49709803, "size" : 2190709456, "avgObjSize" : 44.06996857340191, "storageSize" : 3267653632, "numExtents" : 23, "nindexes" : 1, "lastExtentSize" : 851263488, "paddingFactor" : 1.0000000000429572, "systemFlags" : 1, "userFlags" : 0, "totalIndexSize" : 1250568256, "indexSizes" : { "_id_" : 1250568256 }, "ok" : 1 }
Сценарий базовой агрегации
Теперь пришло время создать базовый сценарий агрегации:
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 pipeline = [ { $match:{ "_id":{ $gte: fromDate.getTime(), $lt : toDate.getTime() } } }, { $unwind:"$values" }, { $project:{ timestamp:{ $subtract:[ "$_id", { $mod:[ "$_id", groupDeltaMillis ] } ] }, value : "$values" } }, { $group: { "_id": { "timestamp" : "$timestamp" }, "count": { $sum: 1 }, "avg": { $avg: "$value" }, "min": { $min: "$value" }, "max": { $max: "$value" } } }, { $sort: { "_id.timestamp" : 1 } } ]; 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 }; }
Тестирование новой модели данных
Мы просто повторно используем тестовую платформу, которую мы создали ранее, и мы заинтересованы в проверке двух вариантов использования:
- предварительная загрузка данных и индексов
- предварительная загрузка рабочего набора
Предварительная загрузка данных и индексов
D:\wrk\vladmihalcea\vladmihalcea.wordpress.com\mongodb-facts\aggregator\timeseries>mongo random touch_index_data.js MongoDB shell version: 2.4.6 connecting to: random Touch {data: true, index: true} took 17.351s
T1 | Средний | Средний | Средний |
T2 | Средний | Средний | Средний |
T3 | Средний | Средний | Средний |
T4 | Средний | Средний | Средний |
T4 | Средний | Средний | Средний |
Средний | Средний | Средний | Средний |
По сравнению с нашей предыдущей версией мы получили лучшие результаты, и это было возможно, потому что мы не могли предварительно загрузить как данные, так и индексы, а не просто данные. Все данные и индексы соответствуют нашей 8 ГБ оперативной памяти:
Предварительная загрузка рабочего набора
D:\wrk\vladmihalcea\vladmihalcea.wordpress.com\mongodb-facts\aggregator\timeseries>mongo random compacted_aggregate_year_report.js MongoDB shell version: 2.4.6 connecting to: random Aggregating from Sun Jan 01 2012 02:00:00 GMT+0200 (GTB Standard Time) to Tue Jan 01 2013 02:00:00 GMT+0200 (GTB Standard Time) Aggregation took:307.84s Fetched :366 documents.
T1 | Средний | Средний | Средний |
T2 | Средний | Средний | Средний |
T3 | Средний | Средний | Средний |
T4 | Средний | Средний | Средний |
T4 | Средний | Средний | Средний |
Средний | Средний | Средний | Средний |
Это лучший результат, который у нас есть, и мы можем согласиться с этой новой моделью данных, поскольку она уже удовлетворяет нашим требованиям к производительности виртуального проекта.
Вывод
Это быстро или медленно?
На этот вопрос вам придется ответить самому. Производительность – это функция, ограниченная контекстом. То, что быстро для данного бизнес-кейса, может быть чрезвычайно медленным для другого.
Есть одна вещь, в которой можно быть уверенным. Это почти в шесть раз быстрее, чем моя готовая версия.
Эти цифры не предназначены для сравнения с какой-либо другой альтернативой NoSQL или SQL. Они полезны только при сравнении версии прототипа с альтернативой оптимизированной модели данных, поэтому мы можем узнать, как моделирование данных влияет на общую производительность приложения.
Код доступен на GitHub .