Skip to content
This repository was archived by the owner on Sep 17, 2025. It is now read-only.

Commit 6868eea

Browse files
authored
Implemented average utilization target type for ResourceMetrics (#8)
* Implemented average utilization target type for ResourceMetrics * Fixed stabilization window duration for the scale proposals
1 parent edbc6c0 commit 6868eea

File tree

7 files changed

+263
-44
lines changed

7 files changed

+263
-44
lines changed

.vscode/launch.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"name": "Launch test file",
9+
"type": "go",
10+
"request": "launch",
11+
"mode": "test",
12+
"program": "${file}",
13+
"args": [
14+
"-test.v",
15+
"-ginkgo.v"
16+
]
17+
},
18+
]
19+
}

api/v1alpha1/metric_target.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
3+
Copyright 2020 Adobe
4+
All Rights Reserved.
5+
6+
NOTICE: Adobe permits you to use, modify, and distribute this file in
7+
accordance with the terms of the Adobe license agreement accompanying
8+
it. If you have received this file from a source other than Adobe,
9+
then your use, modification, or distribution of it requires the prior
10+
written permission of Adobe.
11+
12+
*/
13+
14+
package v1alpha1
15+
16+
import (
17+
"fmt"
18+
)
19+
20+
func (sm *ScaleMetric) GetMetricTarget() (*MetricTarget, error) {
21+
switch sm.Type {
22+
case PrometheusScaleMetricType:
23+
return &sm.Prometheus.Target, nil
24+
case ResourceScaleMetricType:
25+
return &sm.Resource.Target, nil
26+
default:
27+
return nil, fmt.Errorf("unknown metric type %s", sm.Type)
28+
}
29+
}

examples/resource-scaler.yaml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ data:
1919
resource:
2020
name: cpu
2121
target:
22-
type: AverageValue # AverageValue/Utilization/Value
22+
type: AverageValue
2323
averageValue: 10
2424
- type: Resource
2525
resource:
2626
name: memory
2727
container: nginx
2828
target:
29-
type: AverageValue # AverageValue/Utilization/Value
30-
averageValue: 100
31-
# averageUtilization
32-
# value
29+
type: Utilization
30+
averageUalue: 50
31+

replicas/replicas_calculator.go

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ written permission of Adobe.
1414
package replicas
1515

1616
import (
17-
"errors"
1817
"fmt"
1918
"math"
2019

2120
"github.com/adobe/kratos/api/v1alpha1"
2221
"github.com/adobe/kratos/metrics"
22+
corev1 "k8s.io/api/core/v1"
2323
)
2424

2525
type ReplicaCalculator struct {
@@ -32,17 +32,71 @@ func NewReplicaCalculator(tolerance float64) *ReplicaCalculator {
3232
}
3333
}
3434

