ASP. NET Базовая буферизация ответов при отключении ядра - PullRequest
7 голосов
/ 09 марта 2020

Я пытаюсь передать большой файл JSON, созданный на лету, клиенту (может быть 500 МБ +). Я пытаюсь отключить буферизацию ответов по разным причинам, хотя в основном из-за эффективности памяти.

Я пытался записать напрямую в HttpContext.Response.BodyWriter, но ответ, похоже, буферизировался в памяти перед записью в вывод. Тип возвращаемого значения этого метода: Task.

HttpContext.Response.ContentType = "application/json";
HttpContext.Response.ContentLength = null;
await HttpContext.Response.StartAsync(cancellationToken);
var bodyStream = HttpContext.Response.BodyWriter.AsStream(true);
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("["), cancellationToken);
await foreach (var item in cursor.WithCancellation(cancellationToken)
    .ConfigureAwait(false))
{
    await bodyStream.WriteAsync(JsonSerializer.SerializeToUtf8Bytes(item, DefaultSettings.JsonSerializerOptions), cancellationToken);
    await bodyStream.WriteAsync(Encoding.UTF8.GetBytes(","), cancellationToken);

    await bodyStream.FlushAsync(cancellationToken);
    await Task.Delay(100,cancellationToken);
}
await bodyStream.WriteAsync(Encoding.UTF8.GetBytes("]"), cancellationToken);
bodyStream.Close();
await HttpContext.Response.CompleteAsync().ConfigureAwait(false);

Примечание. Я понимаю, что этот код очень хакерский, пытаюсь заставить его работать, а затем очищаю его

Я использую Task.Delay для проверки того, что ответ не буферизуется при локальном тестировании, поскольку у меня нет полных производственных данных. Я также пробовал IAsyncEnumerable и yield return, но это не помогло, потому что ответ настолько велик, что Кестрел считает, что перечисляемое бесконечно.

Я пробовал

  1. Установка KestrelServerLimits.MaxResponseBufferSize для небольшого числа, даже 0;
  2. Запись с HttpContext.Response.WriteAsync
  3. Запись с HttpContext.Response.BodyWriter.AsStream()
  4. Запись с помощью паттерна и HttpContext.Response.BodyWriter
  5. Удаление всего промежуточного программного обеспечения
  6. Удаление вызовов на IApplicationBuilder.UseResponseCompression

Обновление

Попытка отключения буферизации ответа перед установкой ContentType (т.е. перед любой записью в ответ) без эффекта
var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
responseBufferingFeature?.DisableBuffering();

Обновленный пример кода

Это воспроизводит проблему довольно просто. Клиент не получает никаких данных, пока не будет вызван response.CompleteAsync().

[HttpGet]
[Route("stream")]
public async Task<EmptyResult> FileStream(CancellationToken cancellationToken)
{
    var response = DisableResponseBuffering(HttpContext);
    HttpContext.Response.Headers.Add("Content-Type", "application/gzip");
    HttpContext.Response.Headers.Add("Content-Disposition", $"attachment; filename=\"player-data.csv.gz\"");
    await response.StartAsync().ConfigureAwait(false);
    var memory = response.Writer.GetMemory(1024*1024*10);
    response.Writer.Advance(1024*1024*10);
    await response.Writer.FlushAsync(cancellationToken).ConfigureAwait(false);
    await Task.Delay(5000).ConfigureAwait(false);
    var str2 = Encoding.UTF8.GetBytes("Bar!\r\n");
    memory = response.Writer.GetMemory(str2.Length);
    str2.CopyTo(memory);
    response.Writer.Advance(str2.Length);
    await response.CompleteAsync().ConfigureAwait(false);
    return new EmptyResult();
}

private IHttpResponseBodyFeature DisableResponseBuffering(HttpContext context)
{
    var responseBufferingFeature = context.Features.Get<IHttpResponseBodyFeature>();
    responseBufferingFeature?.DisableBuffering();
    return responseBufferingFeature;
}

1 Ответ

1 голос
/ 10 марта 2020

Попробуйте отключить буферизацию в ответных фьючерсах:

HttpContext.Features.Get<IHttpResponseBodyFeature>().DisableBuffering()
//As mentioned in documentation, to take effect, call it before any writes

И используйте BodyWriter в Utf8JsonWriter для большей эффективности:

 var pipe = context.HttpContext.Response.BodyWriter;
 await pipe.WriteAsync(startArray);
 using (var writer = new Utf8JsonWriter(pipe,
            new JsonWriterOptions
            {
                Indented = option.WriteIndented,
                Encoder = option.Encoder,
                SkipValidation = true
            }))
 {
      var dotSet = false;
      foreach (var item in enumerable)
      {
           if (dotSet)
               await pipe.WriteAsync(dot);
           JsonSerializer.Serialize(writer, item, itemType, option);
           await pipe.FlushAsync();
           writer.Reset();
           dotSet = true;
      }
 }
 await pipe.WriteAsync(endArray);

В моем случае это даст результаты: всего выделение памяти становится больше на 80% по сравнению с newcoreapp2.2 после первых запросов, но утечек памяти больше нет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...