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

io_handle: cache envoy address instances for recvmsg and recvmmsg #34649

Merged
merged 15 commits into from
Jun 25, 2024

Conversation

danzh2010
Copy link
Contributor

Commit Message: Maintain a LRU cache of Address::Instance objects in IoSocketHandleImpl to keep track of source and destination addresses received in recvmsg and recvmmsg. This cache is added mainly used for UDP read. This is an alternative to #33917 to address the issue that each recvmsg/recvmmsg will create a new instance of the source/destination addresses (which does stringification upon instantiation) even though they have been received before.

The cache can be initialized during IoSocketHandleImpl construction based on a new param max_addresses_cache_size_ in SocketCreationOptions. This change only enables EnvoyQuicClientConnection to use a SocketCreationOptions with size 4 to create the underlying IoSocketHandleImpl. There is no change to the server side recvmsg and recvmmsg calls as the significant CPU cost on server side hasn't been observed.

There is no cache support in non-IoSocketHandleImpl-based implementations.

Additional Description: Added QUICHE dependency to IoSocketHandleImpl to use data structures like QuicLRUCache and QuicSocketAddress. Refactored source/common/http:utility_lib to break the circular dependency as needed.

Risk Level: low, only affect quic upstream
Testing: added new unit and integration tests
Docs Changes: N/A
Release Notes: yes
Platform Specific Features: N/A
Runtime guard: envoy.reloadable_features.quic_upstream_socket_use_address_cache_for_read

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Copy link

CC @envoyproxy/runtime-guard-changes: FYI only for changes made to (source/common/runtime/runtime_features.cc).

🐱

Caused by: #34649 was opened by danzh2010.

see: more, trace.

@alyssawilk
Copy link
Contributor

Throwing over to @RyanTheOptimist but please get !googler review once you're done

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Copy link
Contributor

@RyanTheOptimist RyanTheOptimist left a comment

Choose a reason for hiding this comment

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

Thanks for doing this. For some reason I thought we could do this at the QUIC layer, but after looking at your PR, obviously we have to do it at the socket layer. I think this looks pretty good. Do we have any sort of benchmark we can run to ensure that this does what we want?

@@ -15,8 +17,14 @@ struct SocketCreationOptions {
// and only valid on Linux.
bool mptcp_enabled_{false};

// Specifies whether there should be a cache for all the address instances associated to this
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's rephrase this slightly:

// Specifies the maximum size of the cache of the address instances associated with
// packets received by this socket. If this is 0, no addresses will be cached. Is only
// valid for datagram sockets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

false, /*max_addresses_cache_size_ = */ (
Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.quic_upstream_socket_use_address_cache_for_read")
? 4u
Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, why 4? Is this same method use for client and server? (The runtime feature says "upstream" so maybe this is client-only?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is only used to create client socket. I would think 4 is sufficient for a quic client connection, with a default path and an alternative path.

Runtime::runtimeFeatureEnabled(
"envoy.reloadable_features.quic_upstream_socket_use_address_cache_for_read")
? 4u
: 0u)});
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Up to you, but consider factoring this out into a small helper method:

int getSocketAddressCacheSize() {
  if (Runtime::runtimeFeatureEnabled(
                  "envoy.reloadable_features.quic_upstream_socket_use_address_cache_for_read") {
    return 4;
  }
  return 0;
}

This would avoid the need for the /*max_addresses_cache_size_ = */ ( which is a bit hard to parse mentally. Instead we would simply say:
Network::SocketCreationOptions{false, getSocketAddressCacheSize()}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added a tmp variable for this.

Address::InstanceConstSharedPtr
maybeGetDstAddressFromHeader(const cmsghdr& cmsg, uint32_t self_port, os_fd_t fd, bool v6only);

std::unique_ptr<AddressInstanceLRUCache> recent_received_addresses_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Brief comment, in particular explaining when it can be null.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done


private:
Address::InstanceConstSharedPtr
maybeGetDstAddressFromHeader(const cmsghdr& cmsg, uint32_t self_port, os_fd_t fd, bool v6only);
Copy link
Contributor

Choose a reason for hiding this comment

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

Brief comment please. In particular, explain if the return value can be null and if so why, and what the "maybe" implies.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

quic_address, std::make_unique<Address::InstanceConstSharedPtr>(new_address));
return new_address;
}
return *it->second;
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: since this is the smaller codepath, let's inver the conditional and reduce the nesting a tad:

    if (it != recent_received_addresses_->end()) {
      return *it->second;
    }
    Address::InstanceConstSharedPtr new_address =
        Address::addressFromSockAddrOrDie(ss, sizeof(sockaddr_in6), fd, v6only);
    recent_received_addresses_->Insert(
        quic_address, std::make_unique<Address::InstanceConstSharedPtr>(new_address));
    return new_address;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

