-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
HttpConnectionPool contention for HTTP/1.1 #70098
Comments
Tagging subscribers to this area: @dotnet/ncl Issue DetailsWhen multiple requests use the same connection pool (same handler + same destination), they will hit a bunch of contention when acquiring and releasing the connection from the pool. A quick example comparing the HttpClient benchmark using 1 vs 8 handlers, both with 256 worker tasks, shows a ~13% difference.
I saw similar numbers (a ~15% E2E difference) in my LLRP experiment when switching to using multiple handlers. Approximate numbers from a benchmark doing minimal work outside of spamming the connection pool:
On my machine (8 core CPU) the Threads=6 scenario is spending 57 % of CPU on getting the lock in We should look into reducing the contention here if possible.
|
Are you able to run a similar test on previous versions, e.g. .NET Core 3.1 vs .NET 5 vs .NET 6 vs .NET 7? I'm curious if this has changed recently. |
Needs tiny changes in the benchmark app, I'll do that |
From a quick test it looks like the Threads=1 case is significantly improved with every release whereas contention (% drop depending on number of threads) is very similar. |
Triage: Contention in HTTP/1.1. Might be similar problems in HTTP/2 and HTTP/3, but they are not measured / tracked here. |
As an FYI - numbers for HTTP/2 on .NET 6, scenario is GET with OK (no content) from the server:
|
Scratch that, I forgot the huge H2 improvements in Kestrel; but the gap still persists:
|
We are seeing this under load as well in our production environments. In one scenario we actually created a "proxy" in GoLang that took a bunch of GET requests as a POST reducing 25 HTTP connections to 1 from the .NET perspective and let GoLang deal with heavy load. Pretty sad that we had to do that, but it helped, but in other scenarios it is more complicated and we can't go that route. We have an HTTP Varnish cache and a decent number of our responses are less than 1 MS but we will see contention at times turn that into 40+ms, which kills performance and our ability to handle high loads as operations take so much longer. One thing we are thinking about doing is adding multiple "destinations" aka host entries to maybe alleviate the contention, this reminds me of the old browser days when you could only have so many concurrent requests per domain. We tried HTTP/2 but that didn't seem to help and under heavy load we were getting "accessing a disposed object" errors, which forced us back to 1.1 Things we've noticed DNS contention - Our thoughts is the framework should allow us dictate DNS behavior. These DNS entries never change as they are service in the K8s cluster, it rarely changes and if it did, all the pods would have been destroyed beforehand anyway. SSL contention - We switched to HTTP as again, these are internal calls to the cluster. Other things we've thought about doing is move to UDS and proxy to Istio/Envoy. Thoughts on a fix and thoughts on multiple DNS entries? |
Playing around with a (lightly tested) reimplementation of the HTTP/1.1 pool that boils down to
Or YARP's http-http 100 byte scenario
On an in-memory loopback benchmark that stresses the connection pool contention: https://gist.github.com/MihaZupan/27f01d78c71da7b9024b321e743e3d88 Rough RPS numbers with 1-6 threads:
|
@stephentoub @MihaZupan Is there any chance that this fix might get cherry-picked back to .NET 8? /CC @aceven24-csgp |
Why do you ask? Are you seeing such issues with your service? It's unlikely that we would backport this change. This isn't a correctness fix - the behavior is the same. |
@MihaZupan yes we've been seeing these issues since .NET 6 timeframe (see comment added by @aceven24-csgp above). Our application has very high request throughput. Our company typically only aligns with LTS releases hence the ask about back porting to .NET 8, rather than waiting to what would be .NET 10 for us. |
Have you tried manually splitting requests over multiple That would achieve a similar thing as this change at high load. And if it doesn't, you may be running into different issues. |
Yes, we have tried something similar to what you suggested and saw improvements, so we do have a reasonable workaround for now /cc @kevinstapleton |
Hi @MihaZupan, This is somewhat anecdotal now as we implemented the workaround over a year ago, but as @ozziepeeps mentioned, we were seeing heavy contention on the locks within the HTTP connection pool which appeared to be limiting our throughput, primarily when creating new connections. To be specific, in our trace captures, we would see many "RequestLeftQueue" events with high queue times. As a PoC to prove out whether this was affecting us or not, we created a custom handler which had a "pool" of So while we don't need this to be back-ported, we are indeed eager to have a proper fix in the runtime for this. If anything, I think our success with using |
Seeing this during steady state when using HTTP/1 indicates that you didn't have enough connections to handle all requests. The changes I made here are really focused on the "happy path" where we're able to open enough connections to handle the load so eventually we're just processing requests without having to open any new connections. If you were limited by the number of connections, the recent changes wouldn't help with contention. |
When multiple requests use the same connection pool (same handler + same destination), they will hit a bunch of contention when acquiring and releasing the connection from the pool.
A quick example comparing the HttpClient benchmark using 1 vs 8 handlers, both with 256 worker tasks, shows a ~13% difference.
I saw similar numbers (a ~15% E2E difference) in my LLRP experiment when switching to using multiple handlers.
Approximate numbers from a benchmark doing minimal work outside of spamming the connection pool:
On my machine (8 core CPU) the Threads=6 scenario is spending 57 % of CPU on getting the lock in
![image](https://user-images.githubusercontent.com/25307628/171469682-acabdd47-4c25-4ade-9f0e-b5a0ebf20f74.png)
GetHttp11ConnectionAsync
andReturnHttp11Connection
.We should look into reducing the contention here if possible.
The text was updated successfully, but these errors were encountered: