-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Description
SocketsHttpHandler request performance is roughly 5X slower when using HTTP/2.0 than when using HTTP/1.1 to the same destination. There are no meaningful variations in flow control characteristics at the transport (TCP) protocol layers. It appears to be an issue in the HTTP/2.0 implementation itself.
It does not appear to be a server-side issue, as the equivalent HTTP/2.0 request issued using WinHttpHandler does not exhibit the performance reduction against the same HTTP/2.0 server.
Minimal repro:
using System;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Net.Http;
namespace sockinv
{
class Program
{
const uint BYTE_LENGTH = 26_214_400; // 25MB
static void Main(string[] args)
{
var timer = new Stopwatch();
timer.Start();
using (var handler = new SocketsHttpHandler())
{
var result = TestHandler(handler, new Version(1, 1));
result.Wait();
timer.Stop();
Console.WriteLine($"SocketsHttpHandler (Success: {result.Result}) HTTP/1.1 in {timer.ElapsedMilliseconds}ms ({BYTE_LENGTH / timer.ElapsedMilliseconds / 1000:N3} MB/s)");
}
timer.Restart();
using (var handler = new SocketsHttpHandler())
{
var result = TestHandler(handler, new Version(2,0));
result.Wait();
timer.Stop();
Console.WriteLine($"SocketsHttpHandler (Success: {result.Result}) HTTP/2.0 in {timer.ElapsedMilliseconds}ms ({BYTE_LENGTH / timer.ElapsedMilliseconds / 1000:N3} MB/s)");
}
timer.Restart();
using (var handler = new WinHttpHandler())
{
var result = TestHandler(handler, new Version(1, 1));
result.Wait();
timer.Stop();
Console.WriteLine($"WinHttpHandler (Success: {result.Result}) HTTP/1.1 in {timer.ElapsedMilliseconds}ms ({BYTE_LENGTH / timer.ElapsedMilliseconds / 1000:N3} MB/s)");
}
timer.Restart();
using (var handler = new WinHttpHandler())
{
var result = TestHandler(handler, new Version(2, 0));
result.Wait();
timer.Stop();
Console.WriteLine($"WinHttpHandler (Success: {result.Result}) HTTP/2.0 in {timer.ElapsedMilliseconds}ms ({BYTE_LENGTH / timer.ElapsedMilliseconds / 1000:N3} MB/s)");
}
}
static HttpRequestMessage GenerateRequestMessage(Version httpVersion, uint bytes)
{
// Replace the URL below with the URL of server that can generate an arbitrary number of bytes
return new HttpRequestMessage(HttpMethod.Get, $"<YOUR_URL_HERE>&length={bytes}")
{
Version = httpVersion
};
}
static async Task<bool> TestHandler(HttpMessageHandler handler, Version httpVersion)
{
using (var client = new HttpClient(handler, false))
{
var message = GenerateRequestMessage(httpVersion, BYTE_LENGTH);
var response = await client.SendAsync(message);
return response.IsSuccessStatusCode;
}
}
}
}Configuration
Windows 20H2. .NET Core 3.1 & .NET 5.0 were both tested with the same results.
Regression?
Not certain, I didn't test back before .NET Core 3.1.
Data
Example run of the above repro to my test server at 4ms RTT:
SocketsHttpHandler (Success: True) HTTP/1.1 in 622ms (42.000 MB/s)
SocketsHttpHandler (Success: True) HTTP/2.0 in 2220ms (11.000 MB/s)
WinHttpHandler (Success: True) HTTP/1.1 in 465ms (56.000 MB/s)
WinHttpHandler (Success: True) HTTP/2.0 in 489ms (53.000 MB/s)
Analysis
I took a packet capture while exercising the above repro and noted a number of things. First and foremost, SocketsHttpHandler (in the HTTP/1.1 case) and WinHttpHandler (in both the HTTP/1.1 and HTTP/2 cases) appear to be exercising the congestive limit of the network I am on. Additionally, my server is capable of extracting TCP ESTAT data for each request, and the results are unambiguous (and confirmed via packet capture) - congestive loss conspired to reduce the congestion window and cause slower throughput overall.
There does appear to be an issue in read rate in the SocketsHttpHandler HTTP/1.1 case that causes the receive window to grow more slowly (accounting for the very repeatable pullback of ~10MB/s at 4ms RTT between SocketsHttpHandler and WinHttpHandler). That's not the issue in this bug, though.
The real issue is the enormous pullback for HTTP/2 in SocketsHttpHandler. That transfer rate is not explained by any activity at the TCP layer. Receive window space is ample, no congestive loss is observed (and in fact at the server there is ample congestion window space available). TCP is mostly not sending data because it has not been provided data to send. This would indicate an issue at the server, except alternative HTTP/2 client implementations do not exhibit this behavior to this same server (e.g. WinHttpHandler, curl, etc.). Note that in the above dataset, WinHttpHandler using HTTP/2 can hit 53MB/s (again, the congestive limit of this network confirmed with server-side TCP ESTATS and a packet capture).
Interestingly, a packet capture at the client shows that the segments follow a transmission pattern where a small number of bytes are sent punctuated by an RTT-based delay. This indicates a buffering issue.
Given the lack of TCP receive window flow-control impact and the lack of congestive loss in the slow SocketsHttpHandler HTTP/2 case, I suspect an issue in the implementation of HTTP/2 flow control in SocketsHttpHandler is causing the server to starve the TCP connection for bytes.