quic_address, std::make_unique<Address::InstanceConstSharedPtr>(new_address));
return new_address;
}
return *it->second;
Copy link
Contributor

Choose a reason for hiding this comment

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

ditto

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

} else {
output.msg_[i].peer_address_ = Address::addressFromSockAddrOrDie(
raw_addresses[i], hdr.msg_namelen, fd_, socket_v6only_ || !udp_read_normalize_addresses_);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Hm. It seems like we have 4 places where we're doing basically the same thing of looking up an address in the cache and possible creating a new one. Can we share this code somehow, perhaps with a helper method?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved them into a member function.

@@ -477,7 +533,7 @@ IoHandlePtr IoSocketHandleImpl::accept(struct sockaddr* addr, socklen_t* addrlen
return nullptr;
}
return SocketInterfaceImpl::makePlatformSpecificSocket(result.return_value_, socket_v6only_,
domain_);
domain_, {});
Copy link
Contributor

Choose a reason for hiding this comment

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

Why does this do something different from the similar call below? Perhaps we should have a comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

accept() is only used by TCP socket which doesn't use the address cache. Whereas dup() can be used by both TCP and UDP, so copy over the same creation option.

@@ -172,10 +172,10 @@ void EnvoyQuicClientConnection::probeWithNewPort(const quic::QuicSocketAddress&
Network::Address::InstanceConstSharedPtr new_local_address;
if (current_local_address->ip()->version() == Network::Address::IpVersion::v4) {
new_local_address = std::make_shared<Network::Address::Ipv4Instance>(
current_local_address->ip()->addressAsString());
current_local_address->ip()->addressAsString(), &current_local_address->socketInterface());
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure that I understand this change. Can you say more about it? I can't quite plumb it all through in my mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not quite relevant to the rest of the PR. The Ipv4Instance has a handle to a socket interface extension which will be used to create a specific IoHandle instance. If not passed in, the global default extension will be use.

Here we are creating an alternative socket for this connection. The alternative socket should use the same socket interface extension to create IoHandle. Since the address cache is specific to IoSocketHandleImpl, we want to make sure the same extension is used across the connection life time.

@danzh2010
Copy link
Contributor Author

Do we have any sort of benchmark we can run to ensure that this does what we want?

It's hard to benchmark recvmsg and recvmmsg calls. I added unit test and integration test to ensure we don't create different address objects in each read.

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010 danzh2010 requested a review from lizan as a code owner June 18, 2024 14:31
@RyanTheOptimist
Copy link
Contributor

/wait
for passing CI

@danzh2010
Copy link
Contributor Author

/retest

Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010
Copy link
Contributor Author

/retest

@danzh2010
Copy link
Contributor Author

The Envoy/Publish and verify CI failure seems unrelated.

@abeyad
Copy link
Contributor

abeyad commented Jun 20, 2024

/assign @ggreenway for a cross-company reviewer

Copy link

neither of for, a, cross-company, reviewer can be assigned to this issue.

🐱

Caused by: a #34649 (comment) was created by @abeyad.

see: more, trace.

@ggreenway ggreenway self-assigned this Jun 20, 2024
Copy link
Contributor

@ggreenway ggreenway left a comment

Choose a reason for hiding this comment

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

/wait

bazel/external/quiche.BUILD Show resolved Hide resolved
changelogs/current.yaml Outdated Show resolved Hide resolved
source/common/quic/envoy_quic_utils.cc Show resolved Hide resolved
Signed-off-by: Greg Greenway <ggreenway@apple.com>
@ggreenway ggreenway enabled auto-merge (squash) June 24, 2024 22:59
@ggreenway ggreenway disabled auto-merge June 24, 2024 22:59
@ggreenway ggreenway enabled auto-merge (squash) June 24, 2024 22:59
@RyanTheOptimist
Copy link
Contributor

/wait
on CI

Signed-off-by: Dan Zhang <danzh@google.com>
@danzh2010 danzh2010 requested a review from wbpcode as a code owner June 25, 2024 14:25
@ggreenway ggreenway merged commit b32f876 into envoyproxy:main Jun 25, 2024
53 of 56 checks passed
cainelli pushed a commit to cainelli/envoy that referenced this pull request Jul 5, 2024
…voyproxy#34649)

Maintain a LRU cache of Address::Instance objects in IoSocketHandleImpl to keep track of source and destination addresses received in recvmsg and recvmmsg. This cache is added mainly used for UDP read. This is an alternative to envoyproxy#33917 to address the issue that each recvmsg/recvmmsg will create a new instance of the source/destination addresses (which does stringification upon instantiation) even though they have been received before.

The cache can be initialized during IoSocketHandleImpl construction based on a new param max_addresses_cache_size_ in SocketCreationOptions. This change only enables EnvoyQuicClientConnection to use a SocketCreationOptions with size 4 to create the underlying IoSocketHandleImpl. There is no change to the server side recvmsg and recvmmsg calls as the significant CPU cost on server side hasn't been observed.

There is no cache support in non-IoSocketHandleImpl-based implementations.

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Fernando Cainelli <fernando.cainelli-external@getyourguide.com>
cainelli pushed a commit to cainelli/envoy that referenced this pull request Jul 5, 2024
…voyproxy#34649)

Maintain a LRU cache of Address::Instance objects in IoSocketHandleImpl to keep track of source and destination addresses received in recvmsg and recvmmsg. This cache is added mainly used for UDP read. This is an alternative to envoyproxy#33917 to address the issue that each recvmsg/recvmmsg will create a new instance of the source/destination addresses (which does stringification upon instantiation) even though they have been received before.

The cache can be initialized during IoSocketHandleImpl construction based on a new param max_addresses_cache_size_ in SocketCreationOptions. This change only enables EnvoyQuicClientConnection to use a SocketCreationOptions with size 4 to create the underlying IoSocketHandleImpl. There is no change to the server side recvmsg and recvmmsg calls as the significant CPU cost on server side hasn't been observed.

There is no cache support in non-IoSocketHandleImpl-based implementations.

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Fernando Cainelli <fernando.cainelli-external@getyourguide.com>
cainelli pushed a commit to cainelli/envoy that referenced this pull request Jul 5, 2024
…voyproxy#34649)

Maintain a LRU cache of Address::Instance objects in IoSocketHandleImpl to keep track of source and destination addresses received in recvmsg and recvmmsg. This cache is added mainly used for UDP read. This is an alternative to envoyproxy#33917 to address the issue that each recvmsg/recvmmsg will create a new instance of the source/destination addresses (which does stringification upon instantiation) even though they have been received before.

The cache can be initialized during IoSocketHandleImpl construction based on a new param max_addresses_cache_size_ in SocketCreationOptions. This change only enables EnvoyQuicClientConnection to use a SocketCreationOptions with size 4 to create the underlying IoSocketHandleImpl. There is no change to the server side recvmsg and recvmmsg calls as the significant CPU cost on server side hasn't been observed.

There is no cache support in non-IoSocketHandleImpl-based implementations.

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Fernando Cainelli <fernando.cainelli-external@getyourguide.com>
cainelli pushed a commit to cainelli/envoy that referenced this pull request Jul 5, 2024
…voyproxy#34649)

Maintain a LRU cache of Address::Instance objects in IoSocketHandleImpl to keep track of source and destination addresses received in recvmsg and recvmmsg. This cache is added mainly used for UDP read. This is an alternative to envoyproxy#33917 to address the issue that each recvmsg/recvmmsg will create a new instance of the source/destination addresses (which does stringification upon instantiation) even though they have been received before.

The cache can be initialized during IoSocketHandleImpl construction based on a new param max_addresses_cache_size_ in SocketCreationOptions. This change only enables EnvoyQuicClientConnection to use a SocketCreationOptions with size 4 to create the underlying IoSocketHandleImpl. There is no change to the server side recvmsg and recvmmsg calls as the significant CPU cost on server side hasn't been observed.

There is no cache support in non-IoSocketHandleImpl-based implementations.

Signed-off-by: Dan Zhang <danzh@google.com>
Signed-off-by: Fernando Cainelli <fernando.cainelli-external@getyourguide.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants