Skip to content

Commit 7c55ed5

Browse files
committed
Make changes to updater to add the unboosting logic
1 parent a3ce8e2 commit 7c55ed5

File tree

10 files changed

+661
-33
lines changed

10 files changed

+661
-33
lines changed

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: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
22+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
23+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
24+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
25+
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
26+
)
27+
28+
type unboostAnnotationPatchCalculator struct{}
29+
30+
// NewUnboostAnnotationCalculator returns a calculator for the unboost annotation patch.
31+
func NewUnboostAnnotationCalculator() patch.Calculator {
32+
return &unboostAnnotationPatchCalculator{}
33+
}
34+
35+
// PatchResourceTarget returns the Pod resource to apply calculator patches.
36+
func (*unboostAnnotationPatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
37+
return patch.Pod
38+
}
39+
40+
// CalculatePatches calculates the patch to remove the startup CPU boost annotation if the pod is ready to be unboosted.
41+
func (c *unboostAnnotationPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
42+
if vpa_api_util.PodHasCPUBoostInProgress(pod) && vpa_api_util.PodReady(pod) && vpa_api_util.PodStartupBoostDurationPassed(pod, vpa) {
43+
return []resource_admission.PatchRecord{
44+
patch.GetRemoveAnnotationPatch(annotations.StartupCPUBoostAnnotation),
45+
}, nil
46+
}
47+
return []resource_admission.PatchRecord{}, nil
48+
}

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

Lines changed: 63 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,6 @@ 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
231232
for vpa, livePods := range controlledPods {
232233
vpaSize := len(livePods)
233234
updateMode := vpa_api_util.GetUpdateMode(vpa)
@@ -238,31 +239,80 @@ func (u *updater) RunOnce(ctx context.Context) {
238239
continue
239240
}
240241

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

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

247295
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate && features.Enabled(features.InPlaceOrRecreate) {
248-
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(livePods, inPlaceLimiter), vpa)
296+
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(podsAvailableForUpdate, inPlaceLimiter), vpa)
249297
inPlaceUpdatablePodsCounter.Add(vpaSize, len(podsForInPlace))
298+
if len(podsForInPlace) > 0 {
299+
withInPlaceUpdatable = true
300+
}
250301
} else {
251302
// If the feature gate is not enabled but update mode is InPlaceOrRecreate, updater will always fallback to eviction.
252303
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate {
253304
klog.InfoS("Warning: feature gate is not enabled for this updateMode", "featuregate", features.InPlaceOrRecreate, "updateMode", vpa_types.UpdateModeInPlaceOrRecreate)
254305
}
255-
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(livePods, evictionLimiter), vpa)
306+
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(podsAvailableForUpdate, evictionLimiter), vpa)
256307
evictablePodsCounter.Add(vpaSize, updateMode, len(podsForEviction))
308+
if len(podsForEviction) > 0 {
309+
withEvictable = true
310+
}
257311
}
258312

259-
withInPlaceUpdatable := false
260-
withInPlaceUpdated := false
261-
withEvictable := false
262313
withEvicted := false
263314

264315
for _, pod := range podsForInPlace {
265-
withInPlaceUpdatable = true
266316
decision := inPlaceLimiter.CanInPlaceUpdate(pod)
267317

268318
if decision == utils.InPlaceDeferred {
@@ -289,7 +339,6 @@ func (u *updater) RunOnce(ctx context.Context) {
289339
}
290340

291341
for _, pod := range podsForEviction {
292-
withEvictable = true
293342
if !evictionLimiter.CanEvict(pod) {
294343
continue
295344
}

0 commit comments

Comments
 (0)