Рубрики
Без рубрики

Руководство для начинающих по турбонаддуву MongoDB performance

Автор оригинала: Vlad Mihalcea.

Вступление

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

Короче говоря, у нас есть 50-миллионные события, охватывающие период с 1 января 2012 года по 1 января 2013 года, со следующей структурой:

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

Мы хотели бы объединить минимальное, максимальное и среднее значение, а также количество записей для следующих дискретных временных выборок:

  1. все секунды за минуту
  2. все минуты за час
  3. все часы в сутках

Вот как выглядит наш базовый тестовый сценарий:

var testFromDates = [
    new Date(Date.UTC(2012, 5, 10, 11, 25, 59)),
    new Date(Date.UTC(2012, 7, 23, 2, 15, 07)),
    new Date(Date.UTC(2012, 9, 25, 7, 18, 46)),
    new Date(Date.UTC(2012, 1, 27, 18, 45, 23)),
    new Date(Date.UTC(2012, 11, 12, 14, 59, 13))
];

function testFromDatesAggregation(
        matchDeltaMillis, 
        groupDeltaMillis, 
        type, 
        enablePrintResult) {
    
    var aggregationTotalDuration = 0;
    var aggregationAndFetchTotalDuration = 0;
    
    testFromDates.forEach(function(testFromDate) {  
        var timeInterval = calibrateTimeInterval(
            testFromDate, 
            matchDeltaMillis
        );
        
        var fromDate = timeInterval.fromDate;
        var toDate = timeInterval.toDate;
        
        var duration = aggregateData(
            fromDate, 
            toDate, 
            groupDeltaMillis, 
            enablePrintResult
        );
        
        aggregationTotalDuration += 
            duration.aggregationDuration;
        
        aggregationAndFetchTotalDuration += 
            duration.aggregationAndFetchDuration;       
    });
    
    print(
        type + 
        " aggregation took:" + 
        aggregationTotalDuration/testFromDates.length + 
        "s"
    );
    
    if(enablePrintResult) {
        print(
            type + 
            " aggregation and fetch took:" + 
            aggregationAndFetchTotalDuration/testFromDates.length + 
            "s"
        );
    }
}

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

testFromDatesAggregation(
    ONE_MINUTE_MILLIS, 
    ONE_SECOND_MILLIS, 
    'One minute seconds'
);

testFromDatesAggregation(
    ONE_HOUR_MILLIS, 
    ONE_MINUTE_MILLIS, 
    'One hour minutes'
);

testFromDatesAggregation(
    ONE_DAY_MILLIS, 
    ONE_HOUR_MILLIS, 
    'One year days'
);

Мы используем пять отметок времени начала, и они используются для расчета текущего интервала времени тестирования по заданной детализации времени.

Первая отметка времени (например, T1) – Солнце 10 июня 2012 г. 14:25:59 GMT+0300 (летнее время GTB), и соответствующие интервалы времени тестирования:

  1. все секунды в минуте: [ Вс 10 июня 2012 14:25:00 GMT+0300 (летнее время GTB) , Вс 10 июня 2012 14:26:00 GMT+0300 (летнее время GTB) )
  2. все минуты в час: [ Вс 10 июня 2012 14:00:00 GMT+0300 (летнее время GTB) , Вс 10 июня 2012 15:00:00 GMT+0300 (летнее время GTB) )
  3. все часы в сутках: [ Вс 10 июня 2012 03:00:00 GMT+0300 (летнее время GTB) , Пн 11 июня 2012 03:00:00 GMT+0300 (летнее время GTB) )

Холодное тестирование базы данных

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

T1 Средний Средний Средний
T2 Средний Средний Средний
T3 Средний Средний Средний
T4 Средний Средний Средний
T4 Средний Средний Средний
Средний Средний Средний Средний

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

Теплое тестирование базы данных

Разогрев индексов и данных-распространенный метод, используемый как для систем управления базами данных SQL, так и для NoSQL. MongoDB предлагает для этой цели команду touch . Но это не волшебная палочка, вы не используете ее вслепую в надежде оставить все свои проблемы с производительностью позади. Злоупотребляйте им, и производительность вашей базы данных резко снизится, поэтому убедитесь, что вы понимаете свои данные и их использование.

Команда touch позволяет нам указать, что мы хотим предварительно загрузить:

  • данные
  • индексы
  • как данные, так и индексы

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

Размер данных

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

