Skip to content

Commit a3ea8ba

Browse files
Jrmy2402jeremy.spriet
authored andcommitted
feat(recommender): enforce CPU/memory ratio via VPA API instead of flag
1 parent c967445 commit a3ea8ba

File tree

10 files changed

+276
-137
lines changed

10 files changed

+276
-137
lines changed

vertical-pod-autoscaler/deploy/vpa-v1-crd-gen.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,18 @@ spec:
354354
Specifies the maximum amount of resources that will be recommended
355355
for the container. The default is no maximum.
356356
type: object
357+
memoryPerCPU:
358+
anyOf:
359+
- type: integer
360+
- type: string
361+
description: |-
362+
Enforce a fixed memory-per-CPU ratio for this container’s recommendations.
363+
If set, the recommender will adjust memory or CPU so that:
364+
memory_bytes = cpu_cores * memoryPerCPU (bytes per 1 core).
365+
Applied to Target, LowerBound, UpperBound, and UncappedTarget.
366+
Example: "4Gi" means 1 CPU -> 4 GiB.
367+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
368+
x-kubernetes-int-or-string: true
357369
minAllowed:
358370
additionalProperties:
359371
anyOf:

vertical-pod-autoscaler/docs/api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ _Appears in:_
4848
| `maxAllowed` _[ResourceList](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcelist-v1-core)_ | Specifies the maximum amount of resources that will be recommended<br />for the container. The default is no maximum. | | |
4949
| `controlledResources` _[ResourceName](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcename-v1-core)_ | Specifies the type of recommendations that will be computed<br />(and possibly applied) by VPA.<br />If not specified, the default of [ResourceCPU, ResourceMemory] will be used. | | |
5050
| `controlledValues` _[ContainerControlledValues](#containercontrolledvalues)_ | Specifies which resource values should be controlled.<br />The default is "RequestsAndLimits". | | Enum: [RequestsAndLimits RequestsOnly] <br /> |
51+
| `memoryPerCPU` _[Quantity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#quantity-resource-api)_ | Enforce a fixed memory-per-CPU ratio for this container’s recommendations.<br />If set, the recommender will adjust memory or CPU so that:<br /> memory_bytes = cpu_cores * memoryPerCPU (bytes per 1 core).<br />Applied to Target, LowerBound, UpperBound, and UncappedTarget.<br />Example: "4Gi" means 1 CPU -> 4 GiB. | | |
5152

5253

5354
#### ContainerScalingMode

vertical-pod-autoscaler/docs/flags.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@ This document is auto-generated from the flag definitions in the VPA recommender
6666
| `container-recommendation-max-allowed-memory` | | | quantity Maximum amount of memory that will be recommended for a container. VerticalPodAutoscaler-level maximum allowed takes precedence over the global maximum allowed. |
6767
| `cpu-histogram-decay-half-life` | | 24h0m0s | duration The amount of time it takes a historical CPU usage sample to lose half of its weight. |
6868
| `cpu-integer-post-processor-enabled` | | | Enable the cpu-integer recommendation post processor. The post processor will round up CPU recommendations to a whole CPU for pods which were opted in by setting an appropriate label on VPA object (experimental) |
69-
| `enforce-cpu-memory-ratio` | float | | If > 0, enforce a fixed memory-per-CPU ratio expressed as bytes per millicores across all recommendations. |
7069
| `external-metrics-cpu-metric` | string | | ALPHA. Metric to use with external metrics provider for CPU usage. |
7170
| `external-metrics-memory-metric` | string | | ALPHA. Metric to use with external metrics provider for memory usage. |
7271
| `feature-gates` | mapStringBool | | A set of key=value pairs that describe feature gates for alpha/experimental features. Options are:<br>AllAlpha=true\|false (ALPHA - default=false)<br>AllBeta=true\|false (BETA - default=false)<br>InPlaceOrRecreate=true\|false (BETA - default=true) |

vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1/types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package v1
1919
import (
2020
autoscaling "k8s.io/api/autoscaling/v1"
2121
v1 "k8s.io/api/core/v1"
22+
"k8s.io/apimachinery/pkg/api/resource"
2223
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2324
)
2425

@@ -221,6 +222,14 @@ type ContainerResourcePolicy struct {
221222
// The default is "RequestsAndLimits".
222223
// +optional
223224
ControlledValues *ContainerControlledValues `json:"controlledValues,omitempty" protobuf:"bytes,6,rep,name=controlledValues"`
225+
226+
// Enforce a fixed memory-per-CPU ratio for this container’s recommendations.
227+
// If set, the recommender will adjust memory or CPU so that:
228+
// memory_bytes = cpu_cores * memoryPerCPU (bytes per 1 core).
229+
// Applied to Target, LowerBound, UpperBound, and UncappedTarget.
230+
// Example: "4Gi" means 1 CPU -> 4 GiB.
231+
// +optional
232+
MemoryPerCPU *resource.Quantity `json:"memoryPerCPU,omitempty"`
224233
}
225234

226235
const (

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ var (
4040
humanizeMemory = flag.Bool("humanize-memory", false, "DEPRECATED: Convert memory values in recommendations to the highest appropriate SI unit with up to 2 decimal places for better readability. This flag is deprecated and will be removed in a future version. Use --round-memory-bytes instead.")
4141
roundCPUMillicores = flag.Int("round-cpu-millicores", 1, `CPU recommendation rounding factor in millicores. The CPU value will always be rounded up to the nearest multiple of this factor.`)
4242
roundMemoryBytes = flag.Int("round-memory-bytes", 1, `Memory recommendation rounding factor in bytes. The Memory value will always be rounded up to the nearest multiple of this factor.`)
43-
enforceCPUMemoryRatio = flag.Float64("enforce-cpu-memory-ratio", 0, `If > 0, enforce a fixed memory-per-CPU ratio expressed as bytes per millicores across all recommendations.`)
4443
)
4544

4645
// PodResourceRecommender computes resource recommendation for a Vpa object.
@@ -195,10 +194,10 @@ func MapToListOfRecommendedContainerResources(resources RecommendedPodResources)
195194
for _, name := range containerNames {
196195
containerResources = append(containerResources, vpa_types.RecommendedContainerResources{
197196
ContainerName: name,
198-
Target: model.EnforceCPUMemoryRatio(model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes), enforceCPUMemoryRatio),
199-
LowerBound: model.EnforceCPUMemoryRatio(model.ResourcesAsResourceList(resources[name].LowerBound, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes), enforceCPUMemoryRatio),
200-
UpperBound: model.EnforceCPUMemoryRatio(model.ResourcesAsResourceList(resources[name].UpperBound, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes), enforceCPUMemoryRatio),
201-
UncappedTarget: model.EnforceCPUMemoryRatio(model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes), enforceCPUMemoryRatio),
197+
Target: model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes),
198+
LowerBound: model.ResourcesAsResourceList(resources[name].LowerBound, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes),
199+
UpperBound: model.ResourcesAsResourceList(resources[name].UpperBound, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes),
200+
UncappedTarget: model.ResourcesAsResourceList(resources[name].Target, *humanizeMemory, *roundCPUMillicores, *roundMemoryBytes),
202201
})
203202
}
204203
recommendation := &vpa_types.RecommendedPodResources{

vertical-pod-autoscaler/pkg/recommender/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ func run(ctx context.Context, healthCheck *metrics.HealthCheck, commonFlag *comm
268268
postProcessors = append(postProcessors, &routines.IntegerCPUPostProcessor{})
269269
}
270270

271+
postProcessors = append(postProcessors, &routines.MemoryPerCPUPostProcessor{})
271272
globalMaxAllowed := initGlobalMaxAllowed()
272273
// CappingPostProcessor, should always come in the last position for post-processing
273274
postProcessors = append(postProcessors, routines.NewCappingRecommendationProcessor(globalMaxAllowed))

vertical-pod-autoscaler/pkg/recommender/model/types.go

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -81,40 +81,6 @@ func ScaleResource(amount ResourceAmount, factor float64) ResourceAmount {
8181
return resourceAmountFromFloat(float64(amount) * factor)
8282
}
8383

84-
// EnforceCPUMemoryRatio adjusts the CPU or Memory to maintain a fixed ratio in bytes per millicore.
85-
// If the actual memory per millicore is too low, memory is increased.
86-
// If it is too high, CPU is increased.
87-
func EnforceCPUMemoryRatio(resources apiv1.ResourceList, ratioBytesPerMillicore *float64) apiv1.ResourceList {
88-
if ratioBytesPerMillicore == nil || *ratioBytesPerMillicore <= 0 {
89-
// No ratio specified or invalid ratio, nothing to do
90-
return resources
91-
}
92-
93-
cpuQty, hasCPU := resources[apiv1.ResourceCPU]
94-
memQty, hasMem := resources[apiv1.ResourceMemory]
95-
96-
if !hasCPU || !hasMem || cpuQty.IsZero() || memQty.IsZero() {
97-
return resources
98-
}
99-
100-
cpuMilli := float64(cpuQty.MilliValue())
101-
memBytes := float64(memQty.Value())
102-
103-
currentRatio := memBytes / cpuMilli
104-
105-
if currentRatio < *ratioBytesPerMillicore {
106-
// Not enough RAM for the given CPU → increase memory
107-
desiredMem := cpuMilli * *ratioBytesPerMillicore
108-
resources[apiv1.ResourceMemory] = *resource.NewQuantity(int64(desiredMem), resource.BinarySI)
109-
} else if currentRatio > *ratioBytesPerMillicore {
110-
// Too much RAM for the given CPU → increase CPU
111-
desiredCPU := memBytes / *ratioBytesPerMillicore
112-
resources[apiv1.ResourceCPU] = *resource.NewMilliQuantity(int64(desiredCPU), resource.DecimalSI)
113-
}
114-
115-
return resources
116-
}
117-
11884
// ResourcesAsResourceList converts internal Resources representation to ResourcesList.
11985
func ResourcesAsResourceList(resources Resources, humanizeMemory bool, roundCPUMillicores, roundMemoryBytes int) apiv1.ResourceList {
12086
result := make(apiv1.ResourceList)

vertical-pod-autoscaler/pkg/recommender/model/types_test.go

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -758,100 +758,3 @@ func TestResourceAmountFromFloat(t *testing.T) {
758758
})
759759
}
760760
}
761-
762-
type EnforceCPUMemoryRatioTestCase struct {
763-
name string
764-
input apiv1.ResourceList
765-
ratio *float64
766-
expected apiv1.ResourceList
767-
}
768-
769-
func TestEnforceCPUMemoryRatio2(t *testing.T) {
770-
// 1 CPU -> 4 GiB => bytes per millicore
771-
ratio4GiBPerCore := float64(4*1024*1024*1024) / 1000.0 // 4_294_967.296
772-
773-
tc := []EnforceCPUMemoryRatioTestCase{
774-
{
775-
name: "no ratio provided",
776-
input: apiv1.ResourceList{
777-
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
778-
apiv1.ResourceMemory: *resource.NewQuantity(4*1024*1024*1024, resource.BinarySI),
779-
},
780-
ratio: nil,
781-
expected: apiv1.ResourceList{
782-
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
783-
apiv1.ResourceMemory: *resource.NewQuantity(4*1024*1024*1024, resource.BinarySI),
784-
},
785-
},
786-
{
787-
name: "valid ratio already respected",
788-
input: apiv1.ResourceList{
789-
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), // 2 cores
790-
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
791-
},
792-
ratio: float64Ptr(ratio4GiBPerCore),
793-
expected: apiv1.ResourceList{
794-
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI),
795-
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI),
796-
},
797-
},
798-
{
799-
name: "too much RAM, should increase CPU",
800-
input: apiv1.ResourceList{
801-
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), // 1 core
802-
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
803-
},
804-
ratio: float64Ptr(ratio4GiBPerCore),
805-
expected: apiv1.ResourceList{
806-
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), // 8Gi / 4 = 2 cores
807-
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI),
808-
},
809-
},
810-
{
811-
name: "not enough RAM, should increase RAM",
812-
input: apiv1.ResourceList{
813-
apiv1.ResourceCPU: *resource.NewMilliQuantity(4000, resource.DecimalSI), // 4 cores
814-
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
815-
},
816-
ratio: float64Ptr(ratio4GiBPerCore),
817-
expected: apiv1.ResourceList{
818-
apiv1.ResourceCPU: *resource.NewMilliQuantity(4000, resource.DecimalSI),
819-
apiv1.ResourceMemory: *resource.NewQuantity(16*1024*1024*1024, resource.BinarySI), // 4 cores * 4 = 16Gi
820-
},
821-
},
822-
{
823-
name: "missing memory, no-op",
824-
input: apiv1.ResourceList{
825-
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
826-
},
827-
ratio: float64Ptr(ratio4GiBPerCore),
828-
expected: apiv1.ResourceList{
829-
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
830-
},
831-
},
832-
{
833-
name: "zero values, no-op",
834-
input: apiv1.ResourceList{
835-
apiv1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI),
836-
apiv1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
837-
},
838-
ratio: float64Ptr(ratio4GiBPerCore),
839-
expected: apiv1.ResourceList{
840-
apiv1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI),
841-
apiv1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
842-
},
843-
},
844-
}
845-
846-
for _, tc := range tc {
847-
t.Run(tc.name, func(t *testing.T) {
848-
result := EnforceCPUMemoryRatio(tc.input.DeepCopy(), tc.ratio)
849-
assert.Equal(t, tc.expected[apiv1.ResourceCPU], result[apiv1.ResourceCPU])
850-
assert.Equal(t, tc.expected[apiv1.ResourceMemory], result[apiv1.ResourceMemory])
851-
})
852-
}
853-
}
854-
855-
func float64Ptr(v float64) *float64 {
856-
return &v
857-
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
Copyright 2022 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 routines
18+
19+
import (
20+
apiv1 "k8s.io/api/core/v1"
21+
"k8s.io/apimachinery/pkg/api/resource"
22+
vpa_types "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1"
23+
vpa_utils "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/utils/vpa"
24+
)
25+
26+
// MemoryPerCPUPostProcessor enforces a fixed memory-per-CPU ratio for each container's recommendation.
27+
// The ratio is defined in the container's policy as MemoryPerCPU (bytes per 1 CPU core).
28+
// Applied to Target, LowerBound, UpperBound, and UncappedTarget.
29+
type MemoryPerCPUPostProcessor struct{}
30+
31+
var _ RecommendationPostProcessor = &MemoryPerCPUPostProcessor{}
32+
33+
// Process applies the memory-per-CPU enforcement to the recommendation if specified in the container policy.
34+
func (p *MemoryPerCPUPostProcessor) Process(
35+
vpa *vpa_types.VerticalPodAutoscaler,
36+
recommendation *vpa_types.RecommendedPodResources,
37+
) *vpa_types.RecommendedPodResources {
38+
if vpa == nil || vpa.Spec.ResourcePolicy == nil || recommendation == nil {
39+
return recommendation
40+
}
41+
42+
amendedRecommendation := recommendation.DeepCopy()
43+
44+
for _, r := range amendedRecommendation.ContainerRecommendations {
45+
pol := vpa_utils.GetContainerResourcePolicy(r.ContainerName, vpa.Spec.ResourcePolicy)
46+
if pol != nil && pol.MemoryPerCPU != nil {
47+
memPerCPUBytes := pol.MemoryPerCPU.Value()
48+
r.Target = enforceMemoryPerCPU(r.Target, memPerCPUBytes)
49+
r.LowerBound = enforceMemoryPerCPU(r.LowerBound, memPerCPUBytes)
50+
r.UpperBound = enforceMemoryPerCPU(r.UpperBound, memPerCPUBytes)
51+
r.UncappedTarget = enforceMemoryPerCPU(r.UncappedTarget, memPerCPUBytes)
52+
}
53+
}
54+
55+
return amendedRecommendation
56+
}
57+
58+
// enforceMemoryPerCPU adjusts CPU or Memory to satisfy:
59+
//
60+
// memory_bytes = cpu_cores * memPerCPUBytes
61+
//
62+
// If memory is too low for the given CPU, increase memory.
63+
// If memory is too high for the given CPU, increase CPU.
64+
// enforceMemoryPerCPU adjusts CPU or Memory to satisfy:
65+
//
66+
// memory_bytes = cpu_cores * memPerCPUBytes
67+
//
68+
// If memory is too low for the given CPU, increase memory.
69+
// If memory is too high for the given CPU, increase CPU.
70+
func enforceMemoryPerCPU(resources apiv1.ResourceList, bytesPerCore int64) apiv1.ResourceList {
71+
if bytesPerCore <= 0 {
72+
return resources
73+
}
74+
75+
cpuQty, hasCPU := resources[apiv1.ResourceCPU]
76+
memQty, hasMem := resources[apiv1.ResourceMemory]
77+
if !hasCPU || !hasMem || cpuQty.IsZero() || memQty.IsZero() {
78+
return resources
79+
}
80+
81+
// cpuCores = milliCPU / 1000
82+
cpuMilli := cpuQty.MilliValue()
83+
memBytes := memQty.Value()
84+
85+
// Desired memory in bytes = CPU cores * bytes per core
86+
desiredMem := divCeil(cpuMilli*bytesPerCore, 1000)
87+
88+
if memBytes < desiredMem {
89+
// Not enough RAM → increase memory
90+
resources[apiv1.ResourceMemory] = *resource.NewQuantity(desiredMem, resource.BinarySI)
91+
} else if memBytes > desiredMem {
92+
// Too much RAM → increase CPU
93+
desiredMilli := divCeil(memBytes*1000, bytesPerCore)
94+
resources[apiv1.ResourceCPU] = *resource.NewMilliQuantity(desiredMilli, resource.DecimalSI)
95+
}
96+
97+
return resources
98+
}
99+
100+
func divCeil(a, b int64) int64 {
101+
return (a + b - 1) / b
102+
}

0 commit comments

Comments
 (0)