Pull to refresh

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

Reading time5 min
Views6.2K

Возникла необходимость выгрузки большого количества данных на клиент из базы 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.
Интересными считаю не абсолютные значения, а их порядок.


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




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






Выводы


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


Дополнения


Протестирован вариант реализации с yeild return


Вариант 4 (используются параметр BatchSize и yeild Return)


[HttpGet("GetListSync/{vin}/{startTimestamp}/{endTimestamp}")]
public  IEnumerable<Machine> GetListSync(string vin, DateTime startTimestamp, DateTime endTimestamp)
{

    var filter = Builders<Machine>.Filter
  .Where(x => x.Vin == vin &&
              x.Timestamp >= startTimestamp && 
              x.Timestamp <= endTimestamp);

    using (var cursor = _mongoConfig.Database
    .GetCollection<Machine>(_mongoConfig.CollectionName)
        .FindSync(filter, new FindOptions<Machine, Machine> { BatchSize = 10000 }))
    {

        while (cursor.MoveNext())
        {
            var batch = cursor.Current;
            foreach (var doc in batch)
            {
                yield return doc;
            }
        }
    }
}

Дополненные результаты сведены в таблицу:



Так же было замерено время на перемещение курсора await cursor.MoveNextAsync() в варианте 3 и сериализацию batch объектов


foreach (var doc in batch)
{
await Response.WriteAsync(JsonConvert.SerializeObject(doc));
}

с записью в поток вывода. Перемещение курсора занимает 1/3 времени, сериализация и вывод 2/3. Поэтому выгодно использовать StringBuilder для Batch около 2000, прирост памяти при этом незначительный, а время получения данных снижается более чем на треть до 6 — 7 секунд, уменьшается количество вызовов await Response.WriteAsync(JsonConvert.SerializeObject(doc)). Также можно сериализовать объект вручную.

Tags:
Hubs:
Total votes 8: ↑7 and ↓1+6
Comments22

Articles