> db.randomData.dataSize()
3200000032
> db.randomData.totalIndexSize()
2717890448
> db.randomData.totalSize()
7133702032

Размер данных составляет около 3 ГБ, в то время как общий размер составляет почти 7 ГБ. Если я решу предварительно загрузить все данные и индексы, я достигну предела оперативной памяти в 8 ГБ на текущей рабочей станции, на которой я выполняю тесты. Это приведет к замене и снижению производительности.

Принося больше вреда, чем пользы

Чтобы повторить этот сценарий, я собираюсь перезапустить сервер MongoDB и выполнить следующую команду:

db.runCommand({ 
    touch: "randomData", 
    data: true, 
    index: true 
});

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

> mongo random touch_index_data.js

MongoDB shell version: 2.4.6
connecting to: random
Touch {data: true, index: true} took 15.897s

А теперь давайте повторим наши тесты и посмотрим, что мы получим на этот раз:

T1 Средний Средний Средний
T2 Средний Средний 0
T3 Средний Средний Средний
T4 Средний Средний Средний
T4 Средний Средний Средний
Средний Средний Средний Средний

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

Это снимок использования памяти для данного конкретного случая использования:

Чтобы узнать больше об этой теме, я рекомендую потратить некоторое время на чтение о хранилище MongoDB внутренней работе.

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

Как я уже говорил ранее, вам необходимо знать как доступные методы оптимизации, так и ваше конкретное использование данных. В нашем проекте, как я объяснял в своем предыдущем посте , мы используем индекс только на этапе сопоставления. Во время извлечения данных мы также загружаем значения, которые не индексируются. Поскольку размер данных полностью помещается в оперативную память, мы можем выбрать только предварительную загрузку данных, оставив индексы в стороне.

Это хороший выбор, учитывая наши текущие индексы коллекций:

"indexSizes" : {
    "_id_" : 1460021024,
    "created_on_1" : 1257869424
}

Нам вообще не нужен индекс _id, и в нашем конкретном случае его загрузка фактически снижает производительность. Итак, на этот раз мы загружаем только предварительные данные.

db.runCommand({ 
    touch: "randomData", 
    data: true, 
    index: false 
});
> mongo random touch_data.j

MongoDB shell version: 2.4.6
connecting to: random
Touch {data: true} took 14.025s

Повторный запуск всех тестов дает следующие результаты:

T1 Средний Средний 0
T2 Средний Средний 0
T3 Средний Средний Средний
T4 Средний Средний 0
T4 Средний Средний 0
Средний Средний Средний Средний

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

Мы можем предварительно загрузить весь рабочий набор в фоновом режиме, и это определенно должно улучшить все наши агрегации.

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

Для этого я написал следующий сценарий:

load(pwd() + "/../../util/date_util.js");
load(pwd() + "/aggregate_base_report.js");
var minDate = new Date(Date.UTC(2012, 0, 1, 0, 0, 0, 0));
var maxDate = new Date(Date.UTC(2013, 0, 1, 0, 0, 0, 0));
var one_year_millis = (maxDate.getTime() - minDate.getTime());
aggregateData(minDate, maxDate, ONE_DAY_MILLIS);

Это позволит агрегировать данные за год и агрегировать их за каждый день года:

> mongo random 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:299.666s

Fetched: 366 documents.

Выполнение всех тестов дает наилучшие результаты на данный момент:

T1 Средний Средний 0
T2 Средний Средний 0
T3 Средний Средний 0
T4 Средний Средний 0
T4 Средний Средний 0
Средний Средний Средний 0

Давайте проверим наш текущий рабочий набор объем памяти.

db.serverStatus( { workingSet: 1 } );
...
"workingSet" : {
    "note" : "thisIsAnEstimate",
    "pagesInMemory" : 1130387,
    "computationTimeMicros" : 253497,
    "overSeconds" : 723
}

Это оценка, и каждая страница памяти составляет около 4 к, поэтому наш предполагаемый рабочий набор составляет около 4 к *.31 ГБ, что гарантирует, что текущий рабочий набор соответствует нашей оперативной памяти.

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

Вывод

Сравнивая текущие результаты “минуты за час” с моими предыдущими , мы уже видим пятикратное улучшение, но мы еще не закончили с этим. Эта простая оптимизация сократила разрыв между моими предыдущими результатами (0,209 с) и JOOQ Oracle one (0,02 с), хотя их результат все еще немного лучше.

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

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