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

Avoid extra predicate() calls #3869

Merged
merged 6 commits into from
Jul 18, 2018

Conversation

rgs1
Copy link
Member

@rgs1 rgs1 commented Jul 16, 2018

In SubsetLoadBalancer::HostSubsetImpl::update() we have repeated calls
to predicate(). These calls are expensive mostly because of
Envoy::Config::Metadata::metadataValue() calls. Furthermore, metadata
is guarded by a lock since #3814 — so there is some contention too.

This change improves things by doing two things:

  • avoid use of predicate() to derive healthy_hosts_per_locality
  • cache predicate() calls for the special case of only metadata
    changing (current hosts == hosts added)

Signed-off-by: Raul Gutierrez Segales rgs@pinterest.com

In `SubsetLoadBalancer::HostSubsetImpl::update()` we have repeated calls
to `predicate()`. These calls are expensive mostly because of
`Envoy::Config::Metadata::metadataValue()` calls. Furthermore, metadata
is guarded by a lock since envoyproxy#3814 — so there is some contention too.

This change improves things by doing two things:
* avoid use of `predicate()` to derive `healthy_hosts_per_locality`
* cache `predicate()` calls for the special case of only metadata
  changing (current hosts == hosts added)

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
@rgs1
Copy link
Member Author

rgs1 commented Jul 16, 2018

A bit more context here, in SubsetLoadBalancer::HostSubsetImpl::update() (https://github.com/envoyproxy/envoy/blob/master/source/common/upstream/subset_lb.cc) the following is going on:

* go over added hosts (when only metadata change, this is O(n) where n is total hosts)
* go over removed hosts (not a big deal, usually)
* go over original hosts (O(n) where n is total hosts)
* go over hosts per locality (O(n) with n == total hosts) 
* go over hosts per locality and extra healthy ones (again, O(n)

Things get pretty expensive because on every iteration we have to call hostMatches() (https://github.com/envoyproxy/envoy/blob/master/source/common/upstream/subset_lb.cc#L276), which requires a lock and (the very expensive part) metadata lookups and metadata value extraction. From our perf samples, the Envoy::Config::Metadata::metadataValue() calls dominate.

So ideally, we can:

  1. collapse the iterations as much as possible
  2. cache predicate() calls when possible

This diff starts in that way, but there's more I think.

Thoughts?

cc: @mattklein123 @zuercher

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
Copy link
Member

@zuercher zuercher left a comment

Choose a reason for hiding this comment

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

Looks good. One comment about a potential simplification. I'll leave it up to you if you want to do that or not.

@@ -477,20 +480,26 @@ void SubsetLoadBalancer::HostSubsetImpl::update(const HostVector& hosts_added,
HostVectorSharedPtr hosts(new HostVector());
HostVectorSharedPtr healthy_hosts(new HostVector());

// It's possible that hosts_added == original_host_set_.hosts(), e.g.: when
// calling refreshSubsets() if only metadata change. If so, we can avoid the
// predicate() call.
Copy link
Member

Choose a reason for hiding this comment

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

Alternatively, I think you could change refreshSubsets to not pass the host set as hosts added since the underlying LBs don't need use information anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

I would like to do that — it would improve things a lot — but we'd fail to enter this loop in processSubsets:

https://github.com/envoyproxy/envoy/blob/master/source/common/upstream/subset_lb.cc#L200

... which means we wouldn't create the new subsets, if any.

So we'd have to rework processSubsets() or also call findOrCreateSubset() from somewhere else...

I'll look into it, but we can probably merge this in parallel since it's still better.

zuercher
zuercher previously approved these changes Jul 16, 2018
hosts->emplace_back(host);
if (host->healthy()) {
healthy_hosts->emplace_back(host);
}
}
}

// Calling predicate() is expensive since it involves metadata lookups; so we
// can take a shortcut and just filter healthy hosts in a follow-up call.
Copy link
Member

Choose a reason for hiding this comment

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

Sorry just for my understanding, what follow-up call? How come we don't have to do this predicate call here? Maybe make the comment a little more detailed?

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, yes — the follow-up call is the 2nd call to filter(). I'll comment in more detail, and there's actually one more (significant) perf improvement we can have....

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see it now. Sorry did not read it closely. I think maybe you meant "in the follow-up call" ?

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
This is one more optimization to avoid filter() calls using
hostMatches() as a predicate when possible.

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
@rgs1
Copy link
Member Author

rgs1 commented Jul 17, 2018

@zuercher @mattklein123 lets actually not merge this yet. I ran the patch through perf today and although it's nice to see the metadataValue() calls go away, CPU does go up a bit because of the ordered_set calls. So I'd like to play with this a bit more before we merge it.

On a related note, I got into this patch while debugging a case in which a given stream of EDS updates will cause the subset lb to push envoy into 100% CPU usage. I'll try to get a repro tomorrow.

Config::Metadata::metadataValue() is pretty general, which means
we end up calling host_metadata.filter_metadata().find() from
within the main loop of hostMatches().

This should get rid of some of hostMatches()'s overhead.

Tests: running this through perf now to measure.

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
@rgs1
Copy link
Member Author

rgs1 commented Jul 17, 2018

One last thought: I think most improvements are good to go except for maybe using the ordered_set to cache the first set of hostMatches() calls in SubsetLoadBalancer::HostSubsetImpl::update(). I'll do extensive perf runs tomorrow to decide if we want to keep that optimization or not.

Leftovers from inline metadataValue().

Signed-off-by: Raul Gutierrez Segales <rgs@pinterest.com>
@rgs1
Copy link
Member Author

rgs1 commented Jul 17, 2018

@mattklein123 @zuercher on a 2nd thought, if you like it as is we can actually merge it and I can run out of master for a few days and see how it goes. Worst case if it isn't great or that much better, we revert that parts that we don't like.

@rgs1
Copy link
Member Author

rgs1 commented Jul 17, 2018

FWIW, the CPU increase that I saw yesterday was not from this change. I am bisecting now to see who's driving that increase, so I think we are clear here. Perf issue follow-up #3878.

@mattklein123 mattklein123 merged commit 17efc83 into envoyproxy:master Jul 18, 2018
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.

3 participants