Skip to content

Commit 32c68bd

Browse files
authored
Merge pull request #8618 from kamarabbas99/feature-cpu-upd
Make changes to updater to add the unboosting logic
2 parents c66ebc1 + 88a6748 commit 32c68bd

File tree

12 files changed

+614
-33
lines changed

12 files changed

+614
-33
lines changed

vertical-pod-autoscaler/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
k8s.io/code-generator v0.33.0
2020
k8s.io/component-base v0.33.0
2121
k8s.io/klog/v2 v2.130.1
22+
k8s.io/kubernetes v1.33.0
2223
k8s.io/metrics v0.33.0
2324
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979
2425
)
@@ -66,6 +67,9 @@ require (
6667
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
6768
gopkg.in/inf.v0 v0.9.1 // indirect
6869
gopkg.in/yaml.v3 v3.0.1 // indirect
70+
k8s.io/apiextensions-apiserver v0.0.0 // indirect
71+
k8s.io/apiserver v0.33.0 // indirect
72+
k8s.io/controller-manager v0.0.0 // indirect
6973
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 // indirect
7074
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
7175
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect

vertical-pod-autoscaler/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,28 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
175175
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
176176
k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
177177
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
178+
k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs=
179+
k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc=
178180
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
179181
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
182+
k8s.io/apiserver v0.33.0 h1:QqcM6c+qEEjkOODHppFXRiw/cE2zP85704YrQ9YaBbc=
183+
k8s.io/apiserver v0.33.0/go.mod h1:EixYOit0YTxt8zrO2kBU7ixAtxFce9gKGq367nFmqI8=
180184
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
181185
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
182186
k8s.io/code-generator v0.33.0 h1:B212FVl6EFqNmlgdOZYWNi77yBv+ed3QgQsMR8YQCw4=
183187
k8s.io/code-generator v0.33.0/go.mod h1:KnJRokGxjvbBQkSJkbVuBbu6z4B0rC7ynkpY5Aw6m9o=
184188
k8s.io/component-base v0.33.0 h1:Ot4PyJI+0JAD9covDhwLp9UNkUja209OzsJ4FzScBNk=
185189
k8s.io/component-base v0.33.0/go.mod h1:aXYZLbw3kihdkOPMDhWbjGCO6sg+luw554KP51t8qCU=
190+
k8s.io/controller-manager v0.33.0 h1:O9LnTjffOe62d66gMcKLuPXsBjY5sqETWEIzg+DVL8w=
191+
k8s.io/controller-manager v0.33.0/go.mod h1:vQwAQnroav4+UyE2acW1Rj6CSsHPzr2/018kgRLYqlI=
186192
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7 h1:2OX19X59HxDprNCVrWi6jb7LW1PoqTlYqEq5H2oetog=
187193
k8s.io/gengo/v2 v2.0.0-20250207200755-1244d31929d7/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU=
188194
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
189195
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
190196
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
191197
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
198+
k8s.io/kubernetes v1.33.0 h1:BP5Y5yIzUZVeBuE/ESZvnw6TNxjXbLsCckIkljE+R0U=
199+
k8s.io/kubernetes v1.33.0/go.mod h1:2nWuPk0seE4+6sd0x60wQ6rYEXcV7SoeMbU0YbFm/5k=
192200
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
193201
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
194202
k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg=

vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch/util.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ func GetAddAnnotationPatch(annotationName, annotationValue string) resource_admi
4343
}
4444
}
4545

46+
// GetRemoveAnnotationPatch returns a patch to remove an annotation.
47+
func GetRemoveAnnotationPatch(annotationName string) resource_admission.PatchRecord {
48+
return resource_admission.PatchRecord{
49+
Op: "remove",
50+
Path: fmt.Sprintf("/metadata/annotations/%s", annotationName),
51+
}
52+
}
53+
4654
// GetAddResourceRequirementValuePatch returns a patch record to add resource requirements to a container.
4755
func GetAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
4856
return resource_admission.PatchRecord{

vertical-pod-autoscaler/pkg/updater/inplace/resource_updates.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
2626
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
2727
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
28+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
2829
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
2930
)
3031

