-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Add option for merging cluster updates #3941
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
Changes from 5 commits
431a634
8e20204
c6dbc7d
9e3288e
add79b2
29f1d3d
0dd31c4
0bd58e6
67adb12
ec643f1
d05ef99
95e5260
0f70fb3
b392c7b
a399949
f88c297
a1145dd
b33b080
d70809c
10d3670
80e03db
dcd7174
b3745de
6fac84f
9eae09a
0d8475f
4d0c528
09d56cc
3d78234
6408528
31e4fdf
a5a5391
2a264d1
25faa2b
5affa4c
d3464d4
bbbfef8
d81fb7f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -180,7 +180,7 @@ ClusterManagerImpl::ClusterManagerImpl(const envoy::config::bootstrap::v2::Boots | |
| init_helper_([this](Cluster& cluster) { onClusterInit(cluster); }), | ||
| config_tracker_entry_( | ||
| admin.getConfigTracker().add("clusters", [this] { return dumpClusterConfigs(); })), | ||
| system_time_source_(system_time_source) { | ||
| system_time_source_(system_time_source), dispatcher_(main_thread_dispatcher) { | ||
| async_client_manager_ = std::make_unique<Grpc::AsyncClientManagerImpl>(*this, tls); | ||
| const auto& cm_config = bootstrap.cluster_manager(); | ||
| if (cm_config.has_outlier_detection()) { | ||
|
|
@@ -330,7 +330,17 @@ void ClusterManagerImpl::onClusterInit(Cluster& cluster) { | |
| const HostVector& hosts_removed) { | ||
| // This fires when a cluster is about to have an updated member set. We need to send this | ||
| // out to all of the thread local configurations. | ||
| postThreadLocalClusterUpdate(cluster, priority, hosts_added, hosts_removed); | ||
|
|
||
| // Should we coalesce updates? | ||
| bool scheduled = false; | ||
| if (cluster.info()->lbConfig().has_update_merge_window()) { | ||
| scheduled = scheduleUpdate(cluster, priority, hosts_added, hosts_removed); | ||
| } | ||
|
|
||
| // If an update was not scheduled, deliver it immediately. | ||
| if (!scheduled) { | ||
| postThreadLocalClusterUpdate(cluster, priority, hosts_added, hosts_removed); | ||
| } | ||
| }); | ||
|
|
||
| // Finally, if the cluster has any hosts, post updates cross-thread so the per-thread load | ||
|
|
@@ -343,6 +353,80 @@ void ClusterManagerImpl::onClusterInit(Cluster& cluster) { | |
| } | ||
| } | ||
|
|
||
| bool ClusterManagerImpl::scheduleUpdate(const Cluster& cluster, uint32_t priority, | ||
| const HostVector& hosts_added, | ||
| const HostVector& hosts_removed) { | ||
| const auto& update_merge_window = cluster.info()->lbConfig().update_merge_window(); | ||
| const auto timeout = DurationUtil::durationToMilliseconds(update_merge_window); | ||
| PendingUpdatesByPriorityMapPtr updates_by_prio; | ||
| PendingUpdatesPtr updates; | ||
|
|
||
| // Find pending updates for this cluster. | ||
| auto updates_by_prio_it = updates_map_.find(cluster.info()->name()); | ||
| if (updates_by_prio_it != updates_map_.end()) { | ||
| updates_by_prio = updates_by_prio_it->second; | ||
| } else { | ||
| updates_by_prio = std::make_shared<PendingUpdatesByPriorityMap>(); | ||
| updates_map_[cluster.info()->name()] = updates_by_prio; | ||
| } | ||
|
|
||
| // Find pending updates for this priority. | ||
| auto updates_it = updates_by_prio->find(priority); | ||
| if (updates_it != updates_by_prio->end()) { | ||
| updates = updates_it->second; | ||
| } else { | ||
| updates = std::make_shared<PendingUpdates>(); | ||
| (*updates_by_prio)[priority] = updates; | ||
| } | ||
|
|
||
| // Has an update_merge_window gone by since the last update? If so, don't schedule | ||
| // the update so it can be applied immediately. | ||
| auto delta = std::chrono::steady_clock::now() - updates->last_updated; | ||
| uint64_t delta_ms = std::chrono::duration_cast<std::chrono::milliseconds>(delta).count(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: these intermediate vars can be |
||
| if (delta_ms > timeout) { | ||
| updates->last_updated = std::chrono::steady_clock::now(); | ||
| return false; | ||
| } | ||
|
|
||
| // Record the updates — preserving order — that should be applied when the timer fires. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FWIW, I was arguing for just a deterministic ordering, e.g. using |
||
| for (auto host : hosts_added) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
| if (updates->added_seen.count(host) != 1) { | ||
| updates->added.push_back(host); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where does
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, good catch |
||
| } | ||
| } | ||
|
|
||
| for (auto host : hosts_removed) { | ||
| if (updates->removed_seen.count(host) != 1) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about sequences of ops in which a host is added, removed and added back again? I'm not sure if we have correctness here.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, becomes trickier with ordering so we should probably avoid that. if we just kept the sets, we can handle cancellations (added/removed). we'd have to decide if want drop cancelled out ops (i say yes). |
||
| updates->removed.push_back(host); | ||
| } | ||
| } | ||
|
|
||
| // If there's no timer, create one. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Won't this delay the first update after a long period of silence? Should we instead apply immediately in this case? I think we can then just keep track of the last time we applied and only add to a pending queue if < window length.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. addressed in the latest commit |
||
| if (updates->timer == nullptr) { | ||
| updates->timer = dispatcher_.createTimer([this, &cluster, priority, &updates]() -> void { | ||
| applyUpdates(cluster, priority, updates); | ||
| }); | ||
| updates->timer->enableTimer(std::chrono::milliseconds(timeout)); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't we still enable the timer if it's already existing? |
||
| } | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| void ClusterManagerImpl::applyUpdates(const Cluster& cluster, uint32_t priority, | ||
| PendingUpdatesPtr updates) { | ||
| // Deliver pending updates. | ||
| postThreadLocalClusterUpdate(cluster, priority, updates->added, updates->removed); | ||
| cm_stats_.coalesced_updates_.inc(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we also count non-coalesced updates? Or can we just infer this from total? |
||
|
|
||
| // Reset everything. | ||
| updates->timer = nullptr; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, you nuke it here. I'd suggest keeping the timer alive and just doing disable/enable, it probably doesn't matter, but one less wasted allocation/free.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it would be nice to avoid the allocation/free pair, but then we'd have to check if the timer is enabled or not by looking at the last update timestamp... at which point things become a bit more involved. Preferences? On a related note, the logic to apply an update immediately if the last update happened within a merge window should probably also check if a timer exists (e.g.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess we can add a bool to track the timer's state |
||
| updates->added.clear(); | ||
| updates->removed.clear(); | ||
| updates->added_seen.clear(); | ||
| updates->removed_seen.clear(); | ||
| updates->last_updated = std::chrono::steady_clock::now(); | ||
| } | ||
|
|
||
| bool ClusterManagerImpl::addOrUpdateCluster(const envoy::api::v2::Cluster& cluster, | ||
| const std::string& version_info) { | ||
| // First we need to see if this new config is new or an update to an existing dynamic cluster. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,6 +138,7 @@ class ClusterManagerInitHelper : Logger::Loggable<Logger::Id::upstream> { | |
| COUNTER(cluster_added) \ | ||
| COUNTER(cluster_modified) \ | ||
| COUNTER(cluster_removed) \ | ||
| COUNTER(coalesced_updates) \ | ||
| GAUGE (active_clusters) \ | ||
| GAUGE (warming_clusters) | ||
| // clang-format on | ||
|
|
@@ -361,6 +362,23 @@ class ClusterManagerImpl : public ClusterManager, Logger::Loggable<Logger::Id::u | |
| // This map is ordered so that config dumping is consistent. | ||
| typedef std::map<std::string, ClusterDataPtr> ClusterMap; | ||
|
|
||
| struct PendingUpdates { | ||
| PendingUpdates() {} | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed? |
||
| Event::TimerPtr timer; | ||
| HostVector added; | ||
| HostVector removed; | ||
| std::unordered_set<HostSharedPtr> added_seen; | ||
| std::unordered_set<HostSharedPtr> removed_seen; | ||
| MonotonicTime last_updated; | ||
| }; | ||
| typedef std::shared_ptr<PendingUpdates> PendingUpdatesPtr; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: prefer |
||
| typedef std::unordered_map<uint32_t, PendingUpdatesPtr> PendingUpdatesByPriorityMap; | ||
| typedef std::shared_ptr<PendingUpdatesByPriorityMap> PendingUpdatesByPriorityMapPtr; | ||
| typedef std::unordered_map<std::string, PendingUpdatesByPriorityMapPtr> ClusterUpdatesMap; | ||
|
|
||
| void applyUpdates(const Cluster& cluster, uint32_t priority, PendingUpdatesPtr updates); | ||
| bool scheduleUpdate(const Cluster& cluster, uint32_t priority, const HostVector& hosts_added, | ||
| const HostVector& hosts_removed); | ||
| void createOrUpdateThreadLocalCluster(ClusterData& cluster); | ||
| ProtobufTypes::MessagePtr dumpClusterConfigs(); | ||
| static ClusterManagerStats generateStats(Stats::Scope& scope); | ||
|
|
@@ -394,6 +412,8 @@ class ClusterManagerImpl : public ClusterManager, Logger::Loggable<Logger::Id::u | |
| Grpc::AsyncClientManagerPtr async_client_manager_; | ||
| Server::ConfigTracker::EntryOwnerPtr config_tracker_entry_; | ||
| SystemTimeSource& system_time_source_; | ||
| ClusterUpdatesMap updates_map_; | ||
| Event::Dispatcher& dispatcher_; | ||
| }; | ||
|
|
||
| } // namespace Upstream | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1620,6 +1620,67 @@ TEST_F(ClusterManagerImplTest, OriginalDstInitialization) { | |
| factory_.tls_.shutdownThread(); | ||
| } | ||
|
|
||
| TEST_F(ClusterManagerImplTest, CoalescedUpdates) { | ||
| const std::string yaml = R"EOF( | ||
| static_resources: | ||
| clusters: | ||
| - name: cluster_1 | ||
| connect_timeout: 0.250s | ||
| type: STATIC | ||
| lb_policy: ROUND_ROBIN | ||
| hosts: | ||
| - socket_address: | ||
| address: "127.0.0.1" | ||
| port_value: 11001 | ||
| - socket_address: | ||
| address: "127.0.0.1" | ||
| port_value: 11002 | ||
| common_lb_config: | ||
| update_merge_window: 3s | ||
| )EOF"; | ||
|
|
||
| create(parseBootstrapFromV2Yaml(yaml)); | ||
| EXPECT_FALSE(cluster_manager_->get("cluster_1")->info()->addedViaApi()); | ||
|
|
||
| // Remove each host, sequentially. | ||
| const Cluster& cluster = cluster_manager_->clusters().begin()->second; | ||
|
|
||
| HostVectorSharedPtr hosts( | ||
| new HostVector(cluster.prioritySet().hostSetsPerPriority()[0]->hosts())); | ||
| HostsPerLocalitySharedPtr hosts_per_locality = std::make_shared<HostsPerLocalityImpl>(); | ||
| HostVector hosts_added{}; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: No need for |
||
| HostVector hosts_removed_0{(*hosts)[0]}; | ||
| HostVector hosts_removed_1{(*hosts)[1]}; | ||
|
|
||
| // No timer needs to be mocked at this point, since the first update should be applied | ||
| // immediately. | ||
| cluster.prioritySet().hostSetsPerPriority()[0]->updateHosts( | ||
| hosts, hosts, hosts_per_locality, hosts_per_locality, {}, hosts_added, hosts_removed_0); | ||
|
|
||
| // Now we need the timer to be ready, since createTimer() will be call given that this update | ||
| // _will_ be delayed. | ||
| Event::MockTimer* timer = new NiceMock<Event::MockTimer>(&factory_.dispatcher_); | ||
| cluster.prioritySet().hostSetsPerPriority()[0]->updateHosts( | ||
| hosts, hosts, hosts_per_locality, hosts_per_locality, {}, hosts_added, hosts_removed_1); | ||
|
|
||
| // Ensure the coalesced updates were applied. | ||
| timer->callback_(); | ||
| EXPECT_EQ(1, factory_.stats_.counter("cluster_manager.coalesced_updates").value()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it worth validating that the hosts were actually removed?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you mean from the intermediate sets? or that added/removed were > 0 (i think they can be 0)?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah, now i know what you meant
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, might be a bit messy |
||
|
|
||
| // Prepare a new timer for the next round of updates. | ||
| timer = new NiceMock<Event::MockTimer>(&factory_.dispatcher_); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Arguably, we could do the small optimization I mentioned earlier about reusing timers (have some flag to let you know if its set etc.). |
||
|
|
||
| // Add them back. | ||
| cluster.prioritySet().hostSetsPerPriority()[0]->updateHosts( | ||
| hosts, hosts, hosts_per_locality, hosts_per_locality, {}, hosts_removed_0, hosts_added); | ||
| cluster.prioritySet().hostSetsPerPriority()[0]->updateHosts( | ||
| hosts, hosts, hosts_per_locality, hosts_per_locality, {}, hosts_removed_1, hosts_added); | ||
|
|
||
| // Ensure the coalesced updates were applied again. | ||
| timer->callback_(); | ||
| EXPECT_EQ(2, factory_.stats_.counter("cluster_manager.coalesced_updates").value()); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a test that has add, remove, add for the same host would be interesting. |
||
|
|
||
| class ClusterManagerInitHelperTest : public testing::Test { | ||
| public: | ||
| MOCK_METHOD1(onClusterInit, void(Cluster& cluster)); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unclear to me if we need
PendingUpdatesto be shared/weak here and below; can't we just make this a unique_ptr and share a reference, the timer should be cancelled before we delete the object, or maybe I've missed something?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@htuch i agree, address in the latest commit. LMK what you think. Thanks!