Skip to content

Commit f1fb13c

Browse files
committed
Apply CPU startup boost in admission controller if its set
1 parent 0000a7a commit f1fb13c

File tree

9 files changed

+792
-13
lines changed

9 files changed

+792
-13
lines changed

vertical-pod-autoscaler/docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This document is auto-generated from the flag definitions in the VPA admission-c
2424
| `log-file` | string | | If non-empty, use this log file (no effect when -logtostderr=true) |
2525
| `log-file-max-size` | int | 1800 | uDefines the maximum size a log file can grow to (no effect when -logtostderr=true). Unit is megabytes. If the value is 0, the maximum file size is unlimited. |
2626
| `logtostderr` | | true | log to standard error instead of files |
27+
| `max-allowed-cpu-boost` | string | | Maximum amount of CPU that will be applied for a container with boost. |
2728
| `min-tls-version` | string | | The minimum TLS version to accept. Must be set to either tls1_2 or tls1_3. (default "tls1_2") |
2829
| `one-output` | severity | | If true, only write logs to their native level (vs also writing to each lower severity level; no effect when -logtostderr=true) |
2930
| `port` | int | 8000 | The port to listen on. |

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+
maxAllowedCPUBoost = flag.String("max-allowed-cpu-boost", "", "Maximum amount of CPU that will be applied for a container with boost.")
8183
)
8284

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

98+
if *maxAllowedCPUBoost != "" {
99+
if _, err := resource.ParseQuantity(*maxAllowedCPUBoost); err != nil {
100+
klog.ErrorS(err, "Failed to parse maxAllowedCPUBoost")
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, *maxAllowedCPUBoost), 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: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ import (
2121
"strings"
2222

2323
core "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/resource"
2425

2526
resource_admission "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource"
2627
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/admission-controller/resource/pod/recommendation"
2728
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
29+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/features"
30+
"k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/annotations"
2831
resourcehelpers "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/resources"
2932
vpa_api_util "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
3033
)
@@ -37,13 +40,19 @@ const (
3740

3841
type resourcesUpdatesPatchCalculator struct {
3942
recommendationProvider recommendation.Provider
43+
maxAllowedCPUBoost resource.Quantity
4044
}
4145

4246
// NewResourceUpdatesCalculator returns a calculator for
4347
// resource update patches.
44-
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider) Calculator {
48+
func NewResourceUpdatesCalculator(recommendationProvider recommendation.Provider, maxAllowedCPUBoost string) Calculator {
49+
var maxAllowedCPUBoostQuantity resource.Quantity
50+
if maxAllowedCPUBoost != "" {
51+
maxAllowedCPUBoostQuantity = resource.MustParse(maxAllowedCPUBoost)
52+
}
4553
return &resourcesUpdatesPatchCalculator{
4654
recommendationProvider: recommendationProvider,
55+
maxAllowedCPUBoost: maxAllowedCPUBoostQuantity,
4756
}
4857
}
4958

@@ -59,15 +68,42 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
5968
return []resource_admission.PatchRecord{}, fmt.Errorf("failed to calculate resource patch for pod %s/%s: %v", pod.Namespace, pod.Name, err)
6069
}
6170

71+
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
72+
// If update mode is "Off", we don't want to apply any recommendations,
73+
// but we still want to apply startup boost.
74+
for i := range containersResources {
75+
containersResources[i].Requests = nil
76+
containersResources[i].Limits = nil
77+
}
78+
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
79+
}
80+
6281
if annotationsPerContainer == nil {
6382
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
6483
}
6584

6685
updatesAnnotation := []string{}
67-
for i, containerResources := range containersResources {
68-
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containerResources)
69-
result = append(result, newPatches...)
70-
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
86+
for i := range containersResources {
87+
88+
// Apply startup boost if configured
89+
if features.Enabled(features.CPUStartupBoost) {
90+
// Get the container resource policy to check for scaling mode.
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+
continue
94+
}
95+
boostPatches, err := c.applyCPUStartupBoost(i, &pod.Spec.Containers[i], vpa, &containersResources[i])
96+
if err != nil {
97+
return nil, err
98+
}
99+
result = append(result, boostPatches...)
100+
}
101+
102+
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, &containersResources[i])
103+
if len(newPatches) > 0 {
104+
result = append(result, newPatches...)
105+
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
106+
}
71107
}
72108

