Add capability to clean up pools after connections have gone idle#13061
Add capability to clean up pools after connections have gone idle#13061chradcliffe wants to merge 34 commits intoenvoyproxy:masterfrom
Conversation
* Add pool idle timeout timer and callbacks to ConnectionPool::ConnPoolImplBase * Add logic to set the idle timer when there are no pending or active connections and disable the timer when new activity occurs * Add logic to ClusterManagerImpl to remove pools after their idle timeout expires and to remove hosts from the pool maps when the map is empty after an idle timer expiry Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
mattklein123
left a comment
There was a problem hiding this comment.
Nice. LGTM at a high level. Flushing some comments from a first pass. Thank you!
/wait
include/envoy/upstream/upstream.h
Outdated
| /** | ||
| * @return the idle timeout for upstream connection pools. | ||
| */ | ||
| virtual std::chrono::milliseconds poolIdleTimeout() const PURE; |
There was a problem hiding this comment.
nit: I would return absl::optional here as opposed to 0 to make it more clear when there is no timeout.
| : host_(host), priority_(priority), dispatcher_(dispatcher), socket_options_(options), | ||
| transport_socket_options_(transport_socket_options) {} | ||
| transport_socket_options_(transport_socket_options), idle_timeout_(pool_idle_timeout), | ||
| idle_timer_(dispatcher.createTimer([this]() { |
There was a problem hiding this comment.
I have to look again every time I come back to this, but did you double check that conn pools are always deleted inline and not in deferred delete context? I think this is true and we can delete/disable the idle timer in the destructor but I want to make sure we don't have a potential issue where the timer might fire after the pool has been deferred deleted.
There was a problem hiding this comment.
It looks like the HTTP conn pools are never deferred deleted, but unfortunately the TCP conn pools are -- see ClusterManagerImpl::ThreadLocalClusterManagerImpl::drainTcpConnPools.
I can add a cancelPoolIdleTimer() method to ConnectionPool::Instance and add a call to it in drainTcpConnPools, unless there's a better pattern to follow.
There was a problem hiding this comment.
I would need to spend more time with the code to give you a better assessment here. It would be nice to have this be self contained somehow and not add more interface methods, but maybe that is not possible. cc @alyssawilk for additional thoughts as she has been in this code more recently.
There was a problem hiding this comment.
I think (please check) that drainConnectionsImpl is always called before pool destruction so that might be a good and safe place to unregister.
There was a problem hiding this comment.
I put the timer cancellation into closeIdleConnectionsForDrainingPool since that is called from all drain paths.
|
|
||
| // After the pool enters a state in which it has no active connections, the idle timeout callbacks | ||
| // will be called after `idle_timeout_` milliseconds. A value of | ||
| // `std::chrono::milliseconds::max()` is used to disable the timeout |
There was a problem hiding this comment.
per other comment I would use absl::optional here.
| } | ||
|
|
||
| template <typename KEY_TYPE, typename POOL_TYPE> | ||
| bool ConnPoolMap<KEY_TYPE, POOL_TYPE>::erasePool(const KEY_TYPE& key) { |
There was a problem hiding this comment.
I don't see this return value used in the prod code. Can you use it even if only asserting it? Otherwise drop it?
There was a problem hiding this comment.
assert added where it is used in the prod code.
| checkForDrained(); | ||
| checkForIdle(); |
There was a problem hiding this comment.
I would have to dive back in and fully revisit the logic, but it seems to me that checkForDrained and checkForIdle are doing very similar things and have to be called at all the same points. Can they be merged into checkForDrained to limit the required changes in this PR as well as logic divergence?
There was a problem hiding this comment.
I considered whether the logic could be merged, but I think there are subtle differences that prevent that (I think).
checkForDrained does the following:
- Closes connections in the
READYstate (andCONNECTINGif there are no pending streams) - Fires the drained callbacks if there are no pending, ready, busy, and connecting clients
For checkForIdle, we don't want to change the state of the connections at all. I think we also primarily care about active and pending streams rather than the connection state (e.g. if the pool has a bunch of CONNECTING connections but no pending streams, it is still idle).
There are also a few cases where we need to check for idle but we don't want to check for drained, e.g. when a stream closes we want to check whether we just went idle, but it doesn't make sense to check if we've drained since it's unlikely the connection bearing the closed stream has been drained yet.
Does that make sense?
There was a problem hiding this comment.
I'm probably missing something, but my general take is that "idle" and "drained" is basically the same thing. We might do different things (fire drained callback or start timer) but I don't quite understand how they are different in that we are checking to see if there are no active connections.
Can we unify into a single function that takes an enum param if that matters?
My general take is it's going to be pretty hard for the next reader to understand how these things are different and we should try to unify them. cc @alyssawilk on this point also.
There was a problem hiding this comment.
I added some comments to distinguish between these cases -- I hope it's clearer now.
When a pool is "idle" it means that there may be live connections, but those connections do not have any active streams. If the pool sees activity, it ceases to be idle. When the idle timer fires, the pool is drained and removed from the map.
Prior to this PR, a pool is only put into the "draining" state under (afaik) two conditions: the host is unhealthy or the host is being removed. This adds a third state -- the pool has been idle for the idle timeout duration.
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Originally the idle check used the lack of active streams as the start of the idle timer; however, this is too complicated. Instead, we now start the timeout after the last connection has been drained from the pool. Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
|
This pull request has been automatically marked as stale because it has not had activity in the last 7 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
Most of the requested changes are implemented, but I had to set aside this work for other tasks. I plan to push a new set of changes this week. |
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
* Fix original conn pool logic and add test * Ensure that the idle timer is disabled when draining * Drain connections after the idle timer fires but before erasing the pool * Make logic consistent beween conn_pool_base and original_conn_pool Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
* Added note about connection pool lifetime * Added missing note about pools per downstream connection Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
) Clarify that the server may not resend resources upon stream restart only if it has some way to know that the client is not subscribing to new resources that it wasn't previously subscribed to. Risk Level: Low Testing: N/A Docs Changes: Included in PR Clarifies text added in envoyproxy#12580. Signed-off-by: Mark D. Roth <roth@google.com> Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…x a typo (envoyproxy#13696) The comment in v3 version was missing the "If specified.." clause from the v2 version of that comment Risk Level: low Testing: Ran ./ci/run_envoy_docker.sh './ci/do_ci.sh fix_format' Docs Changes: comment in a proto file changed Signed-off-by: Sanjay Pujare <sanjaypujare@google.com> Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
|
@alyssawilk @mattklein123 this is ready for another pass when you get the time. |
mattklein123
left a comment
There was a problem hiding this comment.
Thanks for working on this. A high level comment to make sure we are doing this in the right way. Thank you.
/wait
| return nullptr; | ||
| } | ||
| } | ||
| } // namespace Upstream |
include/envoy/common/conn_pool.h
Outdated
| /** | ||
| * Called when a connection pool has no pending requests or busy connections for a configured | ||
| * timeout duration. | ||
| */ | ||
| using IdlePoolTimeoutCb = std::function<void()>; | ||
|
|
||
| /** | ||
| * Register a callback that gets called when the connection pool has had no pending requests or | ||
| * busy connections for a configured timeout duration. | ||
| * | ||
| * NOTE: The "idle" state is not the same as the "drained" state. A pool may be drained before it | ||
| * has a chance to go idle (e.g. a host is removed), and a pool is drained after it has | ||
| * gone idle. | ||
| */ | ||
| virtual void addIdlePoolTimeoutCallback(IdlePoolTimeoutCb cb) PURE; |
There was a problem hiding this comment.
I know we discussed this before, but it's been a while so I don't remember where we landed with this. Conceptually, it feels broken that we have "drain callbacks" as well as "idle callbacks."
Conceptually these seem like the same thing. How is idle different from draining?
Could we model this differently by doing something like the following:
- Having draining just be a bool state (yes/no).
- Have idle callbacks and timeout be independently setable.
- Draining is basically just setting the drain boolean and setting the idle timeout to 0?
cc @alyssawilk who I would also like to look at this since she has been in this code a lot recently.
There was a problem hiding this comment.
Just getting back to this...
In this PR, the idle timer starts whenever there are no pending or active clients for the pool. When the timer fires, the pool is drained.
So the sequence is currently:
- No active or pending clients on the pool, pool idle timer is set
- Idle timer fires, idle callbacks called
- Cluster manager drains the pool
- All connections on pool are closed, drain callbacks called
- Cluster manager deletes pool
I think in order to make your suggestion work, the definition of the start of the "idle" interval would have to be after all connections have hit their connection idle timeouts. Since "drained" requires that all connections are closed, in order for there to be a single callback, the idle timeout will also have to be fired at that time or later.
This has the downside that a user will have to appropriately configure both the connection idle timeout and the pool idle timeout on a connection pool. Leaving the default connection idle timeout for HTTP would mean that the timeout would be 1 hour + the configured pool idle timeout. If we're okay with asking the user to configure both timeouts, I can make the appropriate changes to this PR and merge the drained callbacks and the idle callbacks.
The new sequence for an idle timeout would be:
- All connections on the pool hit their idle timeout, pool idle timer is set
- Pool idle timer fires, idle callbacks called
- Cluster manager deletes the pool
There was a problem hiding this comment.
So I feel strongly that we should merge drain/idle. IMO it's the same thing. But I'm still confused. Isn't "draining" just setting an idle timeout of 0 as it is implemented today? Why can't the explicit drain process just reset idle timeout to 0 regardless of what it was configured for?
There was a problem hiding this comment.
The current implementation explicitly closes all connections and waits for the connection count to reach zero before calling the drain callbacks, so it's not exactly setting the connection idle timeout to 0.
In this PR I introduced a pool idle timeout independent of the individual connection timeouts, but I think what you're suggesting is that the connection idle timeouts should be good enough.
If we were to rely on the connection idle timeouts only, I'm not sure what to do with the case where all of the connections in a pool close for reasons other than an idle timeout (e.g. a momentary loss of connectivity with the upstream). If we were to rely solely on the number of active connections reaching zero to determine when the pool is "idle", then we might be prematurely deleting and recreating pools if the connections in the pool error out. Maybe it's fine to delete and recreate pools in that case?
There was a problem hiding this comment.
I can go back and look at the code again, but I'm nearly positive that the definition of drained has nothing to do with connection idle timeouts and only whether there are active/pending requests. This was implementing a long, long time ago before there even were connection idle timeouts. Are you sure your analysis is correct on what drain does?
There was a problem hiding this comment.
You're right that it has nothing to do with connection idle timeouts -- I thought you were referring to the connection idle timeouts when you said "Isn't 'draining' just setting an idle timeout of 0 as it is implemented today?"
My understanding of the drain process is as follows (in ConnPoolImplBase::drainConnectionsImpl):
- Close all connections without active requests (
closeIdleConnectionsForDrainingPool) - Move all busy/ready clients to the DRAINING state
- After certain events (e.g. request completion) close all connections without active requests
- When all connections have been closed, call the drained callbacks
My understanding of the code is that a pool isn't drained until it has no pending or active requests and its connections have been closed.
There was a problem hiding this comment.
Right but the point is connections are actively closed when they have no active/pending requests. This means we don't wait for connection idle. Given this, I'm pretty sure it's effectively just setting idle timeout to 0?
There was a problem hiding this comment.
Yes, I agree that it's effectively setting the connection idle timeout to 0.
So if I'm understanding correctly:
- the idle callbacks should trigger based on the connection idle timeout of the last connection in a pool
- the idle and drained callbacks should both be added via the same API
- starting to drain should be setting a draining boolean
The question I have now is: what should we do about a pool whose connections are closed prior to the connection idle timeout -- for example, the remote endpoint closes all connections? Is it acceptable to tear down the pool at that point, even if that means we might recreate the pool immediately afterwards?
There was a problem hiding this comment.
Is it acceptable to tear down the pool at that point, even if that means we might recreate the pool immediately afterwards?
I don't see a problem with this, but if you want you could guard this functionality with whether the draining boolean is set? Up to you.
* Replaced addIdleTimeoutCallback/addDrainedCallback with addIdleCallback with a parameter to indicate whether we should start draining * Removed the pool idle timer, instead relying on the connection idle timeouts to empty the pool Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
…/clean-up-connection-pools Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
|
The latest set of changes significantly simplifies the implementation and combines the idle/drained API into a single call with a flag to indicate whether we should kick off a drain. I've made the behaviour configurable via a per-cluster config option with the thinking that aggressive cleanup of pools might not be desirable for clusters where pools are expected to be frequently reused. We could make this a runtime-guarded feature if that makes more sense -- WDYT? |
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
Signed-off-by: Craig Radcliffe <craig.radcliffe@broadcom.com>
|
/retest |
|
Retrying Azure Pipelines: |
alyssawilk
left a comment
There was a problem hiding this comment.
just did a pass on the first few files - want to make sure I understand the planned lifetime before I dig into the rest.
| checkForDrained(); | ||
| void ConnPoolImplBase::addIdleCallbackImpl(Instance::IdleCb cb, Instance::DrainPool drain) { | ||
| idle_callbacks_.push_back(cb); | ||
| is_draining_ = (drain == Instance::DrainPool::Yes) || is_draining_; |
There was a problem hiding this comment.
is_draining |= (drain == Instance::DrainPool::Yes) ?
| dispatcher_.deferredDelete(client.removeFromList(owningList(client.state_))); | ||
| if (incomplete_stream) { | ||
| checkForDrained(); | ||
| if (incomplete_stream || !is_draining_) { |
There was a problem hiding this comment.
comment why the is_draining_ check here and not elsewhere please
| ENVOY_LOG(debug, "invoking drained callbacks"); | ||
| for (const Instance::DrainedCb& cb : drained_callbacks_) { | ||
| cb(); | ||
| if (has_seen_clients_ && pending_streams_.empty() && ready_clients_.empty() && |
There was a problem hiding this comment.
is has_seen_clients_ valid for the is_draining case?
AFIK if a cluster has never seen traffic and it is told to drain, it should drain.
I wonder if for clusters we're willing to erase we should simply only create them when there's demand, and if the work between creating the cluster and creating a stream would allow removing this boolean?
There was a problem hiding this comment.
You're right -- I think we need to drain right away. I'll push an update with a test.
We already create connection pools on demand in ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::{tcpConnPool,connPool} -- the bool is there to prevent an immediate cleanup on a new pool when the first idle callback is added (we call checkForIdle when a callback is added).
We might be able to remove the bool if we don't check for idle in addIdleCallback when DrainPool::No is passed to the function. That would also solve the issue of always draining when being told to. WDYT?
There was a problem hiding this comment.
yeah, if it's just there to avoid immediate drain I think we can probably avoid it with careful calls. I'm also fine leaving it in as a "but we want to be paranoid" precaution, with appropriate comments :-)
| // that no longer have active connections. The time between the last activity | ||
| // in a pool and its cleanup can be adjusted via the cluster's connection | ||
| // :ref:`idle_timeout <envoy_api_field_config.cluster.v3.Cluster.common_http_protocol_options>` | ||
| bool erase_idle_pools = 53; |
There was a problem hiding this comment.
What is the actual cost of adding a new pool? I'm thinking not that much? And if we only remove a pool in the case of connection idle timeouts or failure of all connections to all hosts, it seems like an edge case and not a huge deal? I think my preference would be to remove this config option, runtime guard the change per your recommendation, but default it to on? @alyssawilk wdyt?
|
This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 7 days if no further activity occurs. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
|
This pull request has been automatically closed because it has not had activity in the last 37 days. Please feel free to give a status update now, ping for review, or re-open when it's ready. Thank you for your contributions! |
Commit Message: Add capability to clean up pools after connections have gone idle
Additional Description:
addDrainedCallbackson the pools toaddIdleCallbackswith a flag to indicate whether the pool should be drainederase_idle_poolsconfiguration option is enabled,ClusterManagerImplwill drain and remove pools after they have gone idle and will remove hosts from the pool maps when the map is empty after all pools for the host have been removedRisk Level: Medium
Testing: Unit tests, manual testing with new feature enabled
Docs Changes: API comments
Release Notes: Updated to document addition of
erase_idle_poolsto clusterFixes #12370