Skip to content
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

[retry only] Additional HTTP/2 connections created when active streams limit is reached #39439

Conversation

alnikola
Copy link
Contributor

HTTP/2 standard commands clients to not open more than one HTTP/2 connection to the same server. At the same time, server has right to limit the maximum number of active streams per that HTTP/2 connection. These two directive combined impose limit on the number of requests concurrently send to the server. This limitation is justified in client to server scenarios, but become a bottleneck in server to server cases like gRPC. This PR introduces a new SocketsHttpHandler API enabling establishing additional HTTP/2 connections to the same server when the maximum stream limit is reached on the existing ones.

Note. This algorithm version uses only retries to make request choose another connection when all stream slots are occupied. It does not implement stream credit management in HttpConnectionPool and therefore exhibit a sub-optimal request scheduling behavior in "request burst" and "infinite requests" scenarios.

Fixes #35088

@ghost
Copy link

ghost commented Jul 16, 2020

Tagging subscribers to this area: @dotnet/ncl
Notify danmosemsft if you want to be subscribed.

@Dotnet-GitSync-Bot
Copy link
Collaborator

Note regarding the new-api-needs-documentation label:

This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change.


private Http2Connection PickRandomConnection(HttpRequestMessage request, Http2Connection[] currentHttp2Connections)
{
// All connections are busy so pick a random one to wait on it.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we decide this is ok? This is one of the main concerns I raised previously.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Due to a high complexity of the other approach. @geoffkizer raised the concern that other solution is so complex that it will hinder other HTTP/2 optimization efforts. We still can go back to a more advanced version later, I won't delete that another branch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc: @karelz

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we explicitly decided it was ok. I agree, it's an issue.

The problem is, given this approach, it's hard to implement this the correct way. We don't have a notification from the Http2Connection that a new stream has become available. We could add this, of course, and we could add some queuing logic (similar to HTTP/1.1) to queue requests when all connections are busy, etc, etc.

That said, I'm not sure it's worth the trouble. The key scenario here is to support multiple HTTP2 connections. My suggestion is that we remove the connection limit here. Meaning, when you enable multiple HTTP2 connections, the number of connections is unbounded. This is what WinHTTP does as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That notification and queueing are implemented in my other PR #38748, but it added noticeable complexity as you can see. It still might be there is a way to achieve the same with some simpler mechanism, but I currently don't see how.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Http2Connection[]? localHttp2Connections = _http2Connections;
int newCollectionSize = localHttp2Connections == null ? 1 : localHttp2Connections.Length + 1;
Http2Connection[] newHttp2Connections = new Http2Connection[newCollectionSize];
newHttp2Connections[0] = newConnection;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we add the connection at the beginning of the list? I thought we were going to add it to the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To have the search from the newest to the oldest in GetHttp2ConnectionAcceptingNewStreams. If we decide to change the order, we will change it here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess what I'm asking is, why did we decide to search from newest to oldest instead of oldest to newest?

I don't have a strong opinion here, but I'd like to understand the rationale.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assumption is that older connections tend to have longer running requests than new ones because the lack of available streams on the old connections is the only driver for creating new ones. Thus, it looks more efficiently to firstly check the newest for available streams.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok.

@alnikola
Copy link
Contributor Author

Maximum connections limit removed.

@@ -648,6 +647,53 @@ public byte[] Http2AltSvcOriginUri
return await GetHttpConnectionAsync(request, async, cancellationToken).ConfigureAwait(false);
}

private Http2Connection? GetExistingHttp2Connection()
{
Http2Connection[]? localConnections = _http2Connections;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably worth a comment here to explain that the array itself is never mutated, though the array reference may change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's explained above _http2Connections declaration.

@@ -1461,7 +1473,7 @@ private void Abort(Exception abortException)

// Check idle timeout when there are not pending requests for a while.
if ((connectionIdleTimeout != Timeout.InfiniteTimeSpan) &&
(_httpStreams.Count == 0) &&
(_httpStreams.Count == 0) && (_idleSinceTickCount > 0) &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On a new Http2Connection _idleSinceTickCount is set to 0 until the first request is sent. On the other hand, setting PooledConnectionIdleTimeout to a specific value starts a timer calling this CleanCacheAndDisposeIfUnused method every second or less, so this check gets done before the first request goes through connection while _idleSinceTickCount is still 0 which is always less thanEnvironment.TickCount64 therefore a brand new connection can get almost immediately expired.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this issue existed previously? I'm glad you caught it!

That said, maybe it would be better to initialize _idleSinceTickCount to the current tick in the Http2Connection constructor? I'm not sure where we check this value, but it seems better to have it be correct always than to special case 0.

Adding @wfurt, I think he wrote this code... thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is an existing issue. Currently _idleSinceTickCount is read only in this method and assigned in RemoveStream. We could initialize it in the constructor, but it seems the intention was to update it when a request completes and stream gets closed, thus setting a value in the ctor will break that invariant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intent is to track the last time the connection might have been idle (if there were no other active streams). As such initializing it to the current tick count in the constructor seems to be more inline with the intent, to me.

I don't know if there's ever a case where we can get into a situation where an Http2Connection is created but never actually used -- maybe if cancellation happens at exactly the right time? If that ever happens, we will still want to idle the connection out eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, makes sense. I will initialize it in the constructor.

Copy link
Contributor

@geoffkizer geoffkizer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me. One small issue and one question above.

@alnikola alnikola merged commit 43670d5 into dotnet:master Jul 28, 2020
@alnikola alnikola deleted the alnikola/35088-retry-only-more-http2-conn-after-stream-count-limit branch July 28, 2020 10:43
Jacksondr5 pushed a commit to Jacksondr5/runtime that referenced this pull request Aug 10, 2020
…s limit is reached (dotnet#39439)

HTTP/2 standard commands clients to not open more than one HTTP/2 connection to the same server. At the same time, server has right to limit the maximum number of active streams per that HTTP/2 connection. These two directives combined impose limit on the number of requests concurrently send to the server. This limitation is justified in client to server scenarios, but become a bottleneck in server to server cases like gRPC. This PR introduces a new SocketsHttpHandler API enabling establishing additional HTTP/2 connections to the same server when the maximum stream limit is reached on the existing ones.

**Note**. This algorithm version uses only retries to make request choose another  connection when all stream slots are occupied. It does not implement stream credit management in `HttpConnectionPool` and therefore exhibit a sub-optimal request scheduling behavior in "request burst" and "infinite requests" scenarios.

Fixes dotnet#35088
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

HTTP2: Create additional connections when maximum active streams is reached
5 participants