Skip to content

Commit aa176bc

Browse files
committed
Apply CPU startup boost in admission controller if its set
1 parent 5cc0654 commit aa176bc

File tree

6 files changed

+511
-8
lines changed

6 files changed

+511
-8
lines changed

vertical-pod-autoscaler/pkg/admission-controller/main.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
"github.com/spf13/pflag"
28+
"k8s.io/apimachinery/pkg/api/resource"
2829
"k8s.io/client-go/informers"
2930
kube_client "k8s.io/client-go/kubernetes"
3031
typedadmregv1 "k8s.io/client-go/kubernetes/typed/admissionregistration/v1"
@@ -78,6 +79,7 @@ var (
7879
registerWebhook = flag.Bool("register-webhook", true, "If set to true, admission webhook object will be created on start up to register with the API server.")
7980
webhookLabels = flag.String("webhook-labels", "", "Comma separated list of labels to add to the webhook object. Format: key1:value1,key2:value2")
8081
registerByURL = flag.Bool("register-by-url", false, "If set to true, admission webhook will be registered by URL (webhookAddress:webhookPort) instead of by service name")
82+
maxAllowedCpu = flag.String("container-recommendation-max-allowed-cpu", "", "Maximum amount of CPU that will be recommended for a container.")
8183
)
8284

8385
func main() {
@@ -93,6 +95,13 @@ func main() {
9395
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
9496
}
9597

98+
if *maxAllowedCpu != "" {
99+
if _, err := resource.ParseQuantity(*maxAllowedCpu); err != nil {
100+
klog.ErrorS(err, "Failed to parse maxAllowedCpu")
101+
klog.FlushAndExit(klog.ExitFlushTimeout, 1)
102+
}
103+
}
104+
96105
healthCheck := metrics.NewHealthCheck(time.Minute)
97106
metrics_admission.Register()
98107
server.Initialize(&commonFlags.EnableProfiling, healthCheck, address)
@@ -145,7 +154,7 @@ func main() {
145154
hostname,
146155
)
147156

148-
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider), patch.NewObservedContainersCalculator()}
157+
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider, *maxAllowedCpu), patch.NewObservedContainersCalculator()}
149158
as := logic.NewAdmissionServer(podPreprocessor, vpaPreprocessor, limitRangeCalculator, vpaMatcher, calculators)
150159
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
151160
as.Serve(w, r)

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

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,14 @@ import (
2121
"strings"
2222

2323
core "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/resource"
25+
"k8s.io/klog/v2"
2426

2527
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
2628
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
2729
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
30+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
31+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
2832
resourcehelpers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/resources"
2933
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
3034
)
@@ -37,13 +41,19 @@ const (
3741

3842
type resourcesUpdatesPatchCalculator struct {
3943
recommendationProvider recommendation.Provider
44+
maxAllowedCpu resource.Quantity
4045
}
4146

4247
// NewResourceUpdatesCalculator returns a calculator for
4348
// resource update patches.
44-
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider) Calculator {
49+
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider, maxAllowedCpu string) Calculator {
50+
var maxAllowedCpuQuantity resource.Quantity
51+
if maxAllowedCpu != "" {
52+
maxAllowedCpuQuantity = resource.MustParse(maxAllowedCpu)
53+
}
4554
return &resourcesUpdatesPatchCalculator{
4655
recommendationProvider: recommendationProvider,
56+
maxAllowedCpu: maxAllowedCpuQuantity,
4757
}
4858
}
4959

@@ -52,11 +62,22 @@ func (*resourcesUpdatesPatchCalculator) PatchResourceTarget() PatchResourceTarge
5262
}
5363

5464
func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *vpa_types.VerticalPodAutoscaler) ([]resource_admission.PatchRecord, error) {
65+
klog.Infof("Calculating patches for pod %s/%s with VPA %s", pod.Namespace, pod.Name, vpa.Name)
5566
result := []resource_admission.PatchRecord{}
5667

5768
containersResources, annotationsPerContainer, err := c.recommendationProvider.GetContainersResourcesForPod(pod, vpa)
5869
if err != nil {
59-
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
70+
return nil, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
71+
}
72+
73+
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
74+
// If update mode is "Off", we don't want to apply any recommendations,
75+
// but we still want to apply startup boost.
76+
for i := range containersResources {
77+
containersResources[i].Requests = nil
78+
containersResources[i].Limits = nil
79+
}
80+
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
6081
}
6182

6283
if annotationsPerContainer == nil {
@@ -65,9 +86,44 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
6586

6687
updatesAnnotation := []string{}
6788
for i, containerResources := range containersResources {
89+
// Apply startup boost if configured
90+
if features.Enabled(features.CPUStartupBoost) {
91+
policy := vpa_api_util.GetContainerResourcePolicy(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
92+
if policy != nil && policy.Mode != nil && *policy.Mode == vpa_types.ContainerScalingModeOff {
93+
klog.V(4).Infof("Not applying startup boost for container %s since its scaling mode is Off", pod.Spec.Containers[i].Name)
94+
continue
95+
} else {
96+
boost, err := getStartupBoost(&pod.Spec.Containers[i], vpa)
97+
if err != nil {
98+
return nil, err
99+
}
100+
if boost != nil {
101+
if !c.maxAllowedCpu.IsZero() && boost.Cmp(c.maxAllowedCpu) > 0 {
102+
cappedBoost := c.maxAllowedCpu
103+
boost = &cappedBoost
104+
}
105+
if containerResources.Requests == nil {
106+
containerResources.Requests = core.ResourceList{}
107+
}
108+
containerResources.Requests[core.ResourceCPU] = *boost
109+
if containerResources.Limits == nil {
110+
containerResources.Limits = core.ResourceList{}
111+
}
112+
containerResources.Limits[core.ResourceCPU] = *boost
113+
originalResources, err := annotations.GetOriginalResourcesAnnotationValue(&pod.Spec.Containers[i])
114+
if err != nil {
115+
return nil, err
116+
}
117+
result = append(result, GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, originalResources))
118+
}
119+
}
120+
}
121+
68122
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containerResources)
69-
result = append(result, newPatches...)
70-
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
123+
if len(newPatches) > 0 {
124+
result = append(result, newPatches...)
125+
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
126+
}
71127
}
72128

73129
if len(updatesAnnotation) > 0 {
@@ -77,6 +133,48 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
77133
return result, nil
78134
}
79135

136+
func getStartupBoost(container *core.Container, vpa *vpa_types.VerticalPodAutoscaler) (*resource.Quantity, error) {
137+
policy := vpa_api_util.GetContainerResourcePolicy(container.Name, vpa.Spec.ResourcePolicy)
138+
startupBoost := vpa.Spec.StartupBoost
139+
if policy != nil && policy.StartupBoost != nil {
140+
startupBoost = policy.StartupBoost
141+
}
142+
if startupBoost == nil {
143+
return nil, nil
144+
}
145+
146+
cpuRequest := container.Resources.Requests[core.ResourceCPU]
147+
boostType := startupBoost.CPU.Type
148+
if boostType == "" {
149+
boostType = vpa_types.FactorStartupBoostType
150+
}
151+
152+
switch boostType {
153+
case vpa_types.FactorStartupBoostType:
154+
if startupBoost.CPU.Factor == nil {
155+
return nil, fmt.Errorf("startupBoost.CPU.Factor is required when Type is Factor or not specified")
156+
}
157+
factor := *startupBoost.CPU.Factor
158+
if factor < 1 {
159+
return nil, fmt.Errorf("boost factor must be >= 1")
160+
}
161+
boostedCPU := cpuRequest.MilliValue()
162+
boostedCPU = int64(float64(boostedCPU) * float64(factor))
163+
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
164+
case vpa_types.QuantityStartupBoostType:
165+
if startupBoost.CPU.Quantity == nil {
166+
return nil, fmt.Errorf("startupBoost.CPU.Quantity is required when Type is Quantity")
167+
}
168+
quantity := *startupBoost.CPU.Quantity
169+
if quantity.Cmp(cpuRequest) < 0 {
170+
return nil, fmt.Errorf("boost quantity %s is less than container's request %s", quantity.String(), cpuRequest.String())
171+
}
172+
return &quantity, nil
173+
default:
174+
return nil, fmt.Errorf("unsupported startup boost type: %s", startupBoost.CPU.Type)
175+
}
176+
}
177+
80178
func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_util.ContainerToAnnotationsMap, containerResources vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, string) {
81179
var patches []resource_admission.PatchRecord
82180
// Add empty resources object if missing.

0 commit comments

Comments
 (0)