@@ -49,9 +50,26 @@ func (*resourcesInplaceUpdatesPatchCalculator) PatchResourceTarget() patch.Patch
4950
func (c *resourcesInplaceUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
5051
result := []resource_admission.PatchRecord{}
5152

52-
containersResources, _, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
53-
if err != nil {
54-
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
53+
var containersResources []vpa_api_util.ContainerResources
54+
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
55+
// If update mode is "Off", we don't want to apply any recommendations,
56+
// but we still want to unboost.
57+
original, err := annotations.GetOriginalResourcesFromAnnotation(pod)
58+
if err != nil {
59+
return nil, err
60+
}
61+
containersResources = []vpa_api_util.ContainerResources{
62+
{
63+
Requests: original.Requests,
64+
Limits: original.Limits,
65+
},
66+
}
67+
} else {
68+
var err error
69+
containersResources, _, err = c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
70+
if err != nil {
71+
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
72+
}
5573
}
5674

5775
for i, containerResources := range containersResources {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package inplace
18+
19+
import (
20+
core "k8s.io/api/core/v1"
21+
22+
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
23+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
24+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
25+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
26+
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
27+
)
28+
29+
type unboostAnnotationPatchCalculator struct{}
30+
31+
// NewUnboostAnnotationCalculator returns a calculator for the unboost annotation patch.
32+
func NewUnboostAnnotationCalculator() patch.Calculator {
33+
return &unboostAnnotationPatchCalculator{}
34+
}
35+
36+
// PatchResourceTarget returns the Pod resource to apply calculator patches.
37+
func (*unboostAnnotationPatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
38+
return patch.Pod
39+
}
40+
41+
// CalculatePatches calculates the patch to remove the startup CPU boost annotation if the pod is ready to be unboosted.
42+
func (c *unboostAnnotationPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
43+
if vpa_api_util.PodHasCPUBoostInProgress(pod) && vpa_api_util.IsPodReadyAndStartupBoostDurationPassed(pod, vpa) {
44+
return []resource_admission.PatchRecord{
45+
patch.GetRemoveAnnotationPatch(annotations.StartupCPUBoostAnnotation),
46+
}, nil
47+
}
48+
return []resource_admission.PatchRecord{}, nil
49+
}

vertical-pod-autoscaler/pkg/updater/logic/updater.go

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,12 @@ func (u *updater) RunOnce(ctx context.Context) {
159159
klog.V(3).InfoS("Skipping VPA object in ignored namespace", "vpa", klog.KObj(vpa), "namespace", vpa.Namespace)
160160
continue
161161
}
162-
if vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeRecreate &&
163-
vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeAuto && vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeInPlaceOrRecreate {
164-
klog.V(3).InfoS("Skipping VPA object because its mode is not \"InPlaceOrRecreate\", \"Recreate\" or \"Auto\"", "vpa", klog.KObj(vpa))
162+
updateMode := vpa_api_util.GetUpdateMode(vpa)
163+
if updateMode != vpa_types.UpdateModeRecreate &&
164+
updateMode != vpa_types.UpdateModeAuto &&
165+
updateMode != vpa_types.UpdateModeInPlaceOrRecreate &&
166+
vpa.Spec.StartupBoost == nil {
167+
klog.V(3).InfoS("Skipping VPA object because its mode is not \"InPlaceOrRecreate\", \"Recreate\" or \"Auto\" and it doesn't have startupBoost configured", "vpa", klog.KObj(vpa))
165168
continue
166169
}
167170
selector, err := u.selectorFetcher.Fetch(ctx, vpa)
@@ -226,8 +229,7 @@ func (u *updater) RunOnce(ctx context.Context) {
226229
defer vpasWithInPlaceUpdatablePodsCounter.Observe()
227230
defer vpasWithInPlaceUpdatedPodsCounter.Observe()
228231

229-
// NOTE: this loop assumes that controlledPods are filtered
230-
// to contain only Pods controlled by a VPA in auto, recreate, or inPlaceOrRecreate mode
232+
cpuStartupBoostEnabled := features.Enabled(features.CPUStartupBoost)
231233
for vpa, livePods := range controlledPods {
232234
vpaSize := len(livePods)
233235
updateMode := vpa_api_util.GetUpdateMode(vpa)
@@ -238,31 +240,80 @@ func (u *updater) RunOnce(ctx context.Context) {
238240
continue
239241
}
240242

241-
evictionLimiter := u.restrictionFactory.NewPodsEvictionRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
242243
inPlaceLimiter := u.restrictionFactory.NewPodsInPlaceRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
244+
podsAvailableForUpdate := make([]*apiv1.Pod, 0)
245+
podsToUnboost := make([]*apiv1.Pod, 0)
246+
withInPlaceUpdated := false
243247

244-
podsForInPlace := make([]*apiv1.Pod, 0)
248+
if cpuStartupBoostEnabled && vpa.Spec.StartupBoost != nil {
249+
// First, handle unboosting for pods that have finished their startup period.
250+
for _, pod := range livePods {
251+
if vpa_api_util.PodHasCPUBoostInProgress(pod) {
252+
if vpa_api_util.IsPodReadyAndStartupBoostDurationPassed(pod, vpa) {
253+
podsToUnboost = append(podsToUnboost, pod)
254+
}
255+
} else {
256+
podsAvailableForUpdate = append(podsAvailableForUpdate, pod)
257+
}
258+
}
259+
260+
// Perform unboosting
261+
for _, pod := range podsToUnboost {
262+
if inPlaceLimiter.CanUnboost(pod, vpa) {
263+
klog.V(2).InfoS("Unboosting pod", "pod", klog.KObj(pod))
264+
err = u.inPlaceRateLimiter.Wait(ctx)
265+
if err != nil {
266+
klog.V(0).InfoS("In-place rate limiter wait failed for unboosting", "error", err)
267+
return
268+
}
269+
err := inPlaceLimiter.InPlaceUpdate(pod, vpa, u.eventRecorder)
270+
if err != nil {
271+
klog.V(0).InfoS("Unboosting failed", "error", err, "pod", klog.KObj(pod))
272+
metrics_updater.RecordFailedInPlaceUpdate(vpaSize, "UnboostError")
273+
} else {
274+
klog.V(2).InfoS("Successfully unboosted pod", "pod", klog.KObj(pod))
275+
withInPlaceUpdated = true
276+
metrics_updater.AddInPlaceUpdatedPod(vpaSize)
277+
}
278+
}
279+
}
280+
} else {
281+
// CPU Startup Boost is not enabled or configured for this VPA,
282+
// so all live pods are available for potential standard VPA updates.
283+
podsAvailableForUpdate = livePods
284+
}
285+
286+
if updateMode == vpa_types.UpdateModeOff || updateMode == vpa_types.UpdateModeInitial {
287+
continue
288+
}
289+
290+
evictionLimiter := u.restrictionFactory.NewPodsEvictionRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
245291
podsForEviction := make([]*apiv1.Pod, 0)
292+
podsForInPlace := make([]*apiv1.Pod, 0)
293+
withInPlaceUpdatable := false
294+
withEvictable := false
246295

247296
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate && features.Enabled(features.InPlaceOrRecreate) {
248-
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(livePods, inPlaceLimiter), vpa)
297+
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(podsAvailableForUpdate, inPlaceLimiter), vpa)
249298
inPlaceUpdatablePodsCounter.Add(vpaSize, len(podsForInPlace))
299+
if len(podsForInPlace) > 0 {
300+
withInPlaceUpdatable = true
301+
}
250302
} else {
251303
// If the feature gate is not enabled but update mode is InPlaceOrRecreate, updater will always fallback to eviction.
252304
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate {
253305
klog.InfoS("Warning: feature gate is not enabled for this updateMode", "featuregate", features.InPlaceOrRecreate, "updateMode", vpa_types.UpdateModeInPlaceOrRecreate)
254306
}
255-
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(livePods, evictionLimiter), vpa)
307+
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(podsAvailableForUpdate, evictionLimiter), vpa)
256308
evictablePodsCounter.Add(vpaSize, updateMode, len(podsForEviction))
309+
if len(podsForEviction) > 0 {
310+
withEvictable = true
311+
}
257312
}
258313

259-
withInPlaceUpdatable := false
260-
withInPlaceUpdated := false
261-
withEvictable := false
262314
withEvicted := false
263315

264316
for _, pod := range podsForInPlace {
265-
withInPlaceUpdatable = true
266317
decision := inPlaceLimiter.CanInPlaceUpdate(pod)
267318

268319
if decision == utils.InPlaceDeferred {
@@ -289,7 +340,6 @@ func (u *updater) RunOnce(ctx context.Context) {
289340
}
290341

291342
for _, pod := range podsForEviction {
292-
withEvictable = true
293343
if !evictionLimiter.CanEvict(pod) {
294344
continue
295345
}

0 commit comments

Comments
 (0)