Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 2dd9eda

Browse files
committedMar 21, 2025
Add configurable tolerance logic.
1 parent 11b6e2a commit 2dd9eda

File tree

4 files changed

+377
-44
lines changed

4 files changed

+377
-44
lines changed
 

‎pkg/controller/podautoscaler/horizontal.go

+39-11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
"k8s.io/apimachinery/pkg/runtime/schema"
3838
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3939
"k8s.io/apimachinery/pkg/util/wait"
40+
utilfeature "k8s.io/apiserver/pkg/util/feature"
4041
autoscalinginformers "k8s.io/client-go/informers/autoscaling/v2"
4142
coreinformers "k8s.io/client-go/informers/core/v1"
4243
"k8s.io/client-go/kubernetes/scheme"
@@ -53,6 +54,7 @@ import (
5354
metricsclient "k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
5455
"k8s.io/kubernetes/pkg/controller/podautoscaler/monitor"
5556
"k8s.io/kubernetes/pkg/controller/util/selectors"
57+
"k8s.io/kubernetes/pkg/features"
5658
)
5759

5860
var (
@@ -86,6 +88,7 @@ type HorizontalController struct {
8688
hpaNamespacer autoscalingclient.HorizontalPodAutoscalersGetter
8789
mapper apimeta.RESTMapper
8890

91+
tolerance float64
8992
replicaCalc *ReplicaCalculator
9093
eventRecorder record.EventRecorder
9194

@@ -146,6 +149,7 @@ func NewHorizontalController(
146149
eventRecorder: recorder,
147150
scaleNamespacer: scaleNamespacer,
148151
hpaNamespacer: hpaNamespacer,
152+
tolerance: tolerance,
149153
downscaleStabilisationWindow: downscaleStabilisationWindow,
150154
monitor: monitor.New(),
151155
queue: workqueue.NewTypedRateLimitingQueueWithConfig(
@@ -181,7 +185,6 @@ func NewHorizontalController(
181185
replicaCalc := NewReplicaCalculator(
182186
metricsClient,
183187
hpaController.podLister,
184-
tolerance,
185188
cpuInitializationPeriod,
186189
delayOfInitialReadinessStatus,
187190
)
@@ -539,8 +542,9 @@ func (a *HorizontalController) computeStatusForObjectMetric(specReplicas, status
539542
},
540543
},
541544
}
545+
tolerances := a.tolerancesForHpa(hpa)
542546
if metricSpec.Object.Target.Type == autoscalingv2.ValueMetricType && metricSpec.Object.Target.Value != nil {
543-
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetObjectMetricReplicas(specReplicas, metricSpec.Object.Target.Value.MilliValue(), metricSpec.Object.Metric.Name, hpa.Namespace, &metricSpec.Object.DescribedObject, selector, metricSelector)
547+
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetObjectMetricReplicas(specReplicas, metricSpec.Object.Target.Value.MilliValue(), metricSpec.Object.Metric.Name, tolerances, hpa.Namespace, &metricSpec.Object.DescribedObject, selector, metricSelector)
544548
if err != nil {
545549
condition := a.getUnableComputeReplicaCountCondition(hpa, "FailedGetObjectMetric", err)
546550
return 0, timestampProposal, "", condition, err
@@ -549,7 +553,7 @@ func (a *HorizontalController) computeStatusForObjectMetric(specReplicas, status
549553
*status = metricStatus
550554
return replicaCountProposal, timestampProposal, fmt.Sprintf("%s metric %s", metricSpec.Object.DescribedObject.Kind, metricSpec.Object.Metric.Name), autoscalingv2.HorizontalPodAutoscalerCondition{}, nil
551555
} else if metricSpec.Object.Target.Type == autoscalingv2.AverageValueMetricType && metricSpec.Object.Target.AverageValue != nil {
552-
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetObjectPerPodMetricReplicas(statusReplicas, metricSpec.Object.Target.AverageValue.MilliValue(), metricSpec.Object.Metric.Name, hpa.Namespace, &metricSpec.Object.DescribedObject, metricSelector)
556+
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetObjectPerPodMetricReplicas(statusReplicas, metricSpec.Object.Target.AverageValue.MilliValue(), metricSpec.Object.Metric.Name, tolerances, hpa.Namespace, &metricSpec.Object.DescribedObject, metricSelector)
553557
if err != nil {
554558
condition := a.getUnableComputeReplicaCountCondition(hpa, "FailedGetObjectMetric", err)
555559
return 0, time.Time{}, "", condition, fmt.Errorf("failed to get %s object metric: %v", metricSpec.Object.Metric.Name, err)
@@ -566,7 +570,8 @@ func (a *HorizontalController) computeStatusForObjectMetric(specReplicas, status
566570

567571
// computeStatusForPodsMetric computes the desired number of replicas for the specified metric of type PodsMetricSourceType.
568572
func (a *HorizontalController) computeStatusForPodsMetric(currentReplicas int32, metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler, selector labels.Selector, status *autoscalingv2.MetricStatus, metricSelector labels.Selector) (replicaCountProposal int32, timestampProposal time.Time, metricNameProposal string, condition autoscalingv2.HorizontalPodAutoscalerCondition, err error) {
569-
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetMetricReplicas(currentReplicas, metricSpec.Pods.Target.AverageValue.MilliValue(), metricSpec.Pods.Metric.Name, hpa.Namespace, selector, metricSelector)
573+
tolerances := a.tolerancesForHpa(hpa)
574+
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetMetricReplicas(currentReplicas, metricSpec.Pods.Target.AverageValue.MilliValue(), metricSpec.Pods.Metric.Name, tolerances, hpa.Namespace, selector, metricSelector)
570575
if err != nil {
571576
condition = a.getUnableComputeReplicaCountCondition(hpa, "FailedGetPodsMetric", err)
572577
return 0, timestampProposal, "", condition, err
@@ -588,12 +593,14 @@ func (a *HorizontalController) computeStatusForPodsMetric(currentReplicas int32,
588593
}
589594

590595
func (a *HorizontalController) computeStatusForResourceMetricGeneric(ctx context.Context, currentReplicas int32, target autoscalingv2.MetricTarget,
591-
resourceName v1.ResourceName, namespace string, container string, selector labels.Selector, sourceType autoscalingv2.MetricSourceType) (replicaCountProposal int32,
596+
resourceName v1.ResourceName, hpa *autoscalingv2.HorizontalPodAutoscaler, container string, selector labels.Selector, sourceType autoscalingv2.MetricSourceType) (replicaCountProposal int32,
592597
metricStatus *autoscalingv2.MetricValueStatus, timestampProposal time.Time, metricNameProposal string,
593598
condition autoscalingv2.HorizontalPodAutoscalerCondition, err error) {
599+
namespace := hpa.Namespace
600+
tolerances := a.tolerancesForHpa(hpa)
594601
if target.AverageValue != nil {
595602
var rawProposal int64
596-
replicaCountProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetRawResourceReplicas(ctx, currentReplicas, target.AverageValue.MilliValue(), resourceName, namespace, selector, container)
603+
replicaCountProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetRawResourceReplicas(ctx, currentReplicas, target.AverageValue.MilliValue(), resourceName, tolerances, namespace, selector, container)
597604
if err != nil {
598605
return 0, nil, time.Time{}, "", condition, fmt.Errorf("failed to get %s usage: %v", resourceName, err)
599606
}
@@ -610,7 +617,7 @@ func (a *HorizontalController) computeStatusForResourceMetricGeneric(ctx context
610617
}
611618

612619
targetUtilization := *target.AverageUtilization
613-
replicaCountProposal, percentageProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetResourceReplicas(ctx, currentReplicas, targetUtilization, resourceName, namespace, selector, container)
620+
replicaCountProposal, percentageProposal, rawProposal, timestampProposal, err := a.replicaCalc.GetResourceReplicas(ctx, currentReplicas, targetUtilization, resourceName, tolerances, namespace, selector, container)
614621
if err != nil {
615622
return 0, nil, time.Time{}, "", condition, fmt.Errorf("failed to get %s utilization: %v", resourceName, err)
616623
}
@@ -630,7 +637,7 @@ func (a *HorizontalController) computeStatusForResourceMetricGeneric(ctx context
630637
func (a *HorizontalController) computeStatusForResourceMetric(ctx context.Context, currentReplicas int32, metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler,
631638
selector labels.Selector, status *autoscalingv2.MetricStatus) (replicaCountProposal int32, timestampProposal time.Time,
632639
metricNameProposal string, condition autoscalingv2.HorizontalPodAutoscalerCondition, err error) {
633-
replicaCountProposal, metricValueStatus, timestampProposal, metricNameProposal, condition, err := a.computeStatusForResourceMetricGeneric(ctx, currentReplicas, metricSpec.Resource.Target, metricSpec.Resource.Name, hpa.Namespace, "", selector, autoscalingv2.ResourceMetricSourceType)
640+
replicaCountProposal, metricValueStatus, timestampProposal, metricNameProposal, condition, err := a.computeStatusForResourceMetricGeneric(ctx, currentReplicas, metricSpec.Resource.Target, metricSpec.Resource.Name, hpa, "", selector, autoscalingv2.ResourceMetricSourceType)
634641
if err != nil {
635642
condition = a.getUnableComputeReplicaCountCondition(hpa, "FailedGetResourceMetric", err)
636643
return replicaCountProposal, timestampProposal, metricNameProposal, condition, err
@@ -649,7 +656,7 @@ func (a *HorizontalController) computeStatusForResourceMetric(ctx context.Contex
649656
func (a *HorizontalController) computeStatusForContainerResourceMetric(ctx context.Context, currentReplicas int32, metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler,
650657
selector labels.Selector, status *autoscalingv2.MetricStatus) (replicaCountProposal int32, timestampProposal time.Time,
651658
metricNameProposal string, condition autoscalingv2.HorizontalPodAutoscalerCondition, err error) {
652-
replicaCountProposal, metricValueStatus, timestampProposal, metricNameProposal, condition, err := a.computeStatusForResourceMetricGeneric(ctx, currentReplicas, metricSpec.ContainerResource.Target, metricSpec.ContainerResource.Name, hpa.Namespace, metricSpec.ContainerResource.Container, selector, autoscalingv2.ContainerResourceMetricSourceType)
659+
replicaCountProposal, metricValueStatus, timestampProposal, metricNameProposal, condition, err := a.computeStatusForResourceMetricGeneric(ctx, currentReplicas, metricSpec.ContainerResource.Target, metricSpec.ContainerResource.Name, hpa, metricSpec.ContainerResource.Container, selector, autoscalingv2.ContainerResourceMetricSourceType)
653660
if err != nil {
654661
condition = a.getUnableComputeReplicaCountCondition(hpa, "FailedGetContainerResourceMetric", err)
655662
return replicaCountProposal, timestampProposal, metricNameProposal, condition, err
@@ -667,8 +674,9 @@ func (a *HorizontalController) computeStatusForContainerResourceMetric(ctx conte
667674

668675
// computeStatusForExternalMetric computes the desired number of replicas for the specified metric of type ExternalMetricSourceType.
669676
func (a *HorizontalController) computeStatusForExternalMetric(specReplicas, statusReplicas int32, metricSpec autoscalingv2.MetricSpec, hpa *autoscalingv2.HorizontalPodAutoscaler, selector labels.Selector, status *autoscalingv2.MetricStatus) (replicaCountProposal int32, timestampProposal time.Time, metricNameProposal string, condition autoscalingv2.HorizontalPodAutoscalerCondition, err error) {
677+
tolerances := a.tolerancesForHpa(hpa)
670678
if metricSpec.External.Target.AverageValue != nil {
671-
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetExternalPerPodMetricReplicas(statusReplicas, metricSpec.External.Target.AverageValue.MilliValue(), metricSpec.External.Metric.Name, hpa.Namespace, metricSpec.External.Metric.Selector)
679+
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetExternalPerPodMetricReplicas(statusReplicas, metricSpec.External.Target.AverageValue.MilliValue(), metricSpec.External.Metric.Name, tolerances, hpa.Namespace, metricSpec.External.Metric.Selector)
672680
if err != nil {
673681
condition = a.getUnableComputeReplicaCountCondition(hpa, "FailedGetExternalMetric", err)
674682
return 0, time.Time{}, "", condition, fmt.Errorf("failed to get %s external metric: %v", metricSpec.External.Metric.Name, err)
@@ -688,7 +696,7 @@ func (a *HorizontalController) computeStatusForExternalMetric(specReplicas, stat
688696
return replicaCountProposal, timestampProposal, fmt.Sprintf("external metric %s(%+v)", metricSpec.External.Metric.Name, metricSpec.External.Metric.Selector), autoscalingv2.HorizontalPodAutoscalerCondition{}, nil
689697
}
690698
if metricSpec.External.Target.Value != nil {
691-
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetExternalMetricReplicas(specReplicas, metricSpec.External.Target.Value.MilliValue(), metricSpec.External.Metric.Name, hpa.Namespace, metricSpec.External.Metric.Selector, selector)
699+
replicaCountProposal, usageProposal, timestampProposal, err := a.replicaCalc.GetExternalMetricReplicas(specReplicas, metricSpec.External.Target.Value.MilliValue(), metricSpec.External.Metric.Name, tolerances, hpa.Namespace, metricSpec.External.Metric.Selector, selector)
692700
if err != nil {
693701
condition = a.getUnableComputeReplicaCountCondition(hpa, "FailedGetExternalMetric", err)
694702
return 0, time.Time{}, "", condition, fmt.Errorf("failed to get external metric %s: %v", metricSpec.External.Metric.Name, err)
@@ -835,6 +843,7 @@ func (a *HorizontalController) reconcileAutoscaler(ctx context.Context, hpaShare
835843
logger.V(4).Info("Proposing desired replicas",
836844
"desiredReplicas", metricDesiredReplicas,
837845
"metric", metricName,
846+
"tolerances", a.tolerancesForHpa(hpa),
838847
"timestamp", metricTimestamp,
839848
"scaleTarget", reference)
840849

@@ -1384,6 +1393,25 @@ func (a *HorizontalController) updateStatus(ctx context.Context, hpa *autoscalin
13841393
return nil
13851394
}
13861395

1396+
// tolerancesForHpa returns the metrics usage ratio tolerances for a given HPA.
1397+
// It ignores configurable tolerances set in the HPA spec.behavior field if the
1398+
// HPAConfigurableTolerance feature gate is disabled.
1399+
func (a *HorizontalController) tolerancesForHpa(hpa *autoscalingv2.HorizontalPodAutoscaler) Tolerances {
1400+
t := Tolerances{a.tolerance, a.tolerance}
1401+
behavior := hpa.Spec.Behavior
1402+
allowConfigurableTolerances := utilfeature.DefaultFeatureGate.Enabled(features.HPAConfigurableTolerance)
1403+
if behavior == nil || !allowConfigurableTolerances {
1404+
return t
1405+
}
1406+
if behavior.ScaleDown != nil && behavior.ScaleDown.Tolerance != nil {
1407+
t.scaleDown = behavior.ScaleDown.Tolerance.AsApproximateFloat64()
1408+
}
1409+
if behavior.ScaleUp != nil && behavior.ScaleUp.Tolerance != nil {
1410+
t.scaleUp = behavior.ScaleUp.Tolerance.AsApproximateFloat64()
1411+
}
1412+
return t
1413+
}
1414+
13871415
// setCondition sets the specific condition type on the given HPA to the specified value with the given reason
13881416
// and message. The message and args are treated like a format string. The condition will be added if it is
13891417
// not present.

‎pkg/controller/podautoscaler/horizontal_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,19 @@ import (
3737
"k8s.io/apimachinery/pkg/runtime/schema"
3838
"k8s.io/apimachinery/pkg/util/wait"
3939
"k8s.io/apimachinery/pkg/watch"
40+
utilfeature "k8s.io/apiserver/pkg/util/feature"
4041
"k8s.io/client-go/informers"
4142
"k8s.io/client-go/kubernetes/fake"
4243
scalefake "k8s.io/client-go/scale/fake"
4344
core "k8s.io/client-go/testing"
45+
featuregatetesting "k8s.io/component-base/featuregate/testing"
4446
"k8s.io/kubernetes/pkg/api/legacyscheme"
4547
autoscalingapiv2 "k8s.io/kubernetes/pkg/apis/autoscaling/v2"
4648
"k8s.io/kubernetes/pkg/controller"
4749
"k8s.io/kubernetes/pkg/controller/podautoscaler/metrics"
4850
"k8s.io/kubernetes/pkg/controller/podautoscaler/monitor"
4951
"k8s.io/kubernetes/pkg/controller/util/selectors"
52+
"k8s.io/kubernetes/pkg/features"
5053
"k8s.io/kubernetes/test/utils/ktesting"
5154
cmapi "k8s.io/metrics/pkg/apis/custom_metrics/v1beta2"
5255
emapi "k8s.io/metrics/pkg/apis/external_metrics/v1beta1"
@@ -2216,6 +2219,107 @@ func TestTolerance(t *testing.T) {
22162219
tc.runTest(t)
22172220
}
22182221

2222+
func TestConfigurableTolerance(t *testing.T) {
2223+
onePercentQuantity := resource.MustParse("0.01")
2224+
ninetyPercentQuantity := resource.MustParse("0.9")
2225+
2226+
testCases := []struct {
2227+
name string
2228+
configurableToleranceGate bool
2229+
replicas int32
2230+
scaleUpRules *autoscalingv2.HPAScalingRules
2231+
scaleDownRules *autoscalingv2.HPAScalingRules
2232+
reportedLevels []uint64
2233+
reportedCPURequests []resource.Quantity
2234+
expectedDesiredReplicas int32
2235+
expectedConditionReason string
2236+
expectedActionLabel monitor.ActionLabel
2237+
}{
2238+
{
2239+
name: "Scaling up because of a 1% configurable tolerance",
2240+
configurableToleranceGate: true,
2241+
replicas: 3,
2242+
scaleUpRules: &autoscalingv2.HPAScalingRules{
2243+
Tolerance: &onePercentQuantity,
2244+
},
2245+
reportedLevels: []uint64{1010, 1030, 1020},
2246+
reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
2247+
expectedDesiredReplicas: 4,
2248+
expectedConditionReason: "SucceededRescale",
2249+
expectedActionLabel: monitor.ActionLabelScaleUp,
2250+
},
2251+
{
2252+
name: "No scale-down because of a 90% configurable tolerance",
2253+
configurableToleranceGate: true,
2254+
replicas: 3,
2255+
scaleDownRules: &autoscalingv2.HPAScalingRules{
2256+
Tolerance: &ninetyPercentQuantity,
2257+
},
2258+
reportedLevels: []uint64{300, 300, 300},
2259+
reportedCPURequests: []resource.Quantity{resource.MustParse("1.0"), resource.MustParse("1.0"), resource.MustParse("1.0")},
2260+
expectedDesiredReplicas: 3,
2261+
expectedConditionReason: "ReadyForNewScale",
2262+
expectedActionLabel: monitor.ActionLabelNone,
2263+
},
2264+
{
2265+
name: "No scaling because of the large default tolerance",
2266+
configurableToleranceGate: true,
2267+
replicas: 3,
2268+
reportedLevels: []uint64{1010, 1030, 1020},
2269+
reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
2270+
expectedDesiredReplicas: 3,
2271+
expectedConditionReason: "ReadyForNewScale",
2272+
expectedActionLabel: monitor.ActionLabelNone,
2273+
},
2274+
{
2275+
name: "No scaling because the configurable tolerance is ignored as the feature gate is disabled",
2276+
configurableToleranceGate: false,
2277+
replicas: 3,
2278+
scaleUpRules: &autoscalingv2.HPAScalingRules{
2279+
Tolerance: &onePercentQuantity,
2280+
},
2281+
reportedLevels: []uint64{1010, 1030, 1020},
2282+
reportedCPURequests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
2283+
expectedDesiredReplicas: 3,
2284+
expectedConditionReason: "ReadyForNewScale",
2285+
expectedActionLabel: monitor.ActionLabelNone,
2286+
},
2287+
}
2288+
2289+
for _, tc := range testCases {
2290+
t.Run(tc.name, func(t *testing.T) {
2291+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, tc.configurableToleranceGate)
2292+
tc := testCase{
2293+
minReplicas: 1,
2294+
maxReplicas: 5,
2295+
specReplicas: tc.replicas,
2296+
statusReplicas: tc.replicas,
2297+
scaleDownRules: tc.scaleDownRules,
2298+
scaleUpRules: tc.scaleUpRules,
2299+
expectedDesiredReplicas: tc.expectedDesiredReplicas,
2300+
CPUTarget: 100,
2301+
reportedLevels: tc.reportedLevels,
2302+
reportedCPURequests: tc.reportedCPURequests,
2303+
useMetricsAPI: true,
2304+
expectedConditions: statusOkWithOverrides(autoscalingv2.HorizontalPodAutoscalerCondition{
2305+
Type: autoscalingv2.AbleToScale,
2306+
Status: v1.ConditionTrue,
2307+
Reason: tc.expectedConditionReason,
2308+
}),
2309+
expectedReportedReconciliationActionLabel: tc.expectedActionLabel,
2310+
expectedReportedReconciliationErrorLabel: monitor.ErrorLabelNone,
2311+
expectedReportedMetricComputationActionLabels: map[autoscalingv2.MetricSourceType]monitor.ActionLabel{
2312+
autoscalingv2.ResourceMetricSourceType: tc.expectedActionLabel,
2313+
},
2314+
expectedReportedMetricComputationErrorLabels: map[autoscalingv2.MetricSourceType]monitor.ErrorLabel{
2315+
autoscalingv2.ResourceMetricSourceType: monitor.ErrorLabelNone,
2316+
},
2317+
}
2318+
tc.runTest(t)
2319+
})
2320+
}
2321+
}
2322+
22192323
func TestToleranceCM(t *testing.T) {
22202324
averageValue := resource.MustParse("20.0")
22212325
tc := testCase{

‎pkg/controller/podautoscaler/replica_calculator.go

+35-23
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,41 @@ const (
4040
defaultTestingDelayOfInitialReadinessStatus = 10 * time.Second
4141
)
4242

43+
// Tolerances contains metric usage ratio scale-up and scale-down tolerances.
44+
type Tolerances struct {
45+
scaleDown float64
46+
scaleUp float64
47+
}
48+
49+
func (t Tolerances) String() string {
50+
return fmt.Sprintf("[down:%.1f%%, up:%.1f%%]", t.scaleDown*100., t.scaleUp*100.)
51+
}
52+
53+
func (t Tolerances) isWithin(usageRatio float64) bool {
54+
return (1.0-t.scaleDown) <= usageRatio && usageRatio <= (1.0+t.scaleUp)
55+
}
56+
4357
// ReplicaCalculator bundles all needed information to calculate the target amount of replicas
4458
type ReplicaCalculator struct {
4559
metricsClient metricsclient.MetricsClient
4660
podLister corelisters.PodLister
47-
tolerance float64
4861
cpuInitializationPeriod time.Duration
4962
delayOfInitialReadinessStatus time.Duration
5063
}
5164

5265
// NewReplicaCalculator creates a new ReplicaCalculator and passes all necessary information to the new instance
53-
func NewReplicaCalculator(metricsClient metricsclient.MetricsClient, podLister corelisters.PodLister, tolerance float64, cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration) *ReplicaCalculator {
66+
func NewReplicaCalculator(metricsClient metricsclient.MetricsClient, podLister corelisters.PodLister, cpuInitializationPeriod, delayOfInitialReadinessStatus time.Duration) *ReplicaCalculator {
5467
return &ReplicaCalculator{
5568
metricsClient: metricsClient,
5669
podLister: podLister,
57-
tolerance: tolerance,
5870
cpuInitializationPeriod: cpuInitializationPeriod,
5971
delayOfInitialReadinessStatus: delayOfInitialReadinessStatus,
6072
}
6173
}
6274

6375
// GetResourceReplicas calculates the desired replica count based on a target resource utilization percentage
6476
// of the given resource for pods matching the given selector in the given namespace, and the current replica count
65-
func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentReplicas int32, targetUtilization int32, resource v1.ResourceName, namespace string, selector labels.Selector, container string) (replicaCount int32, utilization int32, rawUtilization int64, timestamp time.Time, err error) {
77+
func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentReplicas int32, targetUtilization int32, resource v1.ResourceName, tolerances Tolerances, namespace string, selector labels.Selector, container string) (replicaCount int32, utilization int32, rawUtilization int64, timestamp time.Time, err error) {
6678
metrics, timestamp, err := c.metricsClient.GetResourceMetric(ctx, resource, namespace, selector, container)
6779
if err != nil {
6880
return 0, 0, 0, time.Time{}, fmt.Errorf("unable to get metrics for resource %s: %v", resource, err)
@@ -94,7 +106,7 @@ func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentRepl
94106

95107
scaleUpWithUnready := len(unreadyPods) > 0 && usageRatio > 1.0
96108
if !scaleUpWithUnready && len(missingPods) == 0 {
97-
if math.Abs(1.0-usageRatio) <= c.tolerance {
109+
if tolerances.isWithin(usageRatio) {
98110
// return the current replicas if the change would be too small
99111
return currentReplicas, utilization, rawUtilization, timestamp, nil
100112
}
@@ -132,7 +144,7 @@ func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentRepl
132144
return 0, utilization, rawUtilization, time.Time{}, err
133145
}
134146

135-
if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
147+
if tolerances.isWithin(newUsageRatio) || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
136148
// return the current replicas if the change would be too small,
137149
// or if the new usage ratio would cause a change in scale direction
138150
return currentReplicas, utilization, rawUtilization, timestamp, nil
@@ -151,31 +163,31 @@ func (c *ReplicaCalculator) GetResourceReplicas(ctx context.Context, currentRepl
151163

152164
// GetRawResourceReplicas calculates the desired replica count based on a target resource usage (as a raw milli-value)
153165
// for pods matching the given selector in the given namespace, and the current replica count
154-
func (c *ReplicaCalculator) GetRawResourceReplicas(ctx context.Context, currentReplicas int32, targetUsage int64, resource v1.ResourceName, namespace string, selector labels.Selector, container string) (replicaCount int32, usage int64, timestamp time.Time, err error) {
166+
func (c *ReplicaCalculator) GetRawResourceReplicas(ctx context.Context, currentReplicas int32, targetUsage int64, resource v1.ResourceName, tolerances Tolerances, namespace string, selector labels.Selector, container string) (replicaCount int32, usage int64, timestamp time.Time, err error) {
155167
metrics, timestamp, err := c.metricsClient.GetResourceMetric(ctx, resource, namespace, selector, container)
156168
if err != nil {
157169
return 0, 0, time.Time{}, fmt.Errorf("unable to get metrics for resource %s: %v", resource, err)
158170
}
159171

160-
replicaCount, usage, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUsage, namespace, selector, resource)
172+
replicaCount, usage, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUsage, tolerances, namespace, selector, resource)
161173
return replicaCount, usage, timestamp, err
162174
}
163175

164176
// GetMetricReplicas calculates the desired replica count based on a target metric usage
165177
// (as a milli-value) for pods matching the given selector in the given namespace, and the
166178
// current replica count
167-
func (c *ReplicaCalculator) GetMetricReplicas(currentReplicas int32, targetUsage int64, metricName string, namespace string, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
179+
func (c *ReplicaCalculator) GetMetricReplicas(currentReplicas int32, targetUsage int64, metricName string, tolerances Tolerances, namespace string, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
168180
metrics, timestamp, err := c.metricsClient.GetRawMetric(metricName, namespace, selector, metricSelector)
169181
if err != nil {
170182
return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v", metricName, err)
171183
}
172184

173-
replicaCount, usage, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUsage, namespace, selector, v1.ResourceName(""))
185+
replicaCount, usage, err = c.calcPlainMetricReplicas(metrics, currentReplicas, targetUsage, tolerances, namespace, selector, v1.ResourceName(""))
174186
return replicaCount, usage, timestamp, err
175187
}
176188

177189
// calcPlainMetricReplicas calculates the desired replicas for plain (i.e. non-utilization percentage) metrics.
178-
func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMetricsInfo, currentReplicas int32, targetUsage int64, namespace string, selector labels.Selector, resource v1.ResourceName) (replicaCount int32, usage int64, err error) {
190+
func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMetricsInfo, currentReplicas int32, targetUsage int64, tolerances Tolerances, namespace string, selector labels.Selector, resource v1.ResourceName) (replicaCount int32, usage int64, err error) {
179191

180192
podList, err := c.podLister.Pods(namespace).List(selector)
181193
if err != nil {
@@ -199,7 +211,7 @@ func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMet
199211
scaleUpWithUnready := len(unreadyPods) > 0 && usageRatio > 1.0
200212

201213
if !scaleUpWithUnready && len(missingPods) == 0 {
202-
if math.Abs(1.0-usageRatio) <= c.tolerance {
214+
if tolerances.isWithin(usageRatio) {
203215
// return the current replicas if the change would be too small
204216
return currentReplicas, usage, nil
205217
}
@@ -232,7 +244,7 @@ func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMet
232244
// re-run the usage calculation with our new numbers
233245
newUsageRatio, _ := metricsclient.GetMetricUsageRatio(metrics, targetUsage)
234246

235-
if math.Abs(1.0-newUsageRatio) <= c.tolerance || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
247+
if tolerances.isWithin(newUsageRatio) || (usageRatio < 1.0 && newUsageRatio > 1.0) || (usageRatio > 1.0 && newUsageRatio < 1.0) {
236248
// return the current replicas if the change would be too small,
237249
// or if the new usage ratio would cause a change in scale direction
238250
return currentReplicas, usage, nil
@@ -251,22 +263,22 @@ func (c *ReplicaCalculator) calcPlainMetricReplicas(metrics metricsclient.PodMet
251263

252264
// GetObjectMetricReplicas calculates the desired replica count based on a target metric usage (as a milli-value)
253265
// for the given object in the given namespace, and the current replica count.
254-
func (c *ReplicaCalculator) GetObjectMetricReplicas(currentReplicas int32, targetUsage int64, metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
266+
func (c *ReplicaCalculator) GetObjectMetricReplicas(currentReplicas int32, targetUsage int64, metricName string, tolerances Tolerances, namespace string, objectRef *autoscaling.CrossVersionObjectReference, selector labels.Selector, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
255267
usage, _, err = c.metricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector)
256268
if err != nil {
257269
return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v on %s %s/%s", metricName, objectRef.Kind, namespace, objectRef.Name, err)
258270
}
259271

260272
usageRatio := float64(usage) / float64(targetUsage)
261-
replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, namespace, selector)
273+
replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, tolerances, namespace, selector)
262274
return replicaCount, usage, timestamp, err
263275
}
264276

265277
// getUsageRatioReplicaCount calculates the desired replica count based on usageRatio and ready pods count.
266278
// For currentReplicas=0 doesn't take into account ready pods count and tolerance to support scaling to zero pods.
267-
func (c *ReplicaCalculator) getUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, namespace string, selector labels.Selector) (replicaCount int32, timestamp time.Time, err error) {
279+
func (c *ReplicaCalculator) getUsageRatioReplicaCount(currentReplicas int32, usageRatio float64, tolerances Tolerances, namespace string, selector labels.Selector) (replicaCount int32, timestamp time.Time, err error) {
268280
if currentReplicas != 0 {
269-
if math.Abs(1.0-usageRatio) <= c.tolerance {
281+
if tolerances.isWithin(usageRatio) {
270282
// return the current replicas if the change would be too small
271283
return currentReplicas, timestamp, nil
272284
}
@@ -286,15 +298,15 @@ func (c *ReplicaCalculator) getUsageRatioReplicaCount(currentReplicas int32, usa
286298

287299
// GetObjectPerPodMetricReplicas calculates the desired replica count based on a target metric usage (as a milli-value)
288300
// for the given object in the given namespace, and the current replica count.
289-
func (c *ReplicaCalculator) GetObjectPerPodMetricReplicas(statusReplicas int32, targetAverageUsage int64, metricName string, namespace string, objectRef *autoscaling.CrossVersionObjectReference, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
301+
func (c *ReplicaCalculator) GetObjectPerPodMetricReplicas(statusReplicas int32, targetAverageUsage int64, metricName string, tolerances Tolerances, namespace string, objectRef *autoscaling.CrossVersionObjectReference, metricSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
290302
usage, timestamp, err = c.metricsClient.GetObjectMetric(metricName, namespace, objectRef, metricSelector)
291303
if err != nil {
292304
return 0, 0, time.Time{}, fmt.Errorf("unable to get metric %s: %v on %s %s/%s", metricName, objectRef.Kind, namespace, objectRef.Name, err)
293305
}
294306

295307
replicaCount = statusReplicas
296308
usageRatio := float64(usage) / (float64(targetAverageUsage) * float64(replicaCount))
297-
if math.Abs(1.0-usageRatio) > c.tolerance {
309+
if !tolerances.isWithin(usageRatio) {
298310
// update number of replicas if change is large enough
299311
replicaCount = int32(math.Ceil(float64(usage) / float64(targetAverageUsage)))
300312
}
@@ -329,7 +341,7 @@ func (c *ReplicaCalculator) getReadyPodsCount(namespace string, selector labels.
329341
// GetExternalMetricReplicas calculates the desired replica count based on a
330342
// target metric value (as a milli-value) for the external metric in the given
331343
// namespace, and the current replica count.
332-
func (c *ReplicaCalculator) GetExternalMetricReplicas(currentReplicas int32, targetUsage int64, metricName, namespace string, metricSelector *metav1.LabelSelector, podSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
344+
func (c *ReplicaCalculator) GetExternalMetricReplicas(currentReplicas int32, targetUsage int64, metricName string, tolerances Tolerances, namespace string, metricSelector *metav1.LabelSelector, podSelector labels.Selector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
333345
metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector)
334346
if err != nil {
335347
return 0, 0, time.Time{}, err
@@ -344,14 +356,14 @@ func (c *ReplicaCalculator) GetExternalMetricReplicas(currentReplicas int32, tar
344356
}
345357

346358
usageRatio := float64(usage) / float64(targetUsage)
347-
replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, namespace, podSelector)
359+
replicaCount, timestamp, err = c.getUsageRatioReplicaCount(currentReplicas, usageRatio, tolerances, namespace, podSelector)
348360
return replicaCount, usage, timestamp, err
349361
}
350362

351363
// GetExternalPerPodMetricReplicas calculates the desired replica count based on a
352364
// target metric value per pod (as a milli-value) for the external metric in the
353365
// given namespace, and the current replica count.
354-
func (c *ReplicaCalculator) GetExternalPerPodMetricReplicas(statusReplicas int32, targetUsagePerPod int64, metricName, namespace string, metricSelector *metav1.LabelSelector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
366+
func (c *ReplicaCalculator) GetExternalPerPodMetricReplicas(statusReplicas int32, targetUsagePerPod int64, metricName string, tolerances Tolerances, namespace string, metricSelector *metav1.LabelSelector) (replicaCount int32, usage int64, timestamp time.Time, err error) {
355367
metricLabelSelector, err := metav1.LabelSelectorAsSelector(metricSelector)
356368
if err != nil {
357369
return 0, 0, time.Time{}, err
@@ -367,7 +379,7 @@ func (c *ReplicaCalculator) GetExternalPerPodMetricReplicas(statusReplicas int32
367379

368380
replicaCount = statusReplicas
369381
usageRatio := float64(usage) / (float64(targetUsagePerPod) * float64(replicaCount))
370-
if math.Abs(1.0-usageRatio) > c.tolerance {
382+
if !tolerances.isWithin(usageRatio) {
371383
// update number of replicas if the change is large enough
372384
replicaCount = int32(math.Ceil(float64(usage) / float64(targetUsagePerPod)))
373385
}

‎pkg/controller/podautoscaler/replica_calculator_test.go

+199-10
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,10 @@ type replicaCalcTestCase struct {
9090

9191
timestamp time.Time
9292

93-
resource *resourceInfo
94-
metric *metricInfo
95-
container string
93+
tolerances *Tolerances
94+
resource *resourceInfo
95+
metric *metricInfo
96+
container string
9697

9798
podReadiness []v1.ConditionStatus
9899
podStartTime []metav1.Time
@@ -343,7 +344,7 @@ func (tc *replicaCalcTestCase) runTest(t *testing.T) {
343344
informerFactory := informers.NewSharedInformerFactory(testClient, controller.NoResyncPeriodFunc())
344345
informer := informerFactory.Core().V1().Pods()
345346

346-
replicaCalc := NewReplicaCalculator(metricsClient, informer.Lister(), defaultTestingTolerance, defaultTestingCPUInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
347+
replicaCalc := NewReplicaCalculator(metricsClient, informer.Lister(), defaultTestingCPUInitializationPeriod, defaultTestingDelayOfInitialReadinessStatus)
347348

348349
stop := make(chan struct{})
349350
defer close(stop)
@@ -357,8 +358,14 @@ func (tc *replicaCalcTestCase) runTest(t *testing.T) {
357358
})
358359
require.NoError(t, err, "something went horribly wrong...")
359360

361+
// Use default if tolerances are not specified in the test case.
362+
tolerances := Tolerances{defaultTestingTolerance, defaultTestingTolerance}
363+
if tc.tolerances != nil {
364+
tolerances = *tc.tolerances
365+
}
366+
360367
if tc.resource != nil {
361-
outReplicas, outUtilization, outRawValue, outTimestamp, err := replicaCalc.GetResourceReplicas(context.TODO(), tc.currentReplicas, tc.resource.targetUtilization, tc.resource.name, testNamespace, selector, tc.container)
368+
outReplicas, outUtilization, outRawValue, outTimestamp, err := replicaCalc.GetResourceReplicas(context.TODO(), tc.currentReplicas, tc.resource.targetUtilization, tc.resource.name, tolerances, testNamespace, selector, tc.container)
362369

363370
if tc.expectedError != nil {
364371
require.Error(t, err, "there should be an error calculating the replica count")
@@ -381,20 +388,20 @@ func (tc *replicaCalcTestCase) runTest(t *testing.T) {
381388
if tc.metric.singleObject == nil {
382389
t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
383390
}
384-
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, tc.metric.singleObject, selector, nil)
391+
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, tolerances, testNamespace, tc.metric.singleObject, selector, nil)
385392
case objectPerPodMetric:
386393
if tc.metric.singleObject == nil {
387394
t.Fatal("Metric specified as objectMetric but metric.singleObject is nil.")
388395
}
389-
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, testNamespace, tc.metric.singleObject, nil)
396+
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetObjectPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, tolerances, testNamespace, tc.metric.singleObject, nil)
390397
case externalMetric:
391398
if tc.metric.selector == nil {
392399
t.Fatal("Metric specified as externalMetric but metric.selector is nil.")
393400
}
394401
if tc.metric.targetUsage <= 0 {
395402
t.Fatalf("Metric specified as externalMetric but metric.targetUsage is %d which is <=0.", tc.metric.targetUsage)
396403
}
397-
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, tc.metric.selector, selector)
404+
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, tolerances, testNamespace, tc.metric.selector, selector)
398405
case externalPerPodMetric:
399406
if tc.metric.selector == nil {
400407
t.Fatal("Metric specified as externalPerPodMetric but metric.selector is nil.")
@@ -403,9 +410,9 @@ func (tc *replicaCalcTestCase) runTest(t *testing.T) {
403410
t.Fatalf("Metric specified as externalPerPodMetric but metric.perPodTargetUsage is %d which is <=0.", tc.metric.perPodTargetUsage)
404411
}
405412

406-
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, testNamespace, tc.metric.selector)
413+
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetExternalPerPodMetricReplicas(tc.currentReplicas, tc.metric.perPodTargetUsage, tc.metric.name, tolerances, testNamespace, tc.metric.selector)
407414
case podMetric:
408-
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, testNamespace, selector, nil)
415+
outReplicas, outUsage, outTimestamp, err = replicaCalc.GetMetricReplicas(tc.currentReplicas, tc.metric.targetUsage, tc.metric.name, tolerances, testNamespace, selector, nil)
409416
default:
410417
t.Fatalf("Unknown metric type: %d", tc.metric.metricType)
411418
}
@@ -1263,6 +1270,188 @@ func TestReplicaCalcTolerancePerPodCMExternal(t *testing.T) {
12631270
tc.runTest(t)
12641271
}
12651272

1273+
func TestReplicaCalcConfigurableTolerance(t *testing.T) {
1274+
testCases := []struct {
1275+
name string
1276+
replicaCalcTestCase
1277+
}{
1278+
{
1279+
name: "Outside of a 0% tolerance",
1280+
replicaCalcTestCase: replicaCalcTestCase{
1281+
tolerances: &Tolerances{0., 0.},
1282+
currentReplicas: 3,
1283+
expectedReplicas: 4,
1284+
resource: &resourceInfo{
1285+
name: v1.ResourceCPU,
1286+
requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1287+
levels: makePodMetricLevels(909, 1010, 1111),
1288+
targetUtilization: 100,
1289+
expectedUtilization: 101,
1290+
expectedValue: numContainersPerPod * 1010,
1291+
},
1292+
},
1293+
},
1294+
{
1295+
name: "Within a 200% scale-up tolerance",
1296+
replicaCalcTestCase: replicaCalcTestCase{
1297+
tolerances: &Tolerances{defaultTestingTolerance, 2.},
1298+
currentReplicas: 3,
1299+
expectedReplicas: 3,
1300+
resource: &resourceInfo{
1301+
name: v1.ResourceCPU,
1302+
requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1303+
levels: makePodMetricLevels(1890, 1910, 1900),
1304+
targetUtilization: 100,
1305+
expectedUtilization: 190,
1306+
expectedValue: numContainersPerPod * 1900,
1307+
},
1308+
},
1309+
},
1310+
{
1311+
name: "Outside 8% scale-up tolerance (and superfuous scale-down tolerance)",
1312+
replicaCalcTestCase: replicaCalcTestCase{
1313+
tolerances: &Tolerances{2., .08},
1314+
currentReplicas: 3,
1315+
expectedReplicas: 4,
1316+
resource: &resourceInfo{
1317+
name: v1.ResourceCPU,
1318+
requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1319+
levels: makePodMetricLevels(1100, 1080, 1090),
1320+
targetUtilization: 100,
1321+
expectedUtilization: 109,
1322+
expectedValue: numContainersPerPod * 1090,
1323+
},
1324+
},
1325+
},
1326+
{
1327+
name: "Within a 36% scale-down tolerance",
1328+
replicaCalcTestCase: replicaCalcTestCase{
1329+
tolerances: &Tolerances{.36, defaultTestingTolerance},
1330+
currentReplicas: 3,
1331+
expectedReplicas: 3,
1332+
resource: &resourceInfo{
1333+
name: v1.ResourceCPU,
1334+
requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1335+
levels: makePodMetricLevels(660, 640, 650),
1336+
targetUtilization: 100,
1337+
expectedUtilization: 65,
1338+
expectedValue: numContainersPerPod * 650,
1339+
},
1340+
},
1341+
},
1342+
{
1343+
name: "Outside a 34% scale-down tolerance",
1344+
replicaCalcTestCase: replicaCalcTestCase{
1345+
tolerances: &Tolerances{.34, defaultTestingTolerance},
1346+
currentReplicas: 3,
1347+
expectedReplicas: 2,
1348+
resource: &resourceInfo{
1349+
name: v1.ResourceCPU,
1350+
requests: []resource.Quantity{resource.MustParse("0.9"), resource.MustParse("1.0"), resource.MustParse("1.1")},
1351+
levels: makePodMetricLevels(660, 640, 650),
1352+
targetUtilization: 100,
1353+
expectedUtilization: 65,
1354+
expectedValue: numContainersPerPod * 650,
1355+
},
1356+
},
1357+
},
1358+
}
1359+
for _, tc := range testCases {
1360+
t.Run(tc.name, tc.runTest)
1361+
}
1362+
}
1363+
1364+
func TestReplicaCalcConfigurableToleranceCM(t *testing.T) {
1365+
tc := replicaCalcTestCase{
1366+
tolerances: &Tolerances{defaultTestingTolerance, .01},
1367+
currentReplicas: 3,
1368+
expectedReplicas: 4,
1369+
metric: &metricInfo{
1370+
name: "qps",
1371+
levels: []int64{20000, 21000, 21000},
1372+
targetUsage: 20000,
1373+
expectedUsage: 20666,
1374+
metricType: podMetric,
1375+
},
1376+
}
1377+
tc.runTest(t)
1378+
}
1379+
1380+
func TestReplicaCalcConfigurableToleranceCMObject(t *testing.T) {
1381+
tc := replicaCalcTestCase{
1382+
tolerances: &Tolerances{defaultTestingTolerance, .01},
1383+
currentReplicas: 3,
1384+
expectedReplicas: 4,
1385+
metric: &metricInfo{
1386+
name: "qps",
1387+
levels: []int64{20666},
1388+
targetUsage: 20000,
1389+
expectedUsage: 20666,
1390+
singleObject: &autoscalingv2.CrossVersionObjectReference{
1391+
Kind: "Deployment",
1392+
APIVersion: "apps/v1",
1393+
Name: "some-deployment",
1394+
},
1395+
},
1396+
}
1397+
tc.runTest(t)
1398+
}
1399+
1400+
func TestReplicaCalcConfigurableTolerancePerPodCMObject(t *testing.T) {
1401+
tc := replicaCalcTestCase{
1402+
tolerances: &Tolerances{defaultTestingTolerance, .01},
1403+
currentReplicas: 4,
1404+
expectedReplicas: 5,
1405+
metric: &metricInfo{
1406+
metricType: objectPerPodMetric,
1407+
name: "qps",
1408+
levels: []int64{20208},
1409+
perPodTargetUsage: 5000,
1410+
expectedUsage: 5052,
1411+
singleObject: &autoscalingv2.CrossVersionObjectReference{
1412+
Kind: "Deployment",
1413+
APIVersion: "apps/v1",
1414+
Name: "some-deployment",
1415+
},
1416+
},
1417+
}
1418+
tc.runTest(t)
1419+
}
1420+
1421+
func TestReplicaCalcConfigurableToleranceCMExternal(t *testing.T) {
1422+
tc := replicaCalcTestCase{
1423+
tolerances: &Tolerances{defaultTestingTolerance, .01},
1424+
currentReplicas: 3,
1425+
expectedReplicas: 4,
1426+
metric: &metricInfo{
1427+
name: "qps",
1428+
levels: []int64{8900},
1429+
targetUsage: 8800,
1430+
expectedUsage: 8900,
1431+
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
1432+
metricType: externalMetric,
1433+
},
1434+
}
1435+
tc.runTest(t)
1436+
}
1437+
1438+
func TestReplicaCalcConfigurableTolerancePerPodCMExternal(t *testing.T) {
1439+
tc := replicaCalcTestCase{
1440+
tolerances: &Tolerances{defaultTestingTolerance, .01},
1441+
currentReplicas: 3,
1442+
expectedReplicas: 4,
1443+
metric: &metricInfo{
1444+
name: "qps",
1445+
levels: []int64{8600},
1446+
perPodTargetUsage: 2800,
1447+
expectedUsage: 2867,
1448+
selector: &metav1.LabelSelector{MatchLabels: map[string]string{"label": "value"}},
1449+
metricType: externalPerPodMetric,
1450+
},
1451+
}
1452+
tc.runTest(t)
1453+
}
1454+
12661455
func TestReplicaCalcSuperfluousMetrics(t *testing.T) {
12671456
tc := replicaCalcTestCase{
12681457
currentReplicas: 4,

0 commit comments

Comments
 (0)
Please sign in to comment.