35-
func (rc *ReplicaCalculator) CalculateReplicas(currentReplicas int32, metricTarget v1alpha1.MetricTarget, metricValues []metrics.MetricValue) (int32, error) {
35+
func (rc *ReplicaCalculator) CalculateReplicas(currentReplicas int32, requestedResources map[string]*corev1.ResourceList, scaleMetric v1alpha1.ScaleMetric, metricValues []metrics.MetricValue) (int32, error) {
36+
metricTarget, err := scaleMetric.GetMetricTarget()
37+
if err != nil {
38+
return 0, fmt.Errorf("unknown metric type: %s", scaleMetric.Type)
39+
}
40+
3641
switch metricTarget.Type {
3742
case v1alpha1.ValueMetricType:
3843
return rc.calculateValue(currentReplicas, metricTarget, metricValues)
3944
case v1alpha1.AverageValueMetricType:
4045
return rc.calculateAverageValue(currentReplicas, metricTarget, metricValues)
46+
case v1alpha1.UtilizationMetricType:
47+
return rc.calculateUtilization(currentReplicas, scaleMetric, requestedResources, metricValues)
48+
}
49+
return 0, fmt.Errorf("replica calculator not implemented yet: %s", metricTarget.Type)
50+
}
51+
52+
func (rc *ReplicaCalculator) calculateUtilization(currentReplicas int32, scaleMetric v1alpha1.ScaleMetric, requestedResources map[string]*corev1.ResourceList, metricValues []metrics.MetricValue) (int32, error) {
53+
utilization := int64(0)
54+
for _, metricValue := range metricValues {
55+
utilization = utilization + metricValue.Value
56+
}
57+
58+
totalResources, err := rc.getPodRequestedResource(requestedResources[scaleMetric.Resource.Container], scaleMetric.Resource.Name)
59+
60+
if err != nil {
61+
return currentReplicas, err
4162
}
42-
return 0, errors.New(fmt.Sprintf("Replica calculator not implemented yet: %s", metricTarget.Type))
63+
64+
if totalResources == 0 {
65+
return currentReplicas, fmt.Errorf("no resource requests configured for %s", scaleMetric.Resource.Name)
66+
}
67+
68+
metricTarget, _ := scaleMetric.GetMetricTarget()
69+
usageRatio := (float64(utilization) / totalResources) / (float64(*metricTarget.AverageUtilization) / 100.0)
70+
71+
if math.Abs(1.0-usageRatio) <= rc.tolerance {
72+
// return the current replicas if the change would be too small
73+
return currentReplicas, nil
74+
}
75+
76+
replicaCount := int32(math.Ceil(usageRatio * float64(currentReplicas)))
77+
78+
return replicaCount, nil
79+
80+
}
81+
82+
func (rc *ReplicaCalculator) getPodRequestedResource(resources *corev1.ResourceList, resourceName corev1.ResourceName) (float64, error) {
83+
switch resourceName {
84+
case corev1.ResourceCPU:
85+
return float64(resources.Cpu().Value()), nil
86+
case corev1.ResourceMemory:
87+
return float64(resources.Memory().Value()), nil
88+
case corev1.ResourceStorage:
89+
return float64(resources.Storage().Value()), nil
90+
case corev1.ResourceEphemeralStorage:
91+
return float64(resources.StorageEphemeral().Value()), nil
92+
case corev1.ResourcePods:
93+
return float64(resources.Pods().Value()), nil
94+
}
95+
96+
return 0, fmt.Errorf("unknown resource metric type: %s", resourceName)
4397
}
4498

45-
func (rc *ReplicaCalculator) calculateValue(currentReplicas int32, metricTarget v1alpha1.MetricTarget, metricValues []metrics.MetricValue) (int32, error) {
99+
func (rc *ReplicaCalculator) calculateValue(currentReplicas int32, metricTarget *v1alpha1.MetricTarget, metricValues []metrics.MetricValue) (int32, error) {
46100
utilization := int64(0)
47101
for _, metricValue := range metricValues {
48102
utilization = utilization + metricValue.Value
@@ -59,7 +113,7 @@ func (rc *ReplicaCalculator) calculateValue(currentReplicas int32, metricTarget
59113
return replicaCount, nil
60114
}
61115

62-
func (rc *ReplicaCalculator) calculateAverageValue(currentReplicas int32, metricTarget v1alpha1.MetricTarget, metricValues []metrics.MetricValue) (replicaCount int32, err error) {
116+
func (rc *ReplicaCalculator) calculateAverageValue(currentReplicas int32, metricTarget *v1alpha1.MetricTarget, metricValues []metrics.MetricValue) (replicaCount int32, err error) {
63117
utilization := int64(0)
64118
for _, metricValue := range metricValues {
65119
utilization = utilization + metricValue.Value

replicas/replicas_calculator_test.go

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,126 @@ import (
1818
"github.com/adobe/kratos/metrics"
1919
. "github.com/onsi/ginkgo"
2020
. "github.com/onsi/gomega"
21+
corev1 "k8s.io/api/core/v1"
2122
"k8s.io/apimachinery/pkg/api/resource"
2223
)
2324

25+
var noRequestedResources = map[string]*corev1.ResourceList{
26+
"": {
27+
corev1.ResourceCPU: *resource.NewQuantity(0, resource.DecimalSI),
28+
corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
29+
corev1.ResourceStorage: *resource.NewQuantity(0, resource.BinarySI),
30+
},
31+
"nginx": {
32+
corev1.ResourceCPU: *resource.NewQuantity(0, resource.DecimalSI),
33+
corev1.ResourceMemory: *resource.NewQuantity(0, resource.BinarySI),
34+
corev1.ResourceStorage: *resource.NewQuantity(0, resource.BinarySI),
35+
},
36+
}
37+
var requestedResources = map[string]*corev1.ResourceList{
38+
"": {
39+
corev1.ResourceCPU: *resource.NewQuantity(10, resource.DecimalSI),
40+
corev1.ResourceMemory: *resource.NewQuantity(1000, resource.BinarySI),
41+
corev1.ResourceStorage: *resource.NewQuantity(10, resource.BinarySI),
42+
},
43+
"nginx": {
44+
corev1.ResourceCPU: *resource.NewQuantity(10, resource.DecimalSI),
45+
corev1.ResourceMemory: *resource.NewQuantity(1000, resource.BinarySI),
46+
corev1.ResourceStorage: *resource.NewQuantity(10, resource.BinarySI),
47+
},
48+
}
49+
2450
var _ = Describe("ReplicaCalculator", func() {
2551
It("Not implemented target metric type", func() {
2652
replicaCalculator := NewReplicaCalculator(0.1)
2753

28-
metricTarget := v1alpha1.MetricTarget{
29-
Type: v1alpha1.UtilizationMetricType,
54+
scaleMetric := v1alpha1.ScaleMetric{
55+
Type: v1alpha1.ExternalScaleMetricType,
56+
Prometheus: &v1alpha1.PrometheusMetricSource{
57+
Target: v1alpha1.MetricTarget{
58+
Type: v1alpha1.AverageValueMetricType,
59+
AverageValue: resource.NewMilliQuantity(500, resource.DecimalSI),
60+
},
61+
},
3062
}
3163

32-
_, err := replicaCalculator.CalculateReplicas(1, metricTarget, nil)
64+
_, err := replicaCalculator.CalculateReplicas(1, noRequestedResources, scaleMetric, nil)
3365

3466
Expect(err).ToNot(BeNil(), "not implemented metric target should result in error")
3567
})
3668

37-
It("Target value calculator", func() {
69+
It("Prometheus target value calculator", func() {
3870
replicaCalculator := NewReplicaCalculator(0.1)
3971

40-
quantity, _ := resource.ParseQuantity("5")
41-
metricTarget := v1alpha1.MetricTarget{
42-
Type: v1alpha1.ValueMetricType,
43-
Value: &quantity,
72+
scaleMetric := v1alpha1.ScaleMetric{
73+
Type: v1alpha1.PrometheusScaleMetricType,
74+
Prometheus: &v1alpha1.PrometheusMetricSource{
75+
Target: v1alpha1.MetricTarget{
76+
Type: v1alpha1.AverageValueMetricType,
77+
AverageValue: resource.NewQuantity(5, resource.DecimalSI),
78+
},
79+
},
4480
}
4581

4682
metrics := []metrics.MetricValue{
47-
metrics.MetricValue{
83+
{
4884
Value: 10,
4985
},
5086
}
51-
replicas, err := replicaCalculator.CalculateReplicas(1, metricTarget, metrics)
87+
replicas, err := replicaCalculator.CalculateReplicas(1, noRequestedResources, scaleMetric, metrics)
5288

5389
Expect(err).To(BeNil(), "no errors expected for valid arguments")
54-
Expect(replicas).To(Equal(int32(2)), "wrong NR. of replicas")
90+
Expect(replicas).To(Equal(int32(2)), "wrong number of replicas")
5591
})
5692

93+
It("Resource target - no requests specified", func() {
94+
replicaCalculator := NewReplicaCalculator(0.1)
95+
96+
averageUtilization := int32(50)
97+
scaleMetric := v1alpha1.ScaleMetric{
98+
Type: v1alpha1.ResourceScaleMetricType,
99+
Resource: &v1alpha1.ResourceMetricSource{
100+
Name: corev1.ResourceCPU,
101+
Target: v1alpha1.MetricTarget{
102+
Type: v1alpha1.UtilizationMetricType,
103+
AverageUtilization: &averageUtilization,
104+
},
105+
},
106+
}
107+
108+
metrics := []metrics.MetricValue{
109+
{
110+
Value: 10,
111+
},
112+
}
113+
_, err := replicaCalculator.CalculateReplicas(1, noRequestedResources, scaleMetric, metrics)
114+
115+
Expect(err).ToNot(BeNil(), "should fail proposal for pods without requested resources")
116+
})
117+
118+
It("Resource target - value calculator", func() {
119+
replicaCalculator := NewReplicaCalculator(0.1)
120+
121+
averageUtilization := int32(50)
122+
scaleMetric := v1alpha1.ScaleMetric{
123+
Type: v1alpha1.ResourceScaleMetricType,
124+
Resource: &v1alpha1.ResourceMetricSource{
125+
Name: corev1.ResourceCPU,
126+
Target: v1alpha1.MetricTarget{
127+
Type: v1alpha1.UtilizationMetricType,
128+
AverageUtilization: &averageUtilization,
129+
},
130+
},
131+
}
132+
133+
metrics := []metrics.MetricValue{
134+
{
135+
Value: 10,
136+
},
137+
}
138+
replicas, err := replicaCalculator.CalculateReplicas(1, requestedResources, scaleMetric, metrics)
139+
140+
Expect(err).To(BeNil(), "no errors expected for valid arguments")
141+
Expect(replicas).To(Equal(int32(2)), "wrong number of replicas")
142+
})
57143
})

scale/scale_facade.go

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ package scale
1515
import (
1616
"context"
1717
"errors"
18-
"fmt"
1918
"time"
2019

2120
"github.com/adobe/kratos/api/common"
@@ -188,7 +187,9 @@ func (f *ScaleFacade) calculateMaxScaleReplicas(item *corev1.ConfigMap, currentR
188187

189188
selector, _ := f.scaleTarget.GetSelectorForTarget(item.GetNamespace(), &spec.Target)
190189

191-
f.log.Info("got selector", "selector", selector)
190+
requestedResources, _ := f.scaleTarget.GetRequestedResources(item.GetNamespace(), selector)
191+
192+
f.log.Info("Pods selector and total requested resources", "selector", selector, "requestedResource", requestedResources)
192193

193194
for _, metric := range spec.Metrics {
194195
metricFetcher, err := f.metricsFactory.GetMetricsFetcher(&metric)
@@ -203,18 +204,11 @@ func (f *ScaleFacade) calculateMaxScaleReplicas(item *corev1.ConfigMap, currentR
203204

204205
if err != nil {
205206
log.Error(err, "error on fetching metric", "metric", metric)
206-
f.eventRecorder.Eventf(item, corev1.EventTypeWarning, "MetricFetchError", "error on fetching metric: %v, error: %v", metric, err.Error())
207-
continue
208-
}
209-
210-
metricTarget, err := f.getMetricTarget(metric)
211-
if err != nil {
212-
log.Error(err, err.Error(), "metric", metric)
213-
f.eventRecorder.Eventf(item, corev1.EventTypeWarning, "MetricTypeError", "Unknown metric type: %v", metric)
207+
f.eventRecorder.Eventf(item, corev1.EventTypeWarning, "MetricFetchError", "error on fetching metric type: %s, error: %v", metric.Type, err.Error())
214208
continue
215209
}
216210

217-
replicaProposal, err := f.replicaCalculator.CalculateReplicas(currentReplicas, *metricTarget, metricValues)
211+
replicaProposal, err := f.replicaCalculator.CalculateReplicas(currentReplicas, requestedResources, metric, metricValues)
218212

219213
log.V(1).Info("metric values and replica proposal", "replicas", replicaProposal, "metrics", metricValues)
220214

@@ -239,17 +233,6 @@ func (f *ScaleFacade) calculateMaxScaleReplicas(item *corev1.ConfigMap, currentR
239233
return maxReplicaProposal
240234
}
241235

242-
func (f *ScaleFacade) getMetricTarget(metric v1alpha1.ScaleMetric) (*v1alpha1.MetricTarget, error) {
243-
switch metric.Type {
244-
case v1alpha1.PrometheusScaleMetricType:
245-
return &metric.Prometheus.Target, nil
246-
case v1alpha1.ResourceScaleMetricType:
247-
return &metric.Resource.Target, nil
248-
default:
249-
return nil, errors.New(fmt.Sprintf("Unknown metric type %s \n", metric.Type))
250-
}
251-
}
252-
253236
func (f *ScaleFacade) updateObjects(originalItem *corev1.ConfigMap, spec *v1alpha1.KratosSpec, status *v1alpha1.KratosStatus) error {
254237
log := f.log.WithValues("item", originalItem.GetName())
255238
_, statusAsString, err := f.marshall(spec, status)
@@ -308,7 +291,7 @@ func (f *ScaleFacade) findLongestPolicyWindow(policies []v1alpha1.ScalingPolicy)
308291

309292
func (f *ScaleFacade) expireRecommendations(windowSeconds int32, recommendations []v1alpha1.Recommendation) []v1alpha1.Recommendation {
310293
result := recommendations[:0]
311-
cutOff := time.Now().Add(time.Duration(-windowSeconds))
294+
cutOff := time.Now().Add(time.Duration(-windowSeconds) * time.Second)
312295
for _, recommendation := range recommendations {
313296
if recommendation.Timestamp.Time.After(cutOff) {
314297
result = append(result, recommendation)

0 commit comments

Comments
 (0)