Skip to content

Commit d914a14

Browse files
author
jeremy.spriet
committed
feat(recommender): add enforce cpu memory ratio
1 parent 2289138 commit d914a14

File tree

4 files changed

+134
-4
lines changed

4 files changed

+134
-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 | 0 | 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 (ALPHA - default=false) |

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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,3 +758,97 @@ 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+
tc := []EnforceCPUMemoryRatioTestCase{
771+
{
772+
name: "no ratio provided",
773+
input: apiv1.ResourceList{
774+
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
775+
apiv1.ResourceMemory: *resource.NewQuantity(4*1024*1024*1024, resource.BinarySI),
776+
},
777+
ratio: nil,
778+
expected: apiv1.ResourceList{
779+
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
780+
apiv1.ResourceMemory: *resource.NewQuantity(4*1024*1024*1024, resource.BinarySI),
781+
},
782+
},
783+
{
784+
name: "valid ratio already respected",
785+
input: apiv1.ResourceList{
786+
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), // 2 cores
787+
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
788+
},
789+
ratio: float64Ptr(4.0),
790+
expected: apiv1.ResourceList{
791+
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI),
792+
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI),
793+
},
794+
},
795+
{
796+
name: "too much RAM, should increase CPU",
797+
input: apiv1.ResourceList{
798+
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI), // 1 core
799+
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
800+
},
801+
ratio: float64Ptr(4.0),
802+
expected: apiv1.ResourceList{
803+
apiv1.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), // 8Gi / 4 = 2 cores
804+
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI),
805+
},
806+
},
807+
{
808+
name: "not enough RAM, should increase RAM",
809+
input: apiv1.ResourceList{
810+
apiv1.ResourceCPU: *resource.NewMilliQuantity(4000, resource.DecimalSI), // 4 cores
811+
apiv1.ResourceMemory: *resource.NewQuantity(8*1024*1024*1024, resource.BinarySI), // 8Gi
812+
},
813+
ratio: float64Ptr(4.0),
814+
expected: apiv1.ResourceList{
815+
apiv1.ResourceCPU: *resource.NewMilliQuantity(4000, resource.DecimalSI),
816+
apiv1.ResourceMemory: *resource.NewQuantity(16*1024*1024*1024, resource.BinarySI), // 4 cores * 4 = 16Gi
817+
},
818+
},
819+
{
820+
name: "missing memory, no-op",
821+
input: apiv1.ResourceList{
822+
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
823+
},
824+
ratio: float64Ptr(4.0),
825+
expected: apiv1.ResourceList{
826+
apiv1.ResourceCPU: *resource.NewMilliQuantity(1000, resource.DecimalSI),
827+
},
828+
},
829+
{
830+
name: "zero values, no-op",
831+
input: apiv1.ResourceList{
832+
apiv1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI),
833+
apiv1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
834+
},
835+
ratio: float64Ptr(4.0),
836+
expected: apiv1.ResourceList{
837+
apiv1.ResourceCPU: *resource.NewMilliQuantity(0, resource.DecimalSI),
838+
apiv1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
839+
},
840+
},
841+
}
842+
843+
for _, tc := range tc {
844+
t.Run(tc.name, func(t *testing.T) {
845+
result := EnforceCPUMemoryRatio(tc.input.DeepCopy(), tc.ratio)
846+
assert.Equal(t, tc.expected[apiv1.ResourceCPU], result[apiv1.ResourceCPU])
847+
assert.Equal(t, tc.expected[apiv1.ResourceMemory], result[apiv1.ResourceMemory])
848+
})
849+
}
850+
}
851+
852+
func float64Ptr(v float64) *float64 {
853+
return &v
854+
}

0 commit comments

Comments
 (0)