-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Add Capacity Buffer controller logic #8521
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
base: master
Are you sure you want to change the base?
Add Capacity Buffer controller logic #8521
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: abdelrahman882 The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
555355e
to
829faae
Compare
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.
A lot of duplicate comments, most important to address:
- Naming should not include buffer infixes, it is redundant
- Implementations should be private and not exposed outside of packages.
- Cleanup methods for filters remove their configs, this is a bug.
- The usage of filter interface should be consistent, eg. we should use the first return value in both cases. This will make it easier to maintain the implementations without caring about the use cases.
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/translators/buffers_translator.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/translators/buffers_translator.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/translators/pod_template_buffer_translator_test.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/updater/buffers_status_updater.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/controller/buffer_controller.go
Outdated
Show resolved
Hide resolved
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.
Reviewed the logic and structure. Did not look into tests as I have seen that @BigDarkClown did that already.
select { | ||
case <-stopCh: | ||
return | ||
case <-time.After(c.loopInterval): |
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.
Shouldn't we run the loop only if there are any buffers and have a watcher on creation of new so that it does not run without a good reason?
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.
As mentioned in the reply below that will contradict with the periodic check over updated buffer objects and quota checking
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.
I meant that we should not run the loop at all if there is no a single buffer object and for starting the loop we should rely on a watch that will inform us that a buffer was created. Once there are any buffers in the cluster we should run periodically and if all get deleted go back to relying on the watch. What would it contradict?
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.
(Note that this can be implemented as optimization later)
|
||
// Constants to use in Capacity Buffers objects | ||
const ( | ||
ActiveProvisioningStrategy = "active-capacity" |
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.
Should this be in common or should it be in some dedicated filter/translator.
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.
The provisioning filter is generic, it filters the buffers with strategy defined when constructing, so we pass ActiveProvisioningStrategy
to the generic provisioningFilter in the controller.
I would prefer keeping it in common because filter
package is not only for strategy filtering so filter.ActiveProvisioningStrategy
wouldn't give much more meaning imo and also in case it would be used by other filters/translators.
But It's not strong opinion, I would change it if you insist
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.
I was thinking that we would keep the logic for a single buffer type in a single place and therefore keep the name together with translation logic and only make the buffer controller rely on the list of supported types. They would reuse the translation logic so that it is not implemented multiple times.
This is simple refactoring though so let's focus on having something working end to end first.
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.
Also the plan was to go with "buffer.x-k8s.io/active-capacity" rather than just active-capacity as per discussion from https://docs.google.com/document/d/1bcct-luMPP51YAeUhVuFV7MIXud5wqHsVBDah9WuKeo/edit?disco=AAABj1R4vNs
(this will require update to validations)
nodeBufferListener v1.CapacityBufferLister, | ||
kubeClient kube_client.Clientset, | ||
) BufferController { | ||
return &bufferController{ |
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.
I thought we would split it more by buffer prov strategy or so in case different strategies need different handling, but for now I would say that it may be better to leave it as is and make sure that when adding second one we will introduce a structure that makes sense at that time.
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.
In this structure it would be very hard to implement for example if translation of pods to pod template differs depending on the provisioning strategy. But as mentioned - not worth creating the whole structure up front. The logic seems well split to be reused and this is more important now
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.
My understanding was that the controller will be operating on only one provisioningStrategy - So just to double check that I understood correctly, we need to have the provisioningStrategies mapped to set of translators and depending on the defined strategy we route the buffer to its corresponding translator, is that correct?
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.
I am not sure whether we need this structure right away but I was assuming that we have a single controller to support all the provisioning strategies that have built in support. (If the user wants to have some custom ones they are free to implement their own controller and handle their translation and execution).
cluster-autoscaler/capacitybuffer/filters/buffers_strategy_filter.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/filters/buffers_strategy_filter.go
Outdated
Show resolved
Hide resolved
cluster-autoscaler/capacitybuffer/translators/buffers_translator.go
Outdated
Show resolved
Hide resolved
return errors | ||
} | ||
|
||
func (b *PodTemplateBufferTranslator) translate(buffer *api_v1.CapacityBuffer) (*api_v1.LocalObjectRef, int32, error) { |
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.
When do you plant to take the buffer.spec limit into account? Also the error is incorrect if you take into account the limits.
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.
The limits will be applied by adding another translator for the limits, which will be executed last and will enforce the limits on the number of pods overriding replicas wrote by previous translators (podTemplateRef or scalableRef).
For your question when
, It will be in separate PR right after merging this one, I have it already implemented but wrapping up its unit tests
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.
In this case the error will be incorrect as we were planning to support an option: do not provide number of replicas and we will count how many replicas fit into the limit.
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.
I would rather have a single place where we calculate the whole translation but provide libraries that handle limits or quotas so that you do not have to reimplement if there are many buffers that need the same logic. Chaining translators is like chaining processors - powerful but later to make any changes you need to understand the whole chain.
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.
IMO it's better to keep it chained in this way, I mean to make a change you don't need to understand the whole chain at all but rather understand what you want you translator to do only.
One note, I will need to double check the API then because we have Xor=replicas,percentage
which I am not sure if it will block having both set to nil
Okay so, my suggestion is to keep it as chained translators, and in the following PR -with introducing limits- I will be double checking the API validation to accept both replicas and percentage as nil along side with fixing this translator here to not return error, does that make sense?
Also I would appreciate if you took a look on the CRD validation and please let me know if you notice something odd.
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.
We can do this in a follow up, but I do not like the idea of chaining in general.
If we provide libraries you will still have to understand only the part that you are modifying, but if you want to read what happens with a single buffer you will just read through a single place that has library calls rather than go through list of translators that are applied one on top of each other making modifying them more complex and error handling much harder. I know that we do a lot of this with processors in our code, but this is because these are extension points. Here I do not see a reason to do it as a list of translators.
829faae
to
312308a
Compare
return &bufferController{ | ||
buffersLister: nodeBufferListener, | ||
strategyFilter: filters.NewStrategyFilter([]string{common.ActiveProvisioningStrategy}), | ||
statusFilter: filters.NewBuffersStatusFilter(map[string]string{ |
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.
Note that filtering on status isn't likely a good idea:
- for pod template we will need to store the generation id. If the pod is update we have to process it once again (as there are limits)
- for quotas we will need to process all the buffers over and over again to make sure that they fit into quotas
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.
Got it,
- For generation id: will update the status filter to include
generation
so we process those with higher generation id - For Quotas: will create another translator that applies on all buffers -including filtered out buffers- that updates status replicas
As mentioned Would add in following PR, this one is big already.
One note here we will need to run the loop periodically and not only if there are any buffers with a watcher on creation -from your previous comment- so we can check quotas and updates, does that make sense to you?
3524194
to
5c1e317
Compare
5c1e317
to
1a7d949
Compare
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.
One micro nit. Overall LGTM, waiting for LGTM from @jbtk to add approval.
/lgtm
} | ||
|
||
func (f *strategyFilter) isAllowedProvisioningStrategy(buffer *v1.CapacityBuffer) bool { | ||
provisioingStrategy := "" |
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.
nit: typo provisioning
/release-note-none |
/release-note-edit
|
@jackfrancis: /release-note-edit must be used with a single release note block. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. |
/release-note-edit
|
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.
Re validations (since the PR is already merged and we are discussing here some of the things):
- xor on percentage and replicas is wrong - you can define neither (the replicas will be counted based on number of pod templates fitting the limits), you can also define both (this will mean that we will use number of replicas as minimum)
- maximum for percentage - you could keep it higher than 100, for example 200 would mean that we keep twice the number of replicas compared to the current size of a workload. I guess this is not a common thing to do but I do not see why we should introduce this limitation
) BufferController { | ||
return &bufferController{ | ||
buffersLister: nodeBufferListener, | ||
strategyFilter: filters.NewStrategyFilter([]string{common.ActiveProvisioningStrategy, ""}), |
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.
Please leave a comment for why the empty is accepted.
return errors | ||
} | ||
|
||
func (b *PodTemplateBufferTranslator) translate(buffer *api_v1.CapacityBuffer) (*api_v1.LocalObjectRef, int32, error) { |
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.
We can do this in a follow up, but I do not like the idea of chaining in general.
If we provide libraries you will still have to understand only the part that you are modifying, but if you want to read what happens with a single buffer you will just read through a single place that has library calls rather than go through list of translators that are applied one on top of each other making modifying them more complex and error handling much harder. I know that we do a lot of this with processors in our code, but this is because these are extension points. Here I do not see a reason to do it as a list of translators.
|
||
// Constants to use in Capacity Buffers objects | ||
const ( | ||
ActiveProvisioningStrategy = "active-capacity" |
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.
Also the plan was to go with "buffer.x-k8s.io/active-capacity" rather than just active-capacity as per discussion from https://docs.google.com/document/d/1bcct-luMPP51YAeUhVuFV7MIXud5wqHsVBDah9WuKeo/edit?disco=AAABj1R4vNs
(this will require update to validations)
What type of PR is this?
/kind feature
What this PR does / why we need it:
Add Capacity Buffer controller loop along with the main needed skeleton for buffers with
podTemplateRef
.Special notes for your reviewer:
This PR includes:
podTemplateRef
translator that updates buffer status accordinglyThis PR is not including:
Capacity buffers resources limits and scalable objects (those will be introduced as translators)
Buffers updating logic (use
PodTemplateGeneration
for updating buffer status)These 2 points will be included in a following PR together
Running controller in CA and CA injection will be in separate PR
Proposal document: https://github.com/kubernetes/autoscaler/blob/master/cluster-autoscaler/proposals/buffers.md
Does this PR introduce a user-facing change?
no
Additional documentation e.g., KEPs (Kubernetes Enhancement Proposals), usage docs, etc.: