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

Cache controllers #3589

Merged
merged 2 commits into from
Nov 24, 2020
Merged

Conversation

jbartosik
Copy link
Collaborator

@jbartosik jbartosik commented Oct 8, 2020

Cache results of attempting to determine the parent a controller.

This is to improve VPAs performance in big clusters, where we need to throttle queries we're making.

How caching is supposed to work:

  • When we first try to determine parent of a controller try to get it (and cache the result).
    • This should happen rarely (when new controllers appear and when VPA starts). So it shouldn't affect VPAs performance heavily
    • It let's us avoid incorrect state
  • On consequent attempts to get parent of a controller return the cached result
    • We do this frequently, so this should significantly improve our performance
  • Refresh content of cache and remove unused entries in parallel

@k8s-ci-robot k8s-ci-robot added the cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. label Oct 8, 2020
@k8s-ci-robot k8s-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Oct 8, 2020
@jbartosik jbartosik force-pushed the cache-controllers branch 5 times, most recently from 9750f74 to 8711a1e Compare October 8, 2020 16:51
@jbartosik
Copy link
Collaborator Author

/hold
I think it's ready for review but want to do some more tests before merging

@k8s-ci-robot k8s-ci-robot added the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 8, 2020
Copy link
Contributor

@mwielgus mwielgus left a comment

Choose a reason for hiding this comment

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

The PR contains non trivial amount of code, but yet it doesn't have any description. Could you please provide some information what it does and why is it needed?

@jbartosik
Copy link
Collaborator Author

The PR contains non trivial amount of code, but yet it doesn't have any description. Could you please provide some information what it does and why is it needed?

I added a description

@jbartosik jbartosik requested a review from mwielgus October 9, 2020 13:01
@jbartosik
Copy link
Collaborator Author

/hold cancel

I ran full-vpa and recommender tests with this PR and they passed.

@k8s-ci-robot k8s-ci-robot removed the do-not-merge/hold Indicates that a PR should not merge because someone has issued a /hold command. label Oct 13, 2020
@jbartosik
Copy link
Collaborator Author

@bskiba PTAL

I checked that with this change and 200 Tekton deployments in a cluster and VPA seems to work correctly

@bskiba
Copy link
Member

bskiba commented Oct 16, 2020

Looking at this now.

@bskiba
Copy link
Member

bskiba commented Oct 16, 2020

OK, I had a first look and looks good, thanks for splitting it into nicely separated commits, makes for a way easier review.
I'll need to take another look on Monday for a deeper review. So far I'd like to confirm some basic things.

  1. When testing with 200 controllers, did you observe api call throttling?
  2. Did you watch the memory consumption? Would be good to confirm we're not leaking, i.e. once we delete the controllers we should see a drop in memory after ttl passes.
  3. With this solution, don't we still issue scale subresource calls when checking if the controller is scalable?
    scale, err := f.scaleNamespacer.Scales(namespace).Get(context.TODO(), groupResource, name, metav1.GetOptions{})

@jbartosik
Copy link
Collaborator Author

OK, I had a first look and looks good, thanks for splitting it into nicely separated commits, makes for a way easier review.
I'll need to take another look on Monday for a deeper review. So far I'd like to confirm some basic things.

  1. When testing with 200 controllers, did you observe api call throttling?

No, I don't see any logs about throttling.

  1. Did you watch the memory consumption? Would be good to confirm we're not leaking, i.e. once we delete the controllers we should see a drop in memory after ttl passes.

I didn't see significant change in memory usage (which is not surprising, we're storing only a couple of controller keys per controller). I'll run another test where I create a bunch of controllers to see memory usage increase, then wait to see if it drops after time.

  1. With this solution, don't we still issue scale subresource calls when checking if the controller is scalable?
    scale, err := f.scaleNamespacer.Scales(namespace).Get(context.TODO(), groupResource, name, metav1.GetOptions{})

We do. I'll add caching for that too. Is it ok if I do this in a separate PR?

