Skip to content

Commit a41284d

Browse files
committed
Add the HorizontalPodAutoscaler tolerance field.
Includes v2beta2 HPA round-trip conversion, defaulting, and validation.
1 parent 463b15b commit a41284d

File tree

11 files changed

+745
-52
lines changed

11 files changed

+745
-52
lines changed

pkg/apis/autoscaling/annotations.go

+8
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ const DefaultCPUUtilization = 80
3636
// BehaviorSpecsAnnotation is the annotation which holds the HPA constraints specs
3737
// when converting the `Behavior` field from autoscaling/v2beta2
3838
const BehaviorSpecsAnnotation = "autoscaling.alpha.kubernetes.io/behavior"
39+
40+
// ToleranceScaleDownAnnotation is the annotation which holds the HPA tolerance specs
41+
// when converting the `ScaleDown.Tolerance` field from autoscaling/v2
42+
const ToleranceScaleDownAnnotation = "autoscaling.alpha.kubernetes.io/scale-down-tolerance"
43+
44+
// ToleranceScaleUpAnnotation is the annotation which holds the HPA tolerance specs
45+
// when converting the `ScaleUp.Tolerance` field from autoscaling/v2
46+
const ToleranceScaleUpAnnotation = "autoscaling.alpha.kubernetes.io/scale-up-tolerance"

pkg/apis/autoscaling/helpers.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ limitations under the License.
1616

1717
package autoscaling
1818

19-
// DropRoundTripHorizontalPodAutoscalerAnnotations removes any annotations used to serialize round-tripped fields from later API versions,
19+
// DropRoundTripHorizontalPodAutoscalerAnnotations removes any annotations used to
20+
// serialize round-tripped fields from HorizontalPodAutoscaler later API versions,
2021
// and returns false if no changes were made and the original input object was returned.
22+
//
2123
// It should always be called when converting internal -> external versions, prior
2224
// to setting any of the custom annotations:
2325
//
@@ -34,12 +36,16 @@ package autoscaling
3436
func DropRoundTripHorizontalPodAutoscalerAnnotations(in map[string]string) (out map[string]string, copied bool) {
3537
_, hasMetricsSpecs := in[MetricSpecsAnnotation]
3638
_, hasBehaviorSpecs := in[BehaviorSpecsAnnotation]
39+
_, hasToleranceScaleDown := in[ToleranceScaleDownAnnotation]
40+
_, hasToleranceScaleUp := in[ToleranceScaleUpAnnotation]
3741
_, hasMetricsStatuses := in[MetricStatusesAnnotation]
3842
_, hasConditions := in[HorizontalPodAutoscalerConditionsAnnotation]
39-
if hasMetricsSpecs || hasBehaviorSpecs || hasMetricsStatuses || hasConditions {
43+
if hasMetricsSpecs || hasBehaviorSpecs || hasToleranceScaleDown || hasToleranceScaleUp || hasMetricsStatuses || hasConditions {
4044
out = DeepCopyStringMap(in)
4145
delete(out, MetricSpecsAnnotation)
4246
delete(out, BehaviorSpecsAnnotation)
47+
delete(out, ToleranceScaleDownAnnotation)
48+
delete(out, ToleranceScaleUpAnnotation)
4349
delete(out, MetricStatusesAnnotation)
4450
delete(out, HorizontalPodAutoscalerConditionsAnnotation)
4551
return out, true

pkg/apis/autoscaling/types.go

+23-4
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,18 @@ const (
138138
DisabledPolicySelect ScalingPolicySelect = "Disabled"
139139
)
140140

141-
// HPAScalingRules configures the scaling behavior for one direction.
142-
// These Rules are applied after calculating DesiredReplicas from metrics for the HPA.
141+
// HPAScalingRules configures the scaling behavior for one direction via
142+
// scaling Policy Rules and a configurable metric tolerance.
143+
//
144+
// Scaling Policy Rules are applied after calculating DesiredReplicas from metrics for the HPA.
143145
// They can limit the scaling velocity by specifying scaling policies.
144146
// They can prevent flapping by specifying the stabilization window, so that the
145147
// number of replicas is not set instantly, instead, the safest value from the stabilization
146148
// window is chosen.
149+
//
150+
// The tolerance is applied to the metric values and prevents scaling too
151+
// eagerly for small metric variations. (Note that setting a tolerance requires
152+
// enabling the alpha HPAConfigurableTolerance feature gate.)
147153
type HPAScalingRules struct {
148154
// StabilizationWindowSeconds is the number of seconds for which past recommendations should be
149155
// considered while scaling up or scaling down.
@@ -157,10 +163,23 @@ type HPAScalingRules struct {
157163
// If not set, the default value MaxPolicySelect is used.
158164
// +optional
159165
SelectPolicy *ScalingPolicySelect
160-
// policies is a list of potential scaling polices which can used during scaling.
161-
// At least one policy must be specified, otherwise the HPAScalingRules will be discarded as invalid
166+
// policies is a list of potential scaling polices which can be used during scaling.
167+
// If not set, use the default values:
168+
// - For scale up: allow doubling the number of pods, or an absolute change of 4 pods in a 15s window.
169+
// - For scale down: allow all pods to be removed in a 15s window.
162170
// +optional
163171
Policies []HPAScalingPolicy
172+
// tolerance is the tolerance on the ratio between the current and desired
173+
// metric value under which no updates are made to the desired number of
174+
// replicas (e.g. 0.01 for 1%). Must be greater than or equal to zero. If not
175+
// set, the default cluster-wide tolerance is applied (by default 10%).
176+
//
177+
// This is an alpha field and requires enabling the HPAConfigurableTolerance
178+
// feature gate.
179+
//
180+
// +featureGate=HPAConfigurableTolerance
181+
// +optional
182+
Tolerance *resource.Quantity
164183
}
165184

166185
// HPAScalingPolicyType is the type of the policy which could be used while making scaling decisions.

pkg/apis/autoscaling/v2/defaults.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ func SetDefaults_HorizontalPodAutoscaler(obj *autoscalingv2.HorizontalPodAutosca
9191
SetDefaults_HorizontalPodAutoscalerBehavior(obj)
9292
}
9393

94-
// SetDefaults_HorizontalPodAutoscalerBehavior fills the behavior if it is not null
94+
// SetDefaults_HorizontalPodAutoscalerBehavior fills the behavior if it contains
95+
// at least one scaling rule policy (for scale-up or scale-down)
9596
func SetDefaults_HorizontalPodAutoscalerBehavior(obj *autoscalingv2.HorizontalPodAutoscaler) {
96-
// if behavior is specified, we should fill all the 'nil' values with the default ones
97+
// If behavior contains a scaling rule policy (either for scale-up, scale-down, or both), we
98+
// should fill all the unset scaling policy fields (i.e. StabilizationWindowSeconds,
99+
// SelectPolicy, Policies) with default values
97100
if obj.Spec.Behavior != nil {
98101
obj.Spec.Behavior.ScaleUp = GenerateHPAScaleUpRules(obj.Spec.Behavior.ScaleUp)
99102
obj.Spec.Behavior.ScaleDown = GenerateHPAScaleDownRules(obj.Spec.Behavior.ScaleDown)
@@ -129,5 +132,8 @@ func copyHPAScalingRules(from, to *autoscalingv2.HPAScalingRules) *autoscalingv2
129132
if from.Policies != nil {
130133
to.Policies = from.Policies
131134
}
135+
if from.Tolerance != nil {
136+
to.Tolerance = from.Tolerance
137+
}
132138
return to
133139
}

pkg/apis/autoscaling/v2/defaults_test.go

+148
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ import (
2020
"reflect"
2121
"testing"
2222

23+
"k8s.io/apimachinery/pkg/api/resource"
2324
"k8s.io/apimachinery/pkg/runtime"
25+
utilfeature "k8s.io/apiserver/pkg/util/feature"
26+
featuregatetesting "k8s.io/component-base/featuregate/testing"
2427
"k8s.io/kubernetes/pkg/api/legacyscheme"
28+
"k8s.io/kubernetes/pkg/features"
2529

2630
"github.com/google/go-cmp/cmp"
2731
"github.com/stretchr/testify/assert"
@@ -132,14 +136,17 @@ func TestGenerateScaleUpRules(t *testing.T) {
132136
rateUpPercentPeriodSeconds int32
133137
stabilizationSeconds *int32
134138
selectPolicy *autoscalingv2.ScalingPolicySelect
139+
tolerance *resource.Quantity
135140

136141
expectedPolicies []autoscalingv2.HPAScalingPolicy
137142
expectedStabilization *int32
138143
expectedSelectPolicy string
144+
expectedTolerance *resource.Quantity
139145
annotation string
140146
}
141147
maxPolicy := autoscalingv2.MaxChangePolicySelect
142148
minPolicy := autoscalingv2.MinChangePolicySelect
149+
sampleTolerance := resource.MustParse("0.5")
143150
tests := []TestCase{
144151
{
145152
annotation: "Default values",
@@ -208,12 +215,25 @@ func TestGenerateScaleUpRules(t *testing.T) {
208215
expectedStabilization: utilpointer.Int32(25),
209216
expectedSelectPolicy: string(autoscalingv2.MaxChangePolicySelect),
210217
},
218+
{
219+
annotation: "Percent policy and tolerance is specified",
220+
rateUpPercent: 7,
221+
rateUpPercentPeriodSeconds: 10,
222+
tolerance: &sampleTolerance,
223+
expectedPolicies: []autoscalingv2.HPAScalingPolicy{
224+
{Type: autoscalingv2.PercentScalingPolicy, Value: 7, PeriodSeconds: 10},
225+
},
226+
expectedStabilization: utilpointer.Int32(0),
227+
expectedSelectPolicy: string(autoscalingv2.MaxChangePolicySelect),
228+
expectedTolerance: &sampleTolerance,
229+
},
211230
}
212231
for _, tc := range tests {
213232
t.Run(tc.annotation, func(t *testing.T) {
214233
scaleUpRules := &autoscalingv2.HPAScalingRules{
215234
StabilizationWindowSeconds: tc.stabilizationSeconds,
216235
SelectPolicy: tc.selectPolicy,
236+
Tolerance: tc.tolerance,
217237
}
218238
if tc.rateUpPods != 0 || tc.rateUpPodsPeriodSeconds != 0 {
219239
scaleUpRules.Policies = append(scaleUpRules.Policies, autoscalingv2.HPAScalingPolicy{
@@ -234,10 +254,138 @@ func TestGenerateScaleUpRules(t *testing.T) {
234254
}
235255

236256
assert.Equal(t, autoscalingv2.ScalingPolicySelect(tc.expectedSelectPolicy), *up.SelectPolicy)
257+
assert.Equal(t, tc.expectedTolerance, up.Tolerance)
237258
})
238259
}
239260
}
240261

262+
func TestSetBehaviorDefaults(t *testing.T) {
263+
sampleTolerance := resource.MustParse("0.5")
264+
maxPolicy := autoscalingv2.MaxChangePolicySelect
265+
policies := []autoscalingv2.HPAScalingPolicy{
266+
{Type: autoscalingv2.PercentScalingPolicy, Value: 7, PeriodSeconds: 10},
267+
}
268+
type TestCase struct {
269+
behavior *autoscalingv2.HorizontalPodAutoscalerBehavior
270+
expectedBehavior *autoscalingv2.HorizontalPodAutoscalerBehavior
271+
annotation string
272+
}
273+
274+
tests := []TestCase{
275+
{
276+
annotation: "Nil behavior",
277+
behavior: nil,
278+
expectedBehavior: nil,
279+
},
280+
{
281+
annotation: "Behavior with stabilizationWindowSeconds and tolerance",
282+
behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
283+
ScaleUp: &autoscalingv2.HPAScalingRules{
284+
StabilizationWindowSeconds: utilpointer.Int32(100),
285+
Tolerance: &sampleTolerance,
286+
},
287+
},
288+
expectedBehavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
289+
ScaleDown: &autoscalingv2.HPAScalingRules{
290+
SelectPolicy: &maxPolicy,
291+
Policies: []autoscalingv2.HPAScalingPolicy{
292+
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
293+
},
294+
},
295+
ScaleUp: &autoscalingv2.HPAScalingRules{
296+
StabilizationWindowSeconds: utilpointer.Int32(100),
297+
SelectPolicy: &maxPolicy,
298+
Policies: []autoscalingv2.HPAScalingPolicy{
299+
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
300+
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
301+
},
302+
Tolerance: &sampleTolerance,
303+
},
304+
},
305+
},
306+
{
307+
annotation: "Behavior with policy, without tolerance",
308+
behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
309+
ScaleDown: &autoscalingv2.HPAScalingRules{
310+
Policies: policies,
311+
},
312+
},
313+
expectedBehavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
314+
ScaleDown: &autoscalingv2.HPAScalingRules{
315+
SelectPolicy: &maxPolicy,
316+
Policies: policies,
317+
},
318+
ScaleUp: &autoscalingv2.HPAScalingRules{
319+
StabilizationWindowSeconds: utilpointer.Int32(0),
320+
SelectPolicy: &maxPolicy,
321+
Policies: []autoscalingv2.HPAScalingPolicy{
322+
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
323+
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
324+
},
325+
},
326+
},
327+
},
328+
}
329+
for _, tc := range tests {
330+
t.Run(tc.annotation, func(t *testing.T) {
331+
hpa := autoscalingv2.HorizontalPodAutoscaler{
332+
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
333+
Behavior: tc.behavior,
334+
},
335+
}
336+
expectedHPA := autoscalingv2.HorizontalPodAutoscaler{
337+
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
338+
Behavior: tc.expectedBehavior,
339+
},
340+
}
341+
SetDefaults_HorizontalPodAutoscalerBehavior(&hpa)
342+
assert.Equal(t, expectedHPA, hpa)
343+
})
344+
}
345+
}
346+
347+
func TestSetBehaviorDefaultsConfigurableToleranceEnabled(t *testing.T) {
348+
// Enable HPAConfigurableTolerance feature gate.
349+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.HPAConfigurableTolerance, true)
350+
351+
// Verify that the tolerance field is left unset.
352+
maxPolicy := autoscalingv2.MaxChangePolicySelect
353+
354+
hpa := autoscalingv2.HorizontalPodAutoscaler{
355+
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
356+
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
357+
ScaleUp: &autoscalingv2.HPAScalingRules{
358+
StabilizationWindowSeconds: utilpointer.Int32(100),
359+
},
360+
},
361+
},
362+
}
363+
364+
expectedHPA := autoscalingv2.HorizontalPodAutoscaler{
365+
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
366+
Behavior: &autoscalingv2.HorizontalPodAutoscalerBehavior{
367+
ScaleDown: &autoscalingv2.HPAScalingRules{
368+
SelectPolicy: &maxPolicy,
369+
Policies: []autoscalingv2.HPAScalingPolicy{
370+
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
371+
},
372+
},
373+
ScaleUp: &autoscalingv2.HPAScalingRules{
374+
StabilizationWindowSeconds: utilpointer.Int32(100),
375+
SelectPolicy: &maxPolicy,
376+
Policies: []autoscalingv2.HPAScalingPolicy{
377+
{Type: autoscalingv2.PodsScalingPolicy, Value: 4, PeriodSeconds: 15},
378+
{Type: autoscalingv2.PercentScalingPolicy, Value: 100, PeriodSeconds: 15},
379+
},
380+
},
381+
},
382+
},
383+
}
384+
385+
SetDefaults_HorizontalPodAutoscalerBehavior(&hpa)
386+
assert.Equal(t, expectedHPA, hpa)
387+
}
388+
241389
func TestHorizontalPodAutoscalerAnnotations(t *testing.T) {
242390
tests := []struct {
243391
hpa autoscalingv2.HorizontalPodAutoscaler

pkg/apis/autoscaling/v2beta2/conversion.go

+64-3
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ limitations under the License.
1717
package v2beta2
1818

1919
import (
20+
"fmt"
21+
2022
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
2123

24+
"k8s.io/apimachinery/pkg/api/resource"
2225
"k8s.io/apimachinery/pkg/conversion"
2326
"k8s.io/kubernetes/pkg/apis/autoscaling"
2427
)
@@ -27,16 +30,74 @@ func Convert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutosca
2730
if err := autoConvert_autoscaling_HorizontalPodAutoscaler_To_v2beta2_HorizontalPodAutoscaler(in, out, s); err != nil {
2831
return err
2932
}
30-
// v2beta2 round-trips to internal without any serialized annotations, make sure any from other versions don't get serialized
31-
out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
33+
// Ensure old round-trips annotations are discarded
34+
annotations, copiedAnnotations := autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
35+
out.Annotations = annotations
36+
37+
behavior := in.Spec.Behavior
38+
if behavior == nil {
39+
return nil
40+
}
41+
// Save the tolerance fields in annotations for round-trip
42+
if behavior.ScaleDown != nil && behavior.ScaleDown.Tolerance != nil {
43+
if !copiedAnnotations {
44+
copiedAnnotations = true
45+
out.Annotations = autoscaling.DeepCopyStringMap(out.Annotations)
46+
}
47+
out.Annotations[autoscaling.ToleranceScaleDownAnnotation] = behavior.ScaleDown.Tolerance.String()
48+
}
49+
if behavior.ScaleUp != nil && behavior.ScaleUp.Tolerance != nil {
50+
if !copiedAnnotations {
51+
copiedAnnotations = true
52+
out.Annotations = autoscaling.DeepCopyStringMap(out.Annotations)
53+
}
54+
out.Annotations[autoscaling.ToleranceScaleUpAnnotation] = behavior.ScaleUp.Tolerance.String()
55+
}
3256
return nil
3357
}
3458

3559
func Convert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in *autoscalingv2beta2.HorizontalPodAutoscaler, out *autoscaling.HorizontalPodAutoscaler, s conversion.Scope) error {
3660
if err := autoConvert_v2beta2_HorizontalPodAutoscaler_To_autoscaling_HorizontalPodAutoscaler(in, out, s); err != nil {
3761
return err
3862
}
39-
// v2beta2 round-trips to internal without any serialized annotations, make sure any from other versions don't get serialized
63+
// Restore the tolerance fields from annotations for round-trip
64+
if tolerance, ok := out.Annotations[autoscaling.ToleranceScaleDownAnnotation]; ok {
65+
if out.Spec.Behavior == nil {
66+
out.Spec.Behavior = &autoscaling.HorizontalPodAutoscalerBehavior{}
67+
}
68+
if out.Spec.Behavior.ScaleDown == nil {
69+
out.Spec.Behavior.ScaleDown = &autoscaling.HPAScalingRules{}
70+
}
71+
q, err := resource.ParseQuantity(tolerance)
72+
if err != nil {
73+
return fmt.Errorf("failed to parse annotation %q: %w", autoscaling.ToleranceScaleDownAnnotation, err)
74+
}
75+
out.Spec.Behavior.ScaleDown.Tolerance = &q
76+
}
77+
if tolerance, ok := out.Annotations[autoscaling.ToleranceScaleUpAnnotation]; ok {
78+
if out.Spec.Behavior == nil {
79+
out.Spec.Behavior = &autoscaling.HorizontalPodAutoscalerBehavior{}
80+
}
81+
if out.Spec.Behavior.ScaleUp == nil {
82+
out.Spec.Behavior.ScaleUp = &autoscaling.HPAScalingRules{}
83+
}
84+
q, err := resource.ParseQuantity(tolerance)
85+
if err != nil {
86+
return fmt.Errorf("failed to parse annotation %q: %w", autoscaling.ToleranceScaleUpAnnotation, err)
87+
}
88+
out.Spec.Behavior.ScaleUp.Tolerance = &q
89+
}
90+
// Do not save round-trip annotations in internal resource
4091
out.Annotations, _ = autoscaling.DropRoundTripHorizontalPodAutoscalerAnnotations(out.Annotations)
4192
return nil
4293
}
94+
95+
func Convert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in *autoscalingv2beta2.HPAScalingRules, out *autoscaling.HPAScalingRules, s conversion.Scope) error {
96+
// Tolerance field is handled in the HorizontalPodAutoscaler conversion function.
97+
return autoConvert_v2beta2_HPAScalingRules_To_autoscaling_HPAScalingRules(in, out, s)
98+
}
99+
100+
func Convert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in *autoscaling.HPAScalingRules, out *autoscalingv2beta2.HPAScalingRules, s conversion.Scope) error {
101+
// Tolerance field is handled in the HorizontalPodAutoscaler conversion function.
102+
return autoConvert_autoscaling_HPAScalingRules_To_v2beta2_HPAScalingRules(in, out, s)
103+
}

0 commit comments

Comments
 (0)