Skip to content

Kestrel always outputs data by chunks of 4096 (HTTP 1.1) and 65536 (HTTP 2) bytes maximum #30545

Closed
@smourier

Description

@smourier

I'm using a .NET 5 (5.0.3) on a Windows 10 20H2 19042.844.

Whatever I try, my ASP.NET Core Kestrel server always writes packets to the wire by a maximum size which is 4096 (HTTP 1.1) and 65536 (HTTP 2).

The issue is the overall "download"" performance is bad (for a set of machines + network that would support bigger chunk size), especially with HTTP 1.1. Sometimes the packets size is only a few bytes.

Here is a reproducing code:

class Program
{
    static void Main(string[] args) => CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
            	// uncomment for HTTP 2
            	//serverOptions.ConfigureEndpointDefaults(listenOptions =>
            	//{
            		  //    listenOptions.Protocols = HttpProtocols.Http2;
            	//});
                webBuilder.UseStartup<Startup>();
            });
}

class Client
{
    public async Task Download()
    {
        var client = new HttpClient();
        var url = "http://localhost:5000/Test/Download";

        var req = new HttpRequestMessage(HttpMethod.Get, url)
        {
          	// uncomment for HTTP 2
            //Version = HttpVersion.Version20,
            //VersionPolicy = HttpVersionPolicy.RequestVersionOrHigher
        };
        
        var resp = await client.SendAsync(req, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        var completed = 0L;
        using (var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false))
        {
            var buffer = new byte[0x100000];
            do
            {
                var read = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length)).ConfigureAwait(false);
                if (read == 0)
                    break;

				// read here is always 4096 or 65536!
                completed += read;
                Console.WriteLine("Read: " + read);
            }
            while (true);
        }

        Console.WriteLine("Completed: " + completed);
    }
}

class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseHttpsRedirection();
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });

		// run a client in another thread
        Task.Run(() => new Client().Download());
    }
}

[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
    [HttpGet]
    [Route("download")]
    public IActionResult Download()
    {
        // create some temp file
        var path = Path.Combine(Path.GetTempPath(), "MyTempFile.bin");
        if (!System.IO.File.Exists(path))
        {
            using (var file = new FileStream(path, FileMode.Create))
            {
                file.SetLength(0x400000); // 4MB
            }
        }

        // stream it out
        var stream = System.IO.File.OpenRead(path);
        return new FileStreamResult(stream, "application/octet-stream");
    }
}

This is my launchSettings.json:

{
  "iisSettings": {
	"windowsAuthentication": false,
	"anonymousAuthentication": true,
	"iisExpress": {
	  "applicationUrl": "http://localhost:54611/",
	  "sslPort": 44346
	}
  },
  "profiles": {
	"IIS Express": {
	  "commandName": "IISExpress",
	  "environmentVariables": {
		"ASPNETCORE_ENVIRONMENT": "Development"
	  }
	},
	"KTest": {
	  "commandName": "Project",
	  "environmentVariables": {
		"ASPNETCORE_ENVIRONMENT": "Development"
	  },
	  "applicationUrl": "https://localhost:5001;http://localhost:5000"
	}
  }
}    	

Things I've tried:

  • configure Kestrel using all KestrelServerOptions.Limits to the max
  • I've also written a custom FileStreamResultExecutor (because the default one seems unconfigurable and uses a 65536-size buffer) with a buffer size > 65536 (obviously only for HTTP/2 in my test), it's used, but it doesn't change anything to the 65536 limit.
  • I've tested other web servers just to make sure, and they don't do that

I think this comes from the internal ConcurrentPipeWriter but I'm unsure why.

Is this expected? Or is this a configuration problem? Is it an OS problem? How can I get bigger chunks?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Perfarea-networkingIncludes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions