Skip to content

socket: add SO_KEEPALIVE option for upstream connections.#3042

Merged
htuch merged 27 commits intoenvoyproxy:masterfrom
JonathanO:add-tcp-keepalive-option
Apr 29, 2018
Merged

socket: add SO_KEEPALIVE option for upstream connections.#3042
htuch merged 27 commits intoenvoyproxy:masterfrom
JonathanO:add-tcp-keepalive-option

Conversation

@JonathanO
Copy link
Copy Markdown
Contributor

Description:
Adds TCP Keepalive support for upstream connections. This can be configured on the cluster manager level, and overridden on the cluster level.

Risk Level: Medium

Testing:
Unit tests have been added. It appears to run and work.

Docs Changes:
envoyproxy/data-plane-api#614

Fixes #3028

API Changes:
envoyproxy/data-plane-api#614

Adds support for configuring TCP Keepalives on upstream connections.
Resolves envoyproxy#3028

Requires envoyproxy/data-plane-api#614

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from 6a6ae7c to fad89fd Compare April 11, 2018 13:15
}
}
cluster_options->emplace_back(new Network::TcpKeepaliveOptionImpl(
PROTOBUF_GET_WRAPPED_OR_DEFAULT(cluster.tcpKeepaliveSettings(), keepalive_probes,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is horrible and I shouldn't have done it this way. I'll move it out to a struct that'll be populated at construction time.

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
typedef std::unique_ptr<ConnectionSocket> ConnectionSocketPtr;

struct TcpKeepaliveConfig {
absl::optional<uint32_t> keepalive_probes_ = absl::nullopt;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need for "= absl::nullopt;". The default constructor sets them properly.

* @return const envoy::api::v2::UpstreamConnectionOptions& cluster manager wide connection
* options for new upstream connections.
*/
virtual const envoy::api::v2::UpstreamConnectionOptions& connectionOptions() const PURE;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Given that configs are usually machine-generated, should we remove this (and the corresponding data-plane-api field) and just set this on each cluster?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This allows it to be set in the bootstrap config easily, and makes it's behaviour consistent with upstream_bind_options.

absl::optional<uint32_t>()),
PROTOBUF_GET_WRAPPED_OR_DEFAULT(connection_options.tcp_keepalive(), keepalive_interval,
absl::optional<uint32_t>())};
} else if (config.upstream_connection_options().has_tcp_keepalive()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These two cases can be refactored and collapsed into one, just operating on a different object.

cluster_options = options;
}
if (cluster.features() & ClusterInfo::Features::USE_TCP_KEEPALIVE) {
if (!cluster_options) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please clean up with duplicate logic from the FREEBIND case above. This is hard to follow.

} else {
cluster_options = options;
}
if (cluster.features() & ClusterInfo::Features::USE_TCP_KEEPALIVE) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of adding a big to features(), can you just condition on whether cluster.tcpKeepaliveSettings() is set or not, and turn it into an optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I copied this pattern from the HTTP2 feature, I assumed there was maybe a performance reason for using the features flags rather than checking an optional field. If there's not, then I'll change it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Looking at this further, I'm wondering if I should actually refactor to instantiate the TcpKeepaliveOptionImpl (and also UpstreamSocketOption for freebind) at ClusterInfoImpl construct time and store them both in a Options vector on ClusterInfo.
That removes them both from Features.
That should also greatly simplify the code in createConnection, since there's no need to have different cases for them (it's just a case of merging options vectors together is necessary.)
This should be safe as neither Option implementation stores any state that differs between connections to the same cluster.

What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My best guess for why this is the way it is: the features flags came befofe the current iteration of socket-option-setters.

I think your suggested refactoring sounds great, and should be a nice cleanup. @htuch do you know any more history on this, or any objections to this cleanup?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've made an attempt at the refactor (based off this branch) in this commit JonathanO@bf4bf78 which I think looks a bit cleaner.

