Статьи

MongoDB и изобразительное искусство моделирования данных

Вступление

Это третья часть нашего учебника временных рядов MongoDB, и в этом посте будет подчеркнута важность моделирования данных. Возможно, вы захотите проверить первую часть этой серии, ознакомиться с требованиями нашего виртуального проекта, а во второй — поговорить о распространенных методах оптимизации.

Когда вы впервые начнете использовать MongoDB, вы сразу заметите, что это модель данных без схемы. Но отсутствие схемы не означает пропуска правильного моделирования данных (отвечающего требованиям вашего бизнеса и производительности). В отличие от базы данных SQL, модель документов NoSQL больше ориентирована на запросы, чем на нормализацию данных. Вот почему ваш дизайн не будет завершен, если он не будет соответствовать вашим шаблонам запросов данных.

Новая модель данных

Наше предыдущее время было смоделировано так:

1
2
3
4
5
{
    "_id" : ObjectId("52cb898bed4bd6c24ae06a9e"),
    "created_on" : ISODate("2012-11-02T01:23:54.010Z")
    "value" : 0.19186609564349055
}

Мы пришли к выводу, что ObjectId работает против нас, поскольку его размер индекса составляет около 1,4 ГБ, а наша логика агрегирования данных его вообще не использует. Единственное истинное преимущество — возможность использования больших вкладышей .

Предыдущее решение использовало поле Date для хранения метки времени создания события. Это повлияло на логику группировки агрегации, которая в итоге имела следующую структуру:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"_id" : {
    "year" : {
        "$year" : [
            "$created_on"
        ]
    },
    "dayOfYear" : {
        "$dayOfYear" : [
            "$created_on"
        ]
    },
    "hour" : {
        "$hour" : [
            "$created_on"
        ]
    },
    "minute" : {
        "$minute" : [
            "$created_on"
        ]
    },
    "second" : {
        "$second" : [
            "$created_on"
        ]
    }
}

Эта группа _id требует некоторой логики приложения для получения правильной даты JSON. Мы также можем изменить поле даты create_on на числовое значение, представляющее количество миллисекунд с начала эпохи Unix. Это может стать нашим новым документом _id (который в любом случае индексируется по умолчанию).

Вот как будет выглядеть наша новая структура документа:

01
02
03
04
05
06
07
08
09
10
11
{
        "_id" : 1346895603146,
        "values" : [ 0.3992688732687384 ]
}
{
        "_id" : 1348436178673,
        "values" : [
                0.7518879524432123,
                0.0017396819312125444
        ]
}

Теперь мы можем легко извлечь ссылку на метку времени (указывающую на текущую секунду, минуту, час или день) из метки времени Unix.

Итак, если текущая временная метка 1346895603146 ​​(четверг, 06 сентября 2012, 01:40:03 146мс по Гринвичу), мы можем извлечь:

1
2
3
4
- the current second time point [Thu, 06 Sep 2012 01:40:03 GMT]: 1346895603000 = (1346895603146 – (1346895603146 % 1000))
- the current minute time point [Thu, 06 Sep 2012 01:40:00 GMT] : 1346895600000 = (1346895603146 – (1346895603146 % (60 * 1000)))
- the current hour time point [Thu, 06 Sep 2012 01:00:00 GMT] : 1346893200000 = (1346895603146 – (1346895603146 % (60 * 60 * 1000)))
- the current day time point [Thu, 06 Sep 2012 00:00:00 GMT] : 1346889600000= (1346895603146 – (1346895603146 % (24 * 60 * 60 * 1000)))

Алгоритм довольно прост, и мы можем использовать его при расчете идентификатора группы агрегации.

Эта новая модель данных позволяет нам иметь один документ на отметку времени. Каждое временное событие добавляет новое значение к массиву «values», поэтому два события, происходящие в один и тот же момент, будут совместно использовать один и тот же документ MongoDB.

Вставка тестовых данных

Все эти изменения требуют изменения скрипта импорта, который мы использовали ранее . На этот раз мы не можем использовать пакетную вставку, и мы будем использовать более реальный подход. На этот раз мы будем использовать пакетный upsert, как в следующем скрипте:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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');

Теперь пришло время вставить 50M документов.

1
2
3
Job#1 inserted 49900000 documents.
Job#1 inserted 50000000 documents.
Job#1 inserted 50000000 in 4265.45s

Вставка записей в 50 млн. Медленнее, чем в предыдущей версии, но мы все равно можем получать 10 тыс. Вставок в секунду без оптимизации записи. Для целей этого теста мы будем предполагать, что достаточно 10 событий в миллисекунду, учитывая, что с такой скоростью у нас будет 315 миллиардов документов в год.

Сжатие данных

Теперь давайте проверим статистику новой коллекции:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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
}

Базовый скрипт агрегации

Теперь пришло время создать базовый скрипт агрегирования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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
    };
}

Тестирование новой модели данных

Мы просто повторно используем тестовый фреймворк, который мы создали ранее, и нам интересно проверить два варианта использования:

  1. предварительная загрузка данных и индексов
  2. предварительная загрузка рабочего набора

Предварительная загрузка данных и индексов

1
2
3
4
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 0.012s 0.044s 0.99s
T2 0.002s 0.044s 0.964s
T3 0.001s 0.043s 0.947s
T4 0.001s 0.043s 0.936s
T4 0.001s 0.043s 0.907s
Средний 0.0034s 0.0433s 0.9488s

По сравнению с нашей предыдущей версией мы получили лучшие результаты, и это стало возможным, потому что теперь мы можем предварительно загружать как данные, так и индексы, а не только данные. Все данные и индексы соответствуют нашей 8 ГБ оперативной памяти:

mongodb_time_series_compact

Предварительная загрузка рабочего набора

1
2
3
4
5
6
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 0.003s 0.037s 0.855s
T2 0.002s 0.037s 0.834s
T3 0.001s 0.037s 0.835s
T4 0.001s 0.036s 0.84s
T4 0.002s 0.036s 0.851s
Средний 0.0018s 0.0366s 0.843s

Это лучший результат, который мы получили, и мы можем согласиться с этой новой моделью данных, поскольку она уже удовлетворяет нашим требованиям к производительности виртуального проекта.

Вывод

Это быстро или медленно?

На этот вопрос вы должны ответить сами. Производительность — это ограниченная контекстом функция. То, что быстро для одного бизнес-кейса, может быть слишком медленным для другого.

Есть одна вещь наверняка. Это почти в шесть раз быстрее, чем моя стандартная версия.

Эти цифры не предназначены для сравнения с любой другой альтернативой NoSQL или SQL. Они полезны только при сравнении версии прототипа с альтернативой оптимизированной модели данных, поэтому мы можем узнать, как моделирование данных влияет на общую производительность приложения.

  • Код доступен на GitHub .