Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ func GetAddAnnotationPatch(annotationName, annotationValue string) resource_admi
}
}

// GetRemoveAnnotationPatch returns a patch to remove an annotation.
func GetRemoveAnnotationPatch(annotationName string) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Op: "remove",
Path: fmt.Sprintf("/metadata/annotations/%s", annotationName),
}
}

// GetAddResourceRequirementValuePatch returns a patch record to add resource requirements to a container.
func GetAddResourceRequirementValuePatch(i int, kind string, resource core.ResourceName, quantity resource.Quantity) resource_admission.PatchRecord {
return resource_admission.PatchRecord{
Expand Down
24 changes: 21 additions & 3 deletions vertical-pod-autoscaler/pkg/updater/inplace/resource_updates.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
)

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

containersResources, _, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
if err != nil {
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
var containersResources []vpa_api_util.ContainerResources
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
// If update mode is "Off", we don't want to apply any recommendations,
// but we still want to unboost.
original, err := annotations.GetOriginalResourcesFromAnnotation(pod)
if err != nil {
return nil, err
}
containersResources = []vpa_api_util.ContainerResources{
{
Requests: original.Requests,
Limits: original.Limits,
},
}
} else {
var err error
containersResources, _, err = c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
if err != nil {
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
}
}

for i, containerResources := range containersResources {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package inplace

import (
core "k8s.io/api/core/v1"

resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/patch"
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
)

type unboostAnnotationPatchCalculator struct{}

// NewUnboostAnnotationCalculator returns a calculator for the unboost annotation patch.
func NewUnboostAnnotationCalculator() patch.Calculator {
return &unboostAnnotationPatchCalculator{}
}

// PatchResourceTarget returns the Pod resource to apply calculator patches.
func (*unboostAnnotationPatchCalculator) PatchResourceTarget() patch.PatchResourceTarget {
return patch.Pod
}

// CalculatePatches calculates the patch to remove the startup CPU boost annotation if the pod is ready to be unboosted.
func (c *unboostAnnotationPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
if vpa_api_util.PodHasCPUBoostInProgress(pod) && vpa_api_util.PodReady(pod) && vpa_api_util.PodStartupBoostDurationPassed(pod, vpa) {
return []resource_admission.PatchRecord{
patch.GetRemoveAnnotationPatch(annotations.StartupCPUBoostAnnotation),
}, nil
}
return []resource_admission.PatchRecord{}, nil
}
77 changes: 63 additions & 14 deletions vertical-pod-autoscaler/pkg/updater/logic/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,12 @@ func (u *updater) RunOnce(ctx context.Context) {
klog.V(3).InfoS("Skipping VPA object in ignored namespace", "vpa", klog.KObj(vpa), "namespace", vpa.Namespace)
continue
}
if vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeRecreate &&
vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeAuto && vpa_api_util.GetUpdateMode(vpa) != vpa_types.UpdateModeInPlaceOrRecreate {
klog.V(3).InfoS("Skipping VPA object because its mode is not \"InPlaceOrRecreate\", \"Recreate\" or \"Auto\"", "vpa", klog.KObj(vpa))
updateMode := vpa_api_util.GetUpdateMode(vpa)
if updateMode != vpa_types.UpdateModeRecreate &&
updateMode != vpa_types.UpdateModeAuto &&
updateMode != vpa_types.UpdateModeInPlaceOrRecreate &&
vpa.Spec.StartupBoost == nil {
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))
continue
}
selector, err := u.selectorFetcher.Fetch(ctx, vpa)
Expand Down Expand Up @@ -226,8 +229,6 @@ func (u *updater) RunOnce(ctx context.Context) {
defer vpasWithInPlaceUpdatablePodsCounter.Observe()
defer vpasWithInPlaceUpdatedPodsCounter.Observe()

// NOTE: this loop assumes that controlledPods are filtered
// to contain only Pods controlled by a VPA in auto, recreate, or inPlaceOrRecreate mode
for vpa, livePods := range controlledPods {
vpaSize := len(livePods)
updateMode := vpa_api_util.GetUpdateMode(vpa)
Expand All @@ -238,31 +239,80 @@ func (u *updater) RunOnce(ctx context.Context) {
continue
}

evictionLimiter := u.restrictionFactory.NewPodsEvictionRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
inPlaceLimiter := u.restrictionFactory.NewPodsInPlaceRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
podsAvailableForUpdate := make([]*apiv1.Pod, 0)
podsToUnboost := make([]*apiv1.Pod, 0)
withInPlaceUpdated := false

podsForInPlace := make([]*apiv1.Pod, 0)
if features.Enabled(features.CPUStartupBoost) && vpa.Spec.StartupBoost != nil {
// First, handle unboosting for pods that have finished their startup period.
for _, pod := range livePods {
if vpa_api_util.PodHasCPUBoostInProgress(pod) {
if vpa_api_util.PodReady(pod) && vpa_api_util.PodStartupBoostDurationPassed(pod, vpa) {
podsToUnboost = append(podsToUnboost, pod)
}
} else {
podsAvailableForUpdate = append(podsAvailableForUpdate, pod)
}
}

// Perform unboosting
for _, pod := range podsToUnboost {
if inPlaceLimiter.CanUnboost(pod, vpa) {
klog.V(2).InfoS("Unboosting pod", "pod", klog.KObj(pod))
err = u.inPlaceRateLimiter.Wait(ctx)
if err != nil {
klog.V(0).InfoS("In-place rate limiter wait failed for unboosting", "error", err)
return
}
err := inPlaceLimiter.InPlaceUpdate(pod, vpa, u.eventRecorder)
if err != nil {
klog.V(0).InfoS("Unboosting failed", "error", err, "pod", klog.KObj(pod))
metrics_updater.RecordFailedInPlaceUpdate(vpaSize, "UnboostError")
} else {
klog.V(2).InfoS("Successfully unboosted pod", "pod", klog.KObj(pod))
withInPlaceUpdated = true
metrics_updater.AddInPlaceUpdatedPod(vpaSize)
}
}
}
} else {
// CPU Startup Boost is not enabled or configured for this VPA,
// so all live pods are available for potential standard VPA updates.
podsAvailableForUpdate = livePods
}

if updateMode == vpa_types.UpdateModeOff || updateMode == vpa_types.UpdateModeInitial {
continue
}

evictionLimiter := u.restrictionFactory.NewPodsEvictionRestriction(creatorToSingleGroupStatsMap, podToReplicaCreatorMap)
podsForEviction := make([]*apiv1.Pod, 0)
podsForInPlace := make([]*apiv1.Pod, 0)
withInPlaceUpdatable := false
withEvictable := false

if updateMode == vpa_types.UpdateModeInPlaceOrRecreate && features.Enabled(features.InPlaceOrRecreate) {
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(livePods, inPlaceLimiter), vpa)
podsForInPlace = u.getPodsUpdateOrder(filterNonInPlaceUpdatablePods(podsAvailableForUpdate, inPlaceLimiter), vpa)
inPlaceUpdatablePodsCounter.Add(vpaSize, len(podsForInPlace))
if len(podsForInPlace) > 0 {
withInPlaceUpdatable = true
}
} else {
// If the feature gate is not enabled but update mode is InPlaceOrRecreate, updater will always fallback to eviction.
if updateMode == vpa_types.UpdateModeInPlaceOrRecreate {
klog.InfoS("Warning: feature gate is not enabled for this updateMode", "featuregate", features.InPlaceOrRecreate, "updateMode", vpa_types.UpdateModeInPlaceOrRecreate)
}
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(livePods, evictionLimiter), vpa)
podsForEviction = u.getPodsUpdateOrder(filterNonEvictablePods(podsAvailableForUpdate, evictionLimiter), vpa)
evictablePodsCounter.Add(vpaSize, updateMode, len(podsForEviction))
if len(podsForEviction) > 0 {
withEvictable = true
}
}

withInPlaceUpdatable := false
withInPlaceUpdated := false
withEvictable := false
withEvicted := false

for _, pod := range podsForInPlace {
withInPlaceUpdatable = true
decision := inPlaceLimiter.CanInPlaceUpdate(pod)

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

for _, pod := range podsForEviction {
withEvictable = true
if !evictionLimiter.CanEvict(pod) {
continue
}
Expand Down
Loading
Loading