I must admit that I'm (re)learning modern C++ by trial and error, and I have no idea if the shared pointer usage in that commit is right or even necessary.
If it looks sane then I'll merge it into this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, that looks better to me. I'll add a few comments when you merge that into this PR, but the basic structure seems right.

// We fail to set the option when the underlying setsockopt syscall fails.
TEST_F(TcpKeepaliveOptionImplTest, SetOptionTcpKeepaliveFailure) {
TcpKeepaliveOptionImpl socket_option{{}};
#ifdef SO_KEEPALIVE
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if (ENVOY_SOCKET_SO_KEEPALIVE.has_value())

}

bool TcpKeepaliveOptionImpl::setTcpKeepalive(Socket& socket,
const absl::optional<uint32_t>& keepalive_probes,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These 3 params can be passed by value instead of by const&

ENVOY_LOG(warn, "Setting SO_KEEPALIVE on socket failed: {}", strerror(error));
return false;
}
if (keepalive_probes.has_value()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please factor out a function for the pattern "if option doesn't exist then return false; set the option; check the error" and call it 4 times.

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from cf72a27 to c813195 Compare April 13, 2018 19:45
Removes options on cluster manager, so no need for handing overriding of
the socket options anymore.

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from 90bbe0e to 8a88054 Compare April 13, 2018 20:31
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from 339fd57 to 937a793 Compare April 16, 2018 20:26
Copy link
Copy Markdown
Member

@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.

This is turning out really nice. Thanks for cleaning up the freebind features thing.

absl::optional<uint32_t> keepalive_time,
absl::optional<uint32_t> keepalive_interval) {
int error;
error = setSocketOption(socket, SOL_SOCKET, ENVOY_SOCKET_SO_KEEPALIVE, 1);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Q: does there need to be a way to configure it to explicitly disable the socket option, ie set it to a value of zero? I think it would only be needed if there's a way to make the default (at the OS level) enabled. Is that a thing we need to do?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Under Linux there's no way to turn it on at the OS level, only tweak the parameters. It appears that under FreeBSD and OSX it's possible to turn it on for everyone with "net.inet.tcp.always_keepalive", but I don't know if it's possible to explicitly disable it using SO_KEEPALIVE=0 in that case.

int TcpKeepaliveOptionImpl::setSocketOption(Socket& socket, int level,
Network::SocketOptionName optname, uint32_t value) {
if (!optname.has_value()) {
return ENOTSUP;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should return -1, and set errno to ENOTSUP

return ENOTSUP;
}
auto& os_syscalls = Api::OsSysCallsSingleton::get();
return os_syscalls.setsockopt(socket.fd(), level, optname.value(), &value, sizeof(value));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These options should all be of type/size int, not uint32_t. See http://man7.org/linux/man-pages/man7/tcp.7.html

}

} // namespace Network
} // namespace Envoy No newline at end of file
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please add a newline at the end of the file

bool ipv4_compat = 6;
}

message TcpKeepalive {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please document the macos behavior. It looks like you added support for the right socket option, but please document that the other two must be unset in the config on macos.

Copy link
Copy Markdown
Contributor Author

@JonathanO JonathanO Apr 18, 2018

Choose a reason for hiding this comment

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

I swear I replied to this, but the comment seems to be missing now.
The reason for the single special case is that MacOS uses a different name for that one constant (TCP_KEEPIDLE vs TCP_KEEPALIVE.) The other constants all have the same name as for Linux. All of these settings are valid for both Linux and MacOS. I've added a comment to that effect in the header where they're defined.

cluster_options->emplace_back(
new Network::TcpKeepaliveOptionImpl(parseTcpKeepaliveConfig(config)));
}
return cluster_options;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if cluster_options is empty, return nullptr?