@jbartosik
Copy link
Collaborator Author

  1. Did you watch the memory consumption? Would be good to confirm we're not leaking, i.e. once we delete the controllers we should see a drop in memory after ttl passes.

I didn't see significant change in memory usage (which is not surprising, we're storing only a couple of controller keys per controller). I'll run another test where I create a bunch of controllers to see memory usage increase, then wait to see if it drops after time.

I started a a few hundreds of pods. As a result memory usage increased from 23Mi to t o31Mi. When I deleted pods memory usage decreased to 26Mi and later (not sure after how much time) down to 25Mi. I'll repeat and see what happens

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Oct 26, 2020
@jbartosik
Copy link
Collaborator Author

@kgolab PTAL

@jbartosik
Copy link
Collaborator Author

  1. With this solution, don't we still issue scale subresource calls when checking if the controller is scalable?
    scale, err := f.scaleNamespacer.Scales(namespace).Get(context.TODO(), groupResource, name, metav1.GetOptions{})

We do. I'll add caching for that too. Is it ok if I do this in a separate PR?

Done

defer cc.mux.Unlock()
now := now()
for k, v := range cc.cache {
if now.After(v.refreshAfter) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd like to understand the idea behind refreshing & clearing stale entries a little better.

IIUC when an entry becomes idle, we'd refresh it few times before deleting which seems counterintuitive.
If this is true, shouldn't we throttle refreshes only to entries which were read recently? Then Get should probably return false to force a refresh upon reading an idle (& not refreshed) entry.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I chose the simplest approach I thought would work (periodically refresh all entries, remove entries that weren't read in a while).

If this is true, shouldn't we throttle refreshes only to entries which were read recently? Then Get should probably return false to force a refresh upon reading an idle (& not refreshed) entry.

If Get returns false then the entry is effectively removed. So if I understand your idea correctly it's effectively the same as setting shorter lifetime for cache entries (less than 2 refresh durations?)

Copy link
Collaborator

Choose a reason for hiding this comment

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

If it's about simplicity then why not just:

  • Insert sets validity time (let's say it's current scaleCacheEntryFreshnessTime),
  • Get checks validity time and returns false if the entry is expired; the caller has to call Insert then,
  • there is no background refresh,
  • there is only background garbage collection?

This reduces number of calls to scaleNamespacer.Get as we never call it for entires that are no longer present.
I understand that the drawback of this solution is that all Gets are executed in the main VPA loop instead of being spread evenly over time.

So basically the discussion boils down to a question: "how long do we actively use an entry, on average"; if it's much longer than scaleCacheEntryLifetime, the amortised cost of multiple refreshes of a no-longer-used entry is low and then the gain from spreading scaleNamespacer.Get calls is likely more important.

If you think we'd indeed have long-lived entries (seems plausible) let's leave the solution as it is now.
I'd love to see some metric (or logs with counter) added at some point so we can assess the impact of this change and maybe fine-tune scaleCacheEntryLifetime vs scaleCacheEntryFreshnessTime, likely lowering the former.

@jbartosik
Copy link
Collaborator Author

@kgolab PTAL

Many questions and some nits but I guess it's worth discussing how exactly the case is expected to behave with regards to refreshes and removal of stale entries.

I pushed changes with comments I applied. I had questions about a few and left them open. I'll add explanation why I want the cache to work this way and a TODO we discussed later today.

@kgolab I added the explanation (above controllerCacheStorage) and TODO. Please take a look

@jbartosik
Copy link
Collaborator Author

@kgolab @bskiba Please take a look

@bskiba
Copy link
Member

bskiba commented Nov 3, 2020

I've set aside some time tomorrow to take a look 👀

if _, ok := cc.cache[key]; ok {
return
}
jitter := time.Duration(rand.Float64()*float64(cc.refreshJitter.Nanoseconds())) * time.Nanosecond
Copy link
Member

Choose a reason for hiding this comment

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

The function is not used here

@jbartosik
Copy link
Collaborator Author

@bskiba @kgolab I pushed changes, please take a look

