Skip to content

Commit a3ce8e2

Browse files
authored
Merge pull request #8458 from kamarabbas99/feature-cpu-ac
Apply CPU startup boost in admission controller if its set
2 parents 220c4a3 + 4b19257 commit a3ce8e2

File tree

9 files changed

+853
-12
lines changed

9 files changed

+853
-12
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` | | | quantity 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: 7 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,8 +79,13 @@ 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 = resource.QuantityValue{}
8183
)
8284

85+
func init() {
86+
flag.Var(&maxAllowedCPUBoost, "max-allowed-cpu-boost", "Maximum amount of CPU that will be applied for a container with boost.")
87+
}
88+
8389
func main() {
8490
commonFlags := common.InitCommonFlags()
8591
klog.InitFlags(nil)
@@ -145,7 +151,7 @@ func main() {
145151
hostname,
146152
)
147153

148-
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider), patch.NewObservedContainersCalculator()}
154+
calculators := []patch.Calculator{patch.NewResourceUpdatesCalculator(recommendationProvider, maxAllowedCPUBoost), patch.NewObservedContainersCalculator()}
149155
as := logic.NewAdmissionServer(podPreprocessor, vpaPreprocessor, limitRangeCalculator, vpaMatcher, calculators)
150156
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
151157
as.Serve(w, r)

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

Lines changed: 156 additions & 5 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,15 @@ 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 resource.QuantityValue) Calculator {
4549
return &resourcesUpdatesPatchCalculator{
4650
recommendationProvider: recommendationProvider,
51+
maxAllowedCPUBoost: maxAllowedCPUBoost.Quantity,
4752
}
4853
}
4954

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

67+
if vpa_api_util.GetUpdateMode(vpa) == vpa_types.UpdateModeOff {
68+
// If update mode is "Off", we don't want to apply any recommendations,
69+
// but we still want to apply startup boost.
70+
for i := range containersResources {
71+
containersResources[i].Requests = nil
72+
containersResources[i].Limits = nil
73+
}
74+
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
75+
}
76+
6277
if annotationsPerContainer == nil {
6378
annotationsPerContainer = vpa_api_util.ContainerToAnnotationsMap{}
6479
}
6580

6681
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)
82+
cpuStartupBoostEnabled := features.Enabled(features.CPUStartupBoost)
83+
for i := range containersResources {
84+
85+
// Apply startup boost if configured
86+
if cpuStartupBoostEnabled {
87+
// Get the container resource policy to check for scaling mode.
88+
policy := vpa_api_util.GetContainerResourcePolicy(pod.Spec.Containers[i].Name, vpa.Spec.ResourcePolicy)
89+
if policy != nil && policy.Mode != nil && *policy.Mode == vpa_types.ContainerScalingModeOff {
90+
continue
91+
}
92+
boostPatches, err := c.applyCPUStartupBoost(&pod.Spec.Containers[i], vpa, &containersResources[i])
93+
if err != nil {
94+
return nil, err
95+
}
96+
result = append(result, boostPatches...)
97+
}
98+
99+
newPatches, newUpdatesAnnotation := getContainerPatch(pod, i, annotationsPerContainer, containersResources[i])
100+
if len(newPatches) > 0 {
101+
result = append(result, newPatches...)
102+
updatesAnnotation = append(updatesAnnotation, newUpdatesAnnotation)
103+
}
71104
}
72105

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

0 commit comments

Comments
 (0)