// Cluster IP_FREEBIND settings, when set, will override the cluster manager wide settings.
if ((bind_config.freebind().value() && !config.upstream_bind_config().has_freebind()) ||
config.upstream_bind_config().freebind().value()) {
cluster_options->emplace_back(new UpstreamSocketOption(true));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

std::make_shared

}
if (config.upstream_connection_options().has_tcp_keepalive()) {
cluster_options->emplace_back(
new Network::TcpKeepaliveOptionImpl(parseTcpKeepaliveConfig(config)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

std::make_shared

if (cluster.features() & ClusterInfo::Features::FREEBIND) {
cluster_options = std::make_shared<Network::ConnectionSocket::Options>();
Network::ConnectionSocket::OptionsSharedPtr connection_options;
if (cluster.clusterSocketOptions() && !cluster.clusterSocketOptions().get()->empty()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if (cluster.clusterSocketOptions() != nullptr && !cluster.clusterSocketOptions()->empty()) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Can be simplified to just the nullptr check if parseClusterSocketOptions() returns nullptr if the options are empty, as you suggest.

*cluster_options = *options;
connection_options = std::make_shared<Network::ConnectionSocket::Options>();
*connection_options = *options;
copy(cluster.clusterSocketOptions().get()->begin(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

std::copy and std::back_inserter?

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from f5fdca6 to 15d52ce Compare April 18, 2018 13:56

typedef std::unique_ptr<ConnectionSocket> ConnectionSocketPtr;

struct TcpKeepaliveConfig {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this can be moved closer to the _impl.cc, since it's an implementation detail essentially and not needed by any interfaces in include/envoy.


struct TcpKeepaliveConfig {
absl::optional<uint32_t> keepalive_probes_;
absl::optional<uint32_t> keepalive_time_;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comments would be good, in particular providing the units of time being used.

return -1;
}
auto& os_syscalls = Api::OsSysCallsSingleton::get();
return os_syscalls.setsockopt(socket.fd(), level, optname.value(), &value, sizeof(value));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I feel like we're redoing https://github.com/envoyproxy/envoy/blob/master/source/common/network/socket_option_impl.h here. I realize these are TCP options, there is also #2793 where I've asked to add in some equivalent support for TCP options. Do you think it's reasonable to come up with some refactoring to avoid redoing how socket options are set, platform features are handled and the various branches tested?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm going to change SocketOptionName to be absl::optional<std::pair<int, int>>, so it can contain the "level" as well as "optname." That should make it possible to clean up the socket option code a bit and maybe make a common Socket::Option implementation, which takes a SocketOptionName and value as constructor args, possible. That might then allow TCP Keepalive, and other simple options, to be implemented with as a factory that produces a vector of options.

SocketOptionImpl has special handling for checking if the socket it's being applied to is IP, and setting different options for IPV6 vs 4. I think that's going to have to remain slightly special (although it might be able to use the generic implementation under the hood.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Awesome. @bmetzdorf for visibility, so that you folks don't collide on this. I could merge #2793 as is if that works for you both.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that works for me.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SGTM, I can address the open aesthetic items from #2793 in another PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

OK, will merge.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

#2793 merged.

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO
Copy link
Copy Markdown
Contributor Author

This should be ready for review again now. I've merged the fast open changes from master, and completed a refactor to hopefully clean up all of the socket option handling.

Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

I like the new approach overall, very clean. I do have some reservations on SocketOptionsImpl. Also @bmetzdorf for thoughts on the TCP socket options, I know he is working on this right now in #3157

@@ -0,0 +1,58 @@
#include "common/network/addr_family_aware_socket_impl.h"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This file is missing a bunch of code coverage.

@@ -0,0 +1,59 @@
#include "common/network/socket_option_factory.h"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This file is missing a bunch of code coverage.


// Socket::Option
bool setOption(Socket& socket, Socket::SocketState state) const override {
bool good = true;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: prefer renaming this to be something more descriptive, e.g. all_socket_options_applied. Coming to think of it, I'm wondering exactly what semantics we want here.. do we want to abort early and just return false in the inner loop rather than continue to apply options?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think you're right, it should abort early since all other loops that call setOption() already abort early. I can't think of a reason not to, and I don't think there was any specific reason I chose this implementation at the time.

const int value_;
};

class SocketOptionsImpl : public Socket::Option, Logger::Loggable<Logger::Id::connection> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need this given that Socket already has a collection of options? Doesn't this then become a collection of options for a collection of options?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was an attempt to allow the socket option factory methods that want to set multiple options (e.g. the TCP keepalive one) to still return a single Socket::Option that the caller would then do whatever they needed to in order to add it to their own list.

The alternatives seemed to be to return a Socket::Options and require the caller to copy the contents of that into their own list (seemed a bit ugly), or for the factory to be provided with an OptionsSharedPtr for the factory to append to (but that doesn't quite mesh with Listener's addListenSocketOption implementation.)

Another alternative would be for me to maybe replace the Socket::Options typedef with an implementation like this one, which would avoid us needing two different types of collection of Options, but would still leave us with nested collections of options (just both levels of the same type.)

I'm tending towards the last idea, but do you have any preference (or a better alternative?)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The two ideas I like are the last one (always use a special collection object) or for the factory to always return a vector/list (Socket::Options). I lean towards always using Socket::Options because then there isn't an issue with weird nesting.

With either of these approaches, I think eventually it would be better for each socketoption to have it's own object. You don't have to make that change as part of this PR if you don't want, but having 1 option per object, and a consistent way to dealing with a collection/set of these objects, would make this a bit cleaner.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah, I also like:

  • One socket option per object.
  • Factory returns always Socket::Options

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, I'll go that path, and for this PR I'll add a bunch of shared utility functions for managing these collections to avoid duplicating code in listener and upstream (rather than adding a new collection type with methods for the needed operations.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've just pushed the change to get rid of SocketOptionsImpl. It would definitely look even cleaner with a new collection type, rather than static utility methods I added on Socket, but I think maybe this is already a sufficient improvement? I'm still missing test coverage as @htuch pointed out, which I'll fix tomorrow.

Added utility methods to Network::Socket instead and updated listener
and upstream to use them.

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
@JonathanO JonathanO force-pushed the add-tcp-keepalive-option branch from 2e84117 to e275246 Compare April 25, 2018 17:12
#include "envoy/common/pure.h"
#include "envoy/network/address.h"

#include "absl/types/optional.h"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unneeded include

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>
htuch
htuch previously approved these changes Apr 27, 2018
Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

LGTM, this has turned out quite nicely. @ggreenway any further comments?

@ggreenway
Copy link
Copy Markdown
Member

@JonathanO You have merge conflicts to resolve.

ggreenway
ggreenway previously approved these changes Apr 27, 2018
Copy link
Copy Markdown
Member

@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.

This looks great! Thanks for refactoring this.

I think this is ready once the merge conflicts are resolved.

@JonathanO JonathanO dismissed stale reviews from ggreenway and htuch via 4c40a6b April 28, 2018 15:01
Copy link
Copy Markdown
Member

@htuch htuch left a comment

Choose a reason for hiding this comment

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

Thanks @JonathanO, this is a really great improvement on what we had.

@htuch htuch merged commit dd953f9 into envoyproxy:master Apr 29, 2018
@JonathanO JonathanO deleted the add-tcp-keepalive-option branch April 30, 2018 10:18
ramaraochavali pushed a commit to ramaraochavali/envoy that referenced this pull request May 3, 2018
…#3042)

Adds TCP Keepalive support for upstream connections. This can be configured on the cluster manager level, and overridden on the cluster level.

Risk Level: Medium

Testing:
Unit tests have been added. It appears to run and work.

Docs Changes:
envoyproxy/data-plane-api#614

Fixes envoyproxy#3028

API Changes:
envoyproxy/data-plane-api#614

Signed-off-by: Jonathan Oddy <jonathan.oddy@transferwise.com>

Signed-off-by: Rama <rama.rao@salesforce.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.

4 participants