MongoからASP.NET Core Web Apiに大量のデータをアップロードするパフォーマンス

MongoDBデータベースからクライアントに大量のデータをアップロードする必要がありました。 データはjsonで、GPSトラッカーから受信した車に関する情報が含まれています。 これらのデータは0.5秒間隔で受信されます。 1台のマシンの場合、約172,000件のレコードが取得されます。







サーバーコードは、標準のMongoDB.Driver 2.4.4ドライバーを使用してASP.NET CORE 2.0で記述されています。 サービスをテストする過程で、Web Apiアプリケーションプロセスによる1つのリクエストの実行時に約700 MBのかなりのメモリ消費が発見されました。 複数のクエリを並行して実行する場合、プロセスメモリは1 GBを超える場合があります。 0.7 GBのRAMを備えた最も安価なドロップレット上のコンテナでサービスを使用することになっているため、RAMの大量消費により、データアップロードプロセスを最適化する必要が生じました。







したがって、メソッドの基本的な実装には、すべてのデータのアップロードとクライアントへの送信が含まれます。 この実装を以下のリストに示します。







オプション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); } }
      
      





別の方法として、以下に示すように、初期行の数とページングされた行の数を設定してクエリを使用する方法が使用されました。 この場合、アンロードは応答ストリームで実行され、RAMの消費を削減します。







オプション2(サブクエリを使用して、応答ストリームに書き込む)



  //      // 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パラメーターを設定するオプションも使用され、データは応答ストリームにも書き込まれました。







オプション3(BatchSizeパラメーターを使用して、応答ストリームに書き込む)



  //      // 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(); }
      
      





データベース内の1つのレコードの構造は次のとおりです。







 {"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を使用して要求に応じて実行されました。

絶対値ではなく、それらの順序を興味深いと考えています。







3つの実装オプションのパフォーマンステスト結果を以下の表にまとめます。











表のデータは、図形式でも表示されます。















結論



要約すると、RAMの消費を削減するためのこのような手段の使用は、パフォーマンスの大幅な低下(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のawait cursor.MoveNextAsync()



およびバッチオブジェクトのシリアル化も測定されました。







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





出力ストリームへの書き込み。 カーソルの移動には1/3の時間がかかり、シリアル化と出力は2/3になります。 したがって、2000年頃にバッチにStringBuilderを使用すると、メモリの増加はそれほど大きくなく、データを受信する時間がawait Response.WriteAsync(JsonConvert.SerializeObject(doc))



秒短縮され、 await Response.WriteAsync(JsonConvert.SerializeObject(doc))



呼び出しの回数await Response.WriteAsync(JsonConvert.SerializeObject(doc))



減ります。 オブジェクトを手動でシリアル化することもできます。








All Articles