ASP.NET Core禁用响应缓冲

16

我尝试向客户端流式传输一个实时生成的大型 JSON 文件(可能超过 500 MB)。我试图禁用响应缓冲,原因多种多样,但主要是为了提高内存效率。

我尝试直接写入 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);

注意:我意识到这段代码非常“hacky”,试图让它运行,然后再进行清理

我正在使用Task.Delay来验证在本地测试时响应是否被缓冲,因为我没有完整的生产数据。我还尝试过IAsyncEnumerableyield return,但是由于响应太大,Kestrel认为可枚举对象是无限的,所以失败了。

我已经尝试过:

  1. 设置KestrelServerLimits.MaxResponseBufferSize为一个很小的数值,甚至为0;
  2. 使用HttpContext.Response.WriteAsync进行编写;
  3. 使用HttpContext.Response.BodyWriter.AsStream()进行编写;
  4. 使用管道编写器模式和HttpContext.Response.BodyWriter进行编写;
  5. 删除所有中间件;
  6. 删除对IApplicationBuilder.UseResponseCompression的调用。

更新

  1. 在设置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;
}
3个回答

2

在使用http.sys(与ASP.NET Core 6一起)时,我能够让它正常工作:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.WebHost.UseHttpSys();
        var app = builder.Build();
        app.MapGet("/", async (context) =>
        {
            context.Response.StatusCode = 201;
            await context.Response.StartAsync();
            await context.Response.WriteAsync("x"); // client gets status code after this line
            await context.Response.WriteAsync("Hello World!");
        });
        app.Run();
    }
}

1

对于那些仍然感兴趣的人,这段代码在使用curl时会立即发送数据:

public async Task Invoke(HttpContext context)
{
    var g = context.Features.Get<IHttpResponseBodyFeature>();
    g.DisableBuffering(); // doesn't seem to make a difference

    context.Response.StatusCode = 200;
    context.Response.ContentType = "text/plain; charset=utf-8";
    //context.Response.ContentLength = null;

    await g.StartAsync();

    for (int i = 0; i < 10; ++i)
    {
        var line = $"this is line {i}\r\n";
        var bytes = utf8.GetBytes(line);
        // it seems context.Response.Body.WriteAsync() and
        // context.Response.BodyWriter.WriteAsync() work exactly the same
        await g.Writer.WriteAsync(new ReadOnlyMemory<byte>(bytes));
        await g.Writer.FlushAsync();
        await Task.Delay(1000);
    }

    await g.CompleteAsync();
}

我尝试使用和不使用DisableBufering(),以及写入管道(IHttpResponseBodyFeature.Writer vs HttpContext.Response.Body)的变化似乎没有什么区别。

在curl中,它立即显示消息,但在Chrome和一些rest客户端中,它等待整个流出现。

因此,我建议使用不等待整个流出现的客户端测试您的代码行为。另一个选项是我仍在检查aspnet core是否自动选择压缩可能性,如果客户端要求,即使在管道中没有配置压缩。


1
尝试在响应future上禁用缓冲:
HttpContext.Features.Get<IHttpResponseBodyFeature>().DisableBuffering()
//As mentioned in documentation, to take effect, call it before any writes

为了提高效率,可以在Utf8JsonWriter中使用BodyWriter

 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);

在我的情况下,它会给出结果:与 newcoreapp2.2 相比,在第一次请求后总内存分配增加了80%以上,但不再存在内存泄漏。

更新了,没有骰子。 - Pete Garafano
我也遇到了同样的问题,对于任何大型JSON都是如此。是的,我的答案并不是解决方案,只是一种避免“OutOfMemory”的变通方法。 - Alexander Vasilyev
你能帮我解决我的问题吗?我已经尝试使用DisableBuffering,但仍然出现了OutOfMemory异常:https://stackoverflow.com/q/74828066/14664018 - AtomicallyBeyond

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接