@jbartosik
Copy link
Collaborator Author

@bskiba I can't reply directly to the comment about using jitter function from API machinery but I applied it

Copy link
Member

@bskiba bskiba left a comment

Choose a reason for hiding this comment

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

Some nits left.

Overall I think we are close to what we want, would like to make sure the cache constants we choose work in our favor.

@kgolab Would you be able to take another look today? I would like you to explicitly approve as well once you're happy with this PR

if ok, scale, err := f.controllerCache.Get(namespace, groupResource, name); ok {
return scale, err
}
scale, err := f.scaleNamespacer.Scales(namespace).Get(context.TODO(), groupResource, name, metav1.GetOptions{})
Copy link
Member

Choose a reason for hiding this comment

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

That makes sense, I'm not gonna be supper stubborn about it :)

@jbartosik
Copy link
Collaborator Author

jbartosik commented Nov 10, 2020

@bskiba @kgolab PTAL

Copy link
Member

@bskiba bskiba left a comment

Choose a reason for hiding this comment

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

I'm happy, I'll LGTM once kgolab takes one more look (I think more eyes are better here as caching is always tricky :) )

@bskiba
Copy link
Member

bskiba commented Nov 10, 2020

And thanks a lot for the work that went into this!

@jbartosik
Copy link
Collaborator Author

@kgolab please take a look

@@ -49,7 +49,13 @@ import (
resourceclient "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
)

const defaultResyncPeriod time.Duration = 10 * time.Minute
const (
scaleCacheLoopPeriod time.Duration = time.Minute
Copy link
Collaborator

@kgolab kgolab Nov 20, 2020

Choose a reason for hiding this comment

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

If you really want this to be spread independently from the VPA main loop, maybe choose some other period, preferably relatively prime to main loop's 60s period?

IIUC it might be a relatively short one, e.g. 13s, as the refresh loop is pretty lightweight except for scaleNamespacer.Get calls which won't change in number but instead get spread more evenly.

Reading through older comments I should've expressed myself clearer when asking for lowering this value from 10 minutes, sorry.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. 7s because it's the shortest that's in full seconds and relatively prime

@kgolab
Copy link
Collaborator

kgolab commented Nov 20, 2020

Thanks!

I added more comments but this already looks good to me.
Please note though that some of this comments are in older conversations so might be more tricky to find, I don't know how to make github show them all together.

And I'm really sorry for taking so much time to get back to this. Please ping me earlier next time.

@kgolab
Copy link
Collaborator

kgolab commented Nov 20, 2020

@bskiba , I cannot execute the command so letting you know here: LGTM

@jbartosik
Copy link
Collaborator Author

@kgolab replies to comments I can't find here but got email notifications for:

  • we don't want to refresh entries in the main loop so it wont' get stuck so I'm keeping as is (but I'm making refresh loop execute more frequently)
  • I added a TODO to add something that will let us optimize performance. I'll probably do it in two stages. First I'll add something to let us determine if we need to improve performance. If we think we need performance improvements I think it's best to use something more detailed than average lifetime (e.g. I think we might want to check for changes in young collections and error responses more quickly than in collections which were around for long time, unchanged)
  • I changed the test for inserting over an old value to not care which value it keeps
  • renamed controllerCache to scaleSubresourceCacheStorage

Type that caches results of attempts to get parent of controllers.
@bskiba
Copy link
Member

bskiba commented Nov 24, 2020

Thanks a lot Joachim
/lgtm
/approve

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label Nov 24, 2020
@k8s-ci-robot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: bskiba, jbartosik

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot merged commit 857477d into kubernetes:master Nov 24, 2020
crgarcia12 added a commit to crgarcia12/autoscaler that referenced this pull request Nov 25, 2020
@jbartosik jbartosik mentioned this pull request Nov 27, 2020
@jbartosik jbartosik deleted the cache-controllers branch January 15, 2021 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. lgtm "Looks good to me", indicates that a PR is ready to be merged. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants