Производительность выгрузки большого количества данных из Mongo в ASP.NET Core Web Api

в 11:17, , рубрики: .net, .net core, ASP, mongodb, mvc

Производительность выгрузки большого количества данных из Mongo в ASP.NET Core Web Api - 1

Возникла необходимость выгрузки большого количества данных на клиент из базы MongoDB. Данные представляют собой json, с информацией о машине, полученный от GPS трекера. Эти данные поступают с интервалом в 0.5 секунд. За сутки для одной машины получается примерно 172 000 записей.

Серверный код написан на ASP.NET CORE 2.0 с использованием стандартного драйвера MongoDB.Driver 2.4.4. В процессе тестирования сервиса выяснилось значительное потребление памяти процессом Web Api приложения — порядка 700 Мб, при выполнении одного запроса. При выполнении нескольких запросов параллельно объем памяти процесса может быть больше 1 Гб. Поскольку предполагается использование сервиса в контейнере на самом дешевом дроплете с оперативной памятью в 0.7 Гб, то большое потребление оперативной памяти привело к необходимости оптимизировать процесс выгрузки данных.

Таким образом, базовая реализация метода предполагает выгрузку всех данных и отправку их клиенту. Эта реализация представлена в листинге ниже.

Вариант 1 (все данные отправляются одновременно)

// Получить состояния по диапазону дат
// GET state/startDate/endDate
[HttpGet("{vin}/{startTimestamp}/{endTimestamp}")]
public async Task<StatesViewModel> Get(string vin, DateTime startTimestamp, 
                                        DateTime endTimestamp)
{
    // Фильтр
    var builder = Builders<Machine>.Filter;

    // Набор фильтров
    var filters = new List<FilterDefinition<Machine>>
    {
        builder.Where(x => x.Vin == vin),
        builder.Where(x => x.Timestamp >= startTimestamp 
        && x.Timestamp <= endTimestamp)
    };

    // Объединение фильтров
    var filterConcat = builder.And(filters);

    using (var cursor = await database
                  .GetCollection<Machine>(_mongoConfig.CollectionName)
                  .FindAsync(filterConcat).ConfigureAwait(false))
    {
        var a = await cursor.ToListAsync().ConfigureAwait(false);
        return _mapper.Map<IEnumerable<Machine>, StatesViewModel>(a);
    }

}

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

Вариант 2 (используются подзапросы и запись в поток Response)

      // Получить состояния по диапазону дат
// GET state/startDate/endDate
[HttpGet("GetListQuaries/{vin}/{startTimestamp}/{endTimestamp}")]
public async Task<ActionResult> GetListQuaries(string vin, DateTime startTimestamp,
DateTime endTimestamp)
{

    Response.ContentType = "application/json";
    await Response.WriteAsync("[").ConfigureAwait(false); ;
    // Фильтр
    var builder = Builders<Machine>.Filter;
    // Набор фильтров
    var filters = new List<FilterDefinition<Machine>>
    {
        builder.Where(x => x.Vin == vin),
        builder.Where(x => x.Timestamp >= startTimestamp 
                    && x.Timestamp <= endTimestamp)
    };

    // Объединение фильтров
    var filterConcat = builder.And(filters);
    int batchSize = 15000;
    int total = 0;
    long count =await database.GetCollection<Machine>
    (_mongoConfig.CollectionName)
    .CountAsync((filterConcat));
    while (total < count)
    {

         using (var cursor = await database
                .GetCollection<Machine>(_mongoConfig.CollectionName)
        .FindAsync(filterConcat, new FindOptions<Machine, Machine>() 
                {Skip = total, Limit = batchSize})
                .ConfigureAwait(false))
        {

            // Move to the next batch of docs
            while (cursor.MoveNext())
            {
                var batch = cursor.Current;
                foreach (var doc in batch)
                {
                await Response.WriteAsync(JsonConvert.SerializeObject(doc))
                                  .ConfigureAwait(false);                        
                }
            }
        }
        total += batchSize;
    }
    await Response.WriteAsync("]").ConfigureAwait(false); ;
    return new EmptyResult();
}

Также применялся вариант установки параметра BatchSize в курсоре, данные также записывались в поток Response.

Вариант 3 (используются параметр BatchSize и запись в поток Response)

  // Получить состояния по диапазону дат
// GET state/startDate/endDate
[HttpGet("GetList/{vin}/{startTimestamp}/{endTimestamp}")]
public  async Task<ActionResult> GetList(string vin, DateTime startTimestamp,                                                       DateTime endTimestamp)
{

    Response.ContentType = "application/json";
    // Фильтр
    var builder = Builders<Machine>.Filter;
    // Набор фильтров
    var filters = new List<FilterDefinition<Machine>>
    {
        builder.Where(x => x.Vin == vin),
        builder.Where(x => x.Timestamp >= startTimestamp 
            && x.Timestamp <= endTimestamp)
    };

    // Объединение фильтров
    var filterConcat = builder.And(filters);

    await Response.WriteAsync("[").ConfigureAwait(false); ;

    using (var  cursor = await database
        .GetCollection<Machine> (_mongoConfig.CollectionName)
        .FindAsync(filterConcat, new FindOptions<Machine, Machine>
        { BatchSize = 15000 })
       .ConfigureAwait(false))
    {
        // Move to the next batch of docs
        while (await cursor.MoveNextAsync().ConfigureAwait(false))
        {
            var batch = cursor.Current;
            foreach (var doc in batch)
            {
                await Response.WriteAsync(JsonConvert.SerializeObject(doc))
                              .ConfigureAwait(false); 
            }
        }
    }

    await Response.WriteAsync("]").ConfigureAwait(false); 
    return new EmptyResult();

}

Одна запись в базе данных имеет следующую структуру:

{"Id":"5a108e0cf389230001fe52f1",
"Vin":"357973047728404",
"Timestamp":"2017-11-18T19:46:16Z",
"Name":null,
"FuelRemaining":null,
"EngineSpeed":null,
"Speed":0,
"Direction":340.0,
"FuelConsumption":null,
"Location":{"Longitude":37.27543,"Latitude":50.11379}}

Тестирование производительности осуществлялось при запросе с использованием HttpClient.
Интересными считаю не абсолютные значения, а их порядок.

Результаты тестирования производительности для трех вариантов реализации сведены в таблице ниже.

Производительность выгрузки большого количества данных из Mongo в ASP.NET Core Web Api - 2

Данные из таблицы также представлены в виде диаграмм:

Производительность выгрузки большого количества данных из Mongo в ASP.NET Core Web Api - 3

Производительность выгрузки большого количества данных из Mongo в ASP.NET Core Web Api - 4

Выводы

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

Автор: atour

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js