Skip to content

Commit 8d25fb7

Browse files
authored
Enable setting PodAutoscaler configuration via YAML labels (#409)
* enable config PA via metadata.labels * fix linter
1 parent 703c4f6 commit 8d25fb7

File tree

7 files changed

+266
-8
lines changed

7 files changed

+266
-8
lines changed

config/samples/autoscaling_v1alpha1_mock_llama.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ metadata:
55
labels:
66
app.kubernetes.io/name: aibrix
77
app.kubernetes.io/managed-by: kustomize
8+
autoscaling.aibrix.ai/max-scale-up-rate: "2"
9+
autoscaling.aibrix.ai/max-scale-down-rate: "2"
10+
kpa.autoscaling.aibrix.ai/stable-window: "60s"
11+
kpa.autoscaling.aibrix.ai/scale-down-delay: "60s"
812
namespace: aibrix-system
913
spec:
1014
scaleTargetRef:

pkg/controller/podautoscaler/common/context.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ limitations under the License.
1616

1717
package common
1818

19+
import (
20+
"strconv"
21+
22+
autoscalingv1alpha1 "github.com/aibrix/aibrix/api/autoscaling/v1alpha1"
23+
"k8s.io/klog/v2"
24+
)
25+
26+
const (
27+
AutoscalingLabelPrefix = "autoscaling.aibrix.ai/"
28+
maxScaleUpRateLabel = AutoscalingLabelPrefix + "max-scale-up-rate"
29+
maxScaleDownRateLabel = AutoscalingLabelPrefix + "max-scale-down-rate"
30+
)
31+
1932
// ScalingContext defines the generalized common that holds all necessary data for scaling calculations.
2033
type ScalingContext interface {
2134
GetTargetValue() float64
@@ -24,6 +37,7 @@ type ScalingContext interface {
2437
GetMaxScaleUpRate() float64
2538
GetMaxScaleDownRate() float64
2639
GetCurrentUsePerPod() float64
40+
UpdateByPaTypes(pa *autoscalingv1alpha1.PodAutoscaler) error
2741
}
2842

2943
// BaseScalingContext provides a base implementation of the ScalingContext interface.
@@ -42,6 +56,8 @@ type BaseScalingContext struct {
4256
currentUsePerPod float64
4357
}
4458

59+
var _ ScalingContext = (*BaseScalingContext)(nil)
60+
4561
// NewBaseScalingContext creates a new instance of BaseScalingContext with default values.
4662
func NewBaseScalingContext() *BaseScalingContext {
4763
return &BaseScalingContext{
@@ -53,6 +69,36 @@ func NewBaseScalingContext() *BaseScalingContext {
5369
}
5470
}
5571

72+
// UpdateByPaTypes should be invoked in any scaling context that embeds BaseScalingContext.
73+
func (b *BaseScalingContext) UpdateByPaTypes(pa *autoscalingv1alpha1.PodAutoscaler) error {
74+
b.ScalingMetric = pa.Spec.TargetMetric
75+
// parse target value
76+
targetValue, err := strconv.ParseFloat(pa.Spec.TargetValue, 64)
77+
if err != nil {
78+
klog.ErrorS(err, "Failed to parse target value", "targetValue", pa.Spec.TargetValue)
79+
return err
80+
}
81+
b.TargetValue = targetValue
82+
83+
for key, value := range pa.Labels {
84+
switch key {
85+
case maxScaleUpRateLabel:
86+
v, err := strconv.ParseFloat(value, 64)
87+
if err != nil {
88+
return err
89+
}
90+
b.MaxScaleUpRate = v
91+
case maxScaleDownRateLabel:
92+
v, err := strconv.ParseFloat(value, 64)
93+
if err != nil {
94+
return err
95+
}
96+
b.MaxScaleDownRate = v
97+
}
98+
}
99+
return nil
100+
}
101+
56102
func (b *BaseScalingContext) SetCurrentUsePerPod(value float64) {
57103
b.currentUsePerPod = value
58104
}

pkg/controller/podautoscaler/podautoscaler_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ func (r *PodAutoscalerReconciler) computeReplicasForMetrics(ctx context.Context,
602602

603603
err = r.updateScalerSpec(ctx, pa)
604604
if err != nil {
605+
klog.ErrorS(err, "Failed to update scaler spec from pa_types")
605606
return 0, "", currentTimestamp, fmt.Errorf("error update scaler spec: %w", err)
606607
}
607608

pkg/controller/podautoscaler/scaler/apa.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import (
3434
"k8s.io/klog/v2"
3535
)
3636

37+
const (
38+
APALabelPrefix = "apa." + scalingcontext.AutoscalingLabelPrefix
39+
upFluctuationToleranceLabel = APALabelPrefix + "up-fluctuation-tolerance"
40+
downFluctuationToleranceLabel = APALabelPrefix + "down-fluctuation-tolerance"
41+
)
42+
3743
// ApaScalingContext defines parameters for scaling decisions.
3844
type ApaScalingContext struct {
3945
scalingcontext.BaseScalingContext
@@ -85,6 +91,30 @@ func NewApaAutoscaler(readyPodsCount int, spec *ApaScalingContext) (*ApaAutoscal
8591
}, nil
8692
}
8793

94+
func (a *ApaScalingContext) UpdateByPaTypes(pa *autoscalingv1alpha1.PodAutoscaler) error {
95+
err := a.BaseScalingContext.UpdateByPaTypes(pa)
96+
if err != nil {
97+
return err
98+
}
99+
for key, value := range pa.Labels {
100+
switch key {
101+
case upFluctuationToleranceLabel:
102+
v, err := strconv.ParseFloat(value, 64)
103+
if err != nil {
104+
return err
105+
}
106+
a.UpFluctuationTolerance = v
107+
case downFluctuationToleranceLabel:
108+
v, err := strconv.ParseFloat(value, 64)
109+
if err != nil {
110+
return err
111+
}
112+
a.DownFluctuationTolerance = v
113+
}
114+
}
115+
return nil
116+
}
117+
88118
func (a *ApaAutoscaler) Scale(originalReadyPodsCount int, metricKey metrics.NamespaceNameMetric, now time.Time) ScaleResult {
89119
spec, ok := a.GetScalingContext().(*ApaScalingContext)
90120
if !ok {

pkg/controller/podautoscaler/scaler/apa_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import (
2020
"testing"
2121
"time"
2222

23+
autoscalingv1alpha1 "github.com/aibrix/aibrix/api/autoscaling/v1alpha1"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
2327
"github.com/aibrix/aibrix/pkg/controller/podautoscaler/metrics"
2428
)
2529

@@ -67,3 +71,52 @@ func TestAPAScale(t *testing.T) {
6771
t.Errorf("result should remain previous replica = %d, but got %d", readyPodCount, result.DesiredPodCount)
6872
}
6973
}
74+
75+
func TestApaUpdateContext(t *testing.T) {
76+
pa := &autoscalingv1alpha1.PodAutoscaler{
77+
Spec: autoscalingv1alpha1.PodAutoscalerSpec{
78+
ScaleTargetRef: corev1.ObjectReference{
79+
Kind: "Deployment",
80+
Name: "example-deployment",
81+
},
82+
MinReplicas: nil, // expecting nil as default since it's a pointer and no value is assigned
83+
MaxReplicas: 5,
84+
TargetValue: "1",
85+
TargetMetric: "test.metrics",
86+
MetricsSources: []autoscalingv1alpha1.MetricSource{
87+
{
88+
Endpoint: "service1.example.com",
89+
Path: "/api/metrics/cpu",
90+
},
91+
},
92+
ScalingStrategy: "APA",
93+
},
94+
ObjectMeta: metav1.ObjectMeta{
95+
Labels: map[string]string{
96+
"autoscaling.aibrix.ai/max-scale-up-rate": "32.1",
97+
"autoscaling.aibrix.ai/max-scale-down-rate": "12.3",
98+
"apa.autoscaling.aibrix.ai/up-fluctuation-tolerance": "1.2",
99+
"apa.autoscaling.aibrix.ai/down-fluctuation-tolerance": "0.9",
100+
},
101+
},
102+
}
103+
apaSpec := NewApaScalingContext()
104+
err := apaSpec.UpdateByPaTypes(pa)
105+
if err != nil {
106+
t.Errorf("Failed to update KpaScalingContext: %v", err)
107+
}
108+
if apaSpec.MaxScaleUpRate != 32.1 {
109+
t.Errorf("expected MaxScaleDownRate = 32.1, got %f", apaSpec.MaxScaleDownRate)
110+
}
111+
if apaSpec.MaxScaleDownRate != 12.3 {
112+
t.Errorf("expected MaxScaleDownRate = 12.3, got %f", apaSpec.MaxScaleDownRate)
113+
}
114+
115+
if apaSpec.UpFluctuationTolerance != 1.2 {
116+
t.Errorf("expected UpFluctuationTolerance = 1.2, got %f", apaSpec.UpFluctuationTolerance)
117+
}
118+
if apaSpec.DownFluctuationTolerance != 0.9 {
119+
t.Errorf("expected DownFluctuationTolerance = 0.9, got %f", apaSpec.DownFluctuationTolerance)
120+
}
121+
122+
}

pkg/controller/podautoscaler/scaler/kpa.go

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ If the metric no longer exceeds the panic threshold, exit the panic mode.
5959
6060
*/
6161

62+
const (
63+
KPALabelPrefix = "kpa." + scalingcontext.AutoscalingLabelPrefix
64+
targetBurstCapacityLabel = KPALabelPrefix + "target-burst-capacity"
65+
activationScaleLabel = KPALabelPrefix + "activation-scale"
66+
panicThresholdLabel = KPALabelPrefix + "panic-threshold"
67+
stableWindowLabel = KPALabelPrefix + "stable-window"
68+
scaleDownDelayLabel = KPALabelPrefix + "scale-down-delay"
69+
)
70+
6271
// KpaScalingContext defines parameters for scaling decisions.
6372
type KpaScalingContext struct {
6473
scalingcontext.BaseScalingContext
@@ -100,6 +109,57 @@ func NewKpaScalingContext() *KpaScalingContext {
100109
}
101110
}
102111

112+
func (k *KpaScalingContext) UpdateByPaTypes(pa *autoscalingv1alpha1.PodAutoscaler) error {
113+
err := k.BaseScalingContext.UpdateByPaTypes(pa)
114+
if err != nil {
115+
return err
116+
}
117+
for key, value := range pa.Labels {
118+
switch key {
119+
case targetBurstCapacityLabel:
120+
v, err := strconv.ParseFloat(value, 64)
121+
if err != nil {
122+
return err
123+
}
124+
k.TargetBurstCapacity = v
125+
case activationScaleLabel:
126+
v, err := strconv.ParseInt(value, 10, 32)
127+
if err != nil {
128+
return err
129+
}
130+
k.ActivationScale = int32(v)
131+
case panicThresholdLabel:
132+
v, err := strconv.ParseFloat(value, 64)
133+
if err != nil {
134+
return err
135+
}
136+
k.PanicThreshold = v
137+
case stableWindowLabel:
138+
v, err := time.ParseDuration(value)
139+
if err != nil {
140+
return err
141+
}
142+
k.StableWindow = v
143+
case scaleDownDelayLabel:
144+
v, err := time.ParseDuration(value)
145+
if err != nil {
146+
return err
147+
}
148+
k.ScaleDownDelay = v
149+
}
150+
}
151+
// unset some attribute if there are no configuration
152+
if _, exists := pa.Labels[scaleDownDelayLabel]; !exists {
153+
// TODO N.B. three parts of KPAScaler are stateful : panic_window, stable_window and delay windows.
154+
// reconcile() updates KpaScalingContext periodically, but doesn't reset these three parts.
155+
// These three parts are only initialized when controller starts.
156+
// Therefore, apply kpa.yaml cannot modify the panic_duration, stable_duration and delay_window duration
157+
k.ScaleDownDelay = 0
158+
}
159+
160+
return nil
161+
}
162+
103163
type KpaAutoscaler struct {
104164
specMux sync.RWMutex
105165
metricClient metrics.MetricClient
@@ -122,10 +182,14 @@ func NewKpaAutoscaler(readyPodsCount int, spec *KpaScalingContext) (*KpaAutoscal
122182
}
123183

124184
// Create a new delay window based on the ScaleDownDelay specified in the spec
125-
if spec.ScaleDownDelay <= 0 {
185+
if spec.ScaleDownDelay < 0 {
126186
return nil, errors.New("ScaleDownDelay must be positive")
127187
}
128-
delayWindow := aggregation.NewTimeWindow(spec.ScaleDownDelay, 1*time.Second)
188+
var delayWindow *aggregation.TimeWindow
189+
// If specify ScaleDownDelay, KpaAutoscaler.delayWindow will be initialized
190+
if spec.ScaleDownDelay > 0 {
191+
delayWindow = aggregation.NewTimeWindow(spec.ScaleDownDelay, 1*time.Second)
192+
}
129193

130194
// As KNative stated:
131195
// We always start in the panic mode, if the deployment is scaled up over 1 pod.
@@ -144,6 +208,7 @@ func NewKpaAutoscaler(readyPodsCount int, spec *KpaScalingContext) (*KpaAutoscal
144208
// TODO missing MetricClient
145209
metricsFetcher := &metrics.RestMetricsFetcher{}
146210
metricsClient := metrics.NewKPAMetricsClient(metricsFetcher)
211+
147212
scalingAlgorithm := algorithm.KpaScalingAlgorithm{}
148213

149214
return &KpaAutoscaler{
@@ -253,6 +318,7 @@ func (k *KpaAutoscaler) Scale(originalReadyPodsCount int, metricKey metrics.Name
253318
// in that case).
254319
klog.V(4).InfoS("DelayWindow details", "delayWindow", k.delayWindow.String())
255320
if k.delayWindow != nil {
321+
// the actual desiredPodCount will be recorded, but return the max replicas during passed delayWindow
256322
k.delayWindow.Record(now, float64(desiredPodCount))
257323
delayedPodCount, err := k.delayWindow.Max()
258324
if err != nil {
@@ -319,15 +385,10 @@ func (k *KpaAutoscaler) UpdateSourceMetrics(ctx context.Context, metricKey metri
319385
func (k *KpaAutoscaler) UpdateScalingContext(pa autoscalingv1alpha1.PodAutoscaler) error {
320386
k.specMux.Lock()
321387
defer k.specMux.Unlock()
322-
323-
targetValue, err := strconv.ParseFloat(pa.Spec.TargetValue, 64)
388+
err := k.scalingContext.UpdateByPaTypes(&pa)
324389
if err != nil {
325-
klog.ErrorS(err, "Failed to parse target value", "targetValue", pa.Spec.TargetValue)
326390
return err
327391
}
328-
k.scalingContext.TargetValue = targetValue
329-
k.scalingContext.ScalingMetric = pa.Spec.TargetMetric
330-
331392
return nil
332393
}
333394

pkg/controller/podautoscaler/scaler/kpa_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ import (
2020
"testing"
2121
"time"
2222

23+
autoscalingv1alpha1 "github.com/aibrix/aibrix/api/autoscaling/v1alpha1"
24+
corev1 "k8s.io/api/core/v1"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
2327
"github.com/aibrix/aibrix/pkg/controller/podautoscaler/common"
2428

2529
"github.com/aibrix/aibrix/pkg/controller/podautoscaler/metrics"
@@ -70,3 +74,62 @@ func TestKpaScale(t *testing.T) {
7074
t.Errorf("result.DesiredPodCount = 10, got %d", result.DesiredPodCount)
7175
}
7276
}
77+
78+
func TestKpaUpdateContext(t *testing.T) {
79+
pa := &autoscalingv1alpha1.PodAutoscaler{
80+
Spec: autoscalingv1alpha1.PodAutoscalerSpec{
81+
ScaleTargetRef: corev1.ObjectReference{
82+
Kind: "Deployment",
83+
Name: "example-deployment",
84+
},
85+
MinReplicas: nil, // expecting nil as default since it's a pointer and no value is assigned
86+
MaxReplicas: 5,
87+
TargetValue: "1",
88+
TargetMetric: "test.metrics",
89+
MetricsSources: []autoscalingv1alpha1.MetricSource{
90+
{
91+
Endpoint: "service1.example.com",
92+
Path: "/api/metrics/cpu",
93+
},
94+
},
95+
ScalingStrategy: "KPA",
96+
},
97+
ObjectMeta: metav1.ObjectMeta{
98+
Labels: map[string]string{
99+
"autoscaling.aibrix.ai/max-scale-up-rate": "32.1",
100+
"autoscaling.aibrix.ai/max-scale-down-rate": "12.3",
101+
"kpa.autoscaling.aibrix.ai/target-burst-capacity": "45.6",
102+
"kpa.autoscaling.aibrix.ai/activation-scale": "3",
103+
"kpa.autoscaling.aibrix.ai/panic-threshold": "2.5",
104+
"kpa.autoscaling.aibrix.ai/stable-window": "60s",
105+
"kpa.autoscaling.aibrix.ai/scale-down-delay": "30s",
106+
},
107+
},
108+
}
109+
kpaSpec := NewKpaScalingContext()
110+
err := kpaSpec.UpdateByPaTypes(pa)
111+
if err != nil {
112+
t.Errorf("Failed to update KpaScalingContext: %v", err)
113+
}
114+
if kpaSpec.MaxScaleUpRate != 32.1 {
115+
t.Errorf("expected MaxScaleDownRate = 32.1, got %f", kpaSpec.MaxScaleDownRate)
116+
}
117+
if kpaSpec.MaxScaleDownRate != 12.3 {
118+
t.Errorf("expected MaxScaleDownRate = 12.3, got %f", kpaSpec.MaxScaleDownRate)
119+
}
120+
if kpaSpec.TargetBurstCapacity != 45.6 {
121+
t.Errorf("expected TargetBurstCapacity = 45.6, got %f", kpaSpec.TargetBurstCapacity)
122+
}
123+
if kpaSpec.ActivationScale != 3 {
124+
t.Errorf("expected ActivationScale = 3, got %d", kpaSpec.ActivationScale)
125+
}
126+
if kpaSpec.PanicThreshold != 2.5 {
127+
t.Errorf("expected PanicThreshold = 2.5, got %f", kpaSpec.PanicThreshold)
128+
}
129+
if kpaSpec.StableWindow != 60*time.Second {
130+
t.Errorf("expected StableWindow = 60s, got %v", kpaSpec.StableWindow)
131+
}
132+
if kpaSpec.ScaleDownDelay != 30*time.Second {
133+
t.Errorf("expected ScaleDownDelay = 10s, got %v", kpaSpec.ScaleDownDelay)
134+
}
135+
}

0 commit comments

Comments
 (0)