73109
if len(updatesAnnotation) > 0 {
@@ -77,7 +113,7 @@ func (c *resourcesUpdatesPatchCalculator) CalculatePatches(pod *core.Pod, vpa *v
77113
return result, nil
78114
}
79115

80-
func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_util.ContainerToAnnotationsMap, containerResources vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, string) {
116+
func getContainerPatch(pod *core.Pod, i int, annotationsPerContainer vpa_api_util.ContainerToAnnotationsMap, containerResources *vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, string) {
81117
var patches []resource_admission.PatchRecord
82118
// Add empty resources object if missing.
83119
requests, limits := resourcehelpers.ContainerRequestsAndLimits(pod.Spec.Containers[i].Name, pod)
@@ -108,3 +144,114 @@ func appendPatchesAndAnnotations(patches []resource_admission.PatchRecord, annot
108144
}
109145
return patches, annotations
110146
}
147+
148+
func (c *resourcesUpdatesPatchCalculator) applyCPUStartupBoost(containerIndex int, container *core.Container, vpa *vpa_types.VerticalPodAutoscaler, containerResources *vpa_api_util.ContainerResources) ([]resource_admission.PatchRecord, error) {
149+
var patches []resource_admission.PatchRecord
150+
151+
startupBoostPolicy := getContainerStartupBoostPolicy(container, vpa)
152+
if startupBoostPolicy == nil {
153+
return nil, nil
154+
}
155+
156+
originalRequest := container.Resources.Requests[core.ResourceCPU]
157+
boostedRequest, err := calculateBoostedCPU(originalRequest, startupBoostPolicy)
158+
if err != nil {
159+
return nil, err
160+
}
161+
162+
if !c.maxAllowedCPUBoost.IsZero() && boostedRequest.Cmp(c.maxAllowedCPUBoost) > 0 {
163+
boostedRequest = &c.maxAllowedCPUBoost
164+
}
165+
166+
controlledCPUResourcePatches, err := c.applyControlledCPUResources(containerIndex, container, vpa, containerResources, boostedRequest, startupBoostPolicy)
167+
if err != nil {
168+
return nil, err
169+
}
170+
patches = append(patches, controlledCPUResourcePatches...)
171+
172+
originalResources, err := annotations.GetOriginalResourcesAnnotationValue(container)
173+
if err != nil {
174+
return nil, err
175+
}
176+
patches = append(patches, GetAddAnnotationPatch(annotations.StartupCPUBoostAnnotation, originalResources))
177+
178+
return patches, nil
179+
}
180+
181+
func getContainerStartupBoostPolicy(container *core.Container, vpa *vpa_types.VerticalPodAutoscaler) *vpa_types.StartupBoost {
182+
policy := vpa_api_util.GetContainerResourcePolicy(container.Name, vpa.Spec.ResourcePolicy)
183+
startupBoost := vpa.Spec.StartupBoost
184+
if policy != nil && policy.StartupBoost != nil {
185+
startupBoost = policy.StartupBoost
186+
}
187+
return startupBoost
188+
}
189+
190+
func calculateBoostedCPU(baseCPU resource.Quantity, startupBoost *vpa_types.StartupBoost) (*resource.Quantity, error) {
191+
if startupBoost == nil {
192+
return &baseCPU, nil
193+
}
194+
195+
boostType := startupBoost.CPU.Type
196+
if boostType == "" {
197+
boostType = vpa_types.FactorStartupBoostType
198+
}
199+
200+
switch boostType {
201+
case vpa_types.FactorStartupBoostType:
202+
if startupBoost.CPU.Factor == nil {
203+
return nil, fmt.Errorf("startupBoost.CPU.Factor is required when Type is Factor or not specified")
204+
}
205+
factor := *startupBoost.CPU.Factor
206+
if factor < 1 {
207+
return nil, fmt.Errorf("boost factor must be >= 1")
208+
}
209+
boostedCPU := baseCPU.MilliValue()
210+
boostedCPU = int64(float64(boostedCPU) * float64(factor))
211+
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
212+
case vpa_types.QuantityStartupBoostType:
213+
if startupBoost.CPU.Quantity == nil {
214+
return nil, fmt.Errorf("startupBoost.CPU.Quantity is required when Type is Quantity")
215+
}
216+
quantity := *startupBoost.CPU.Quantity
217+
boostedCPU := baseCPU.MilliValue() + quantity.MilliValue()
218+
return resource.NewMilliQuantity(boostedCPU, resource.DecimalSI), nil
219+
default:
220+
return nil, fmt.Errorf("unsupported startup boost type: %s", startupBoost.CPU.Type)
221+
}
222+
}
223+
224+
func (c *resourcesUpdatesPatchCalculator) applyControlledCPUResources(containerIndex int, container *core.Container, vpa *vpa_types.VerticalPodAutoscaler, containerResources *vpa_api_util.ContainerResources, boostedRequest *resource.Quantity, startupBoostPolicy *vpa_types.StartupBoost) ([]resource_admission.PatchRecord, error) {
225+
controlledValues := vpa_api_util.GetContainerControlledValues(container.Name, vpa.Spec.ResourcePolicy)
226+
227+
// Apply CPU Requests
228+
if containerResources.Requests == nil {
229+
containerResources.Requests = core.ResourceList{}
230+
}
231+
resourceList := core.ResourceList{core.ResourceCPU: *boostedRequest}
232+
if controlledValues == vpa_types.ContainerControlledValuesRequestsOnly {
233+
vpa_api_util.CapRecommendationToContainerLimit(resourceList, container.Resources.Limits)
234+
}
235+
containerResources.Requests[core.ResourceCPU] = resourceList[core.ResourceCPU]
236+
237+
// Apply CPU Limits
238+
if controlledValues == vpa_types.ContainerControlledValuesRequestsAndLimits {
239+
if containerResources.Limits == nil {
240+
containerResources.Limits = core.ResourceList{}
241+
}
242+
originalLimit := container.Resources.Limits[core.ResourceCPU]
243+
if originalLimit.IsZero() {
244+
originalLimit = container.Resources.Requests[core.ResourceCPU]
245+
}
246+
boostedLimit, err := calculateBoostedCPU(originalLimit, startupBoostPolicy)
247+
if err != nil {
248+
return nil, err
249+
}
250+
if !c.maxAllowedCPUBoost.IsZero() && boostedLimit.Cmp(c.maxAllowedCPUBoost) > 0 {
251+
boostedLimit = &c.maxAllowedCPUBoost
252+
}
253+
containerResources.Limits[core.ResourceCPU] = *boostedLimit
254+
}
255+
256+
return nil, nil
257+
}

0 commit comments

Comments
 (0)