Skip to content

Commit c967445

Browse files
Jrmy2402jeremy.spriet
authored andcommitted
feat(recommender): add enforce cpu memory ratio
1 parent 0ec850d commit c967445

File tree

4 files changed

+137
-4
lines changed

4 files changed

+137
-4
lines changed

vertical-pod-autoscaler/docs/flags.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ 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. |
6970
| `external-metrics-cpu-metric` | string | | ALPHA. Metric to use with external metrics provider for CPU usage. |
7071
| `external-metrics-memory-metric` | string | | ALPHA. Metric to use with external metrics provider for memory usage. |
7172
| `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/recommender/logic/recommender.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ 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.`)
4344
)
4445

4546
// PodResourceRecommender computes resource recommendation for a Vpa object.
@@ -194,10 +195,10 @@ func MapToListOfRecommendedContainerResources(resources RecommendedPodResources)
194195
for _, name := range containerNames {
195196
containerResources = append(containerResources, vpa_types.RecommendedContainerResources{
196197
ContainerName: name,
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),
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),
201202
})
202203
}
203204
recommendation := &vpa_types.RecommendedPodResources{

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,40 @@ 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+
84118
// ResourcesAsResourceList converts internal Resources representation to ResourcesList.
85119
func ResourcesAsResourceList(resources Resources, humanizeMemory bool, roundCPUMillicores, roundMemoryBytes int) apiv1.ResourceList {
86120
result := make(apiv1.ResourceList)

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,3 +758,100 @@ 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+
}

0 commit comments

Comments
 (0)