Skip to content

Commit 68a479c

Browse files
authored
For scaling deployments in particular, use a direct object get rather than the /scale API (#1458)
* For scaling deployments in particular, use a direct object get rather than the /scale API so that it can use the informer cache for better performance. This is annoying as a special case but is so common and improves performance so much that I think it's worthwhile to include. Another option would be to majorly increase the QPS rate limit on the scaling API client however that would also increase kube-apiserver load while watches/informers are generally much less impactful. Signed-off-by: Noah Kantrowitz <noah@coderanger.net> * Update changelog. Signed-off-by: Noah Kantrowitz <noah@coderanger.net> * Use the already-normalized GVKR data so less weird string parsing. Also adds support for StatefulSets for symmetry. Signed-off-by: Noah Kantrowitz <noah@coderanger.net> * Apply suggestions from code review Co-authored-by: Zbynek Roubalik <726523+zroubalik@users.noreply.github.com> Signed-off-by: Noah Kantrowitz <noah@coderanger.net> * Apply suggestions from code review Co-authored-by: Zbynek Roubalik <726523+zroubalik@users.noreply.github.com> Signed-off-by: Noah Kantrowitz <noah@coderanger.net>
1 parent c6c2cd4 commit 68a479c

3 files changed

Lines changed: 61 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
- Add support for the WATCH_NAMESPACE environment variable to the operator ([#1474](https://github.com/kedacore/keda/pull/1474))
3535
- Automatically determine the RabbitMQ protocol when possible, and support setting the protocl via TriggerAuthentication ([#1459](https://github.com/kedacore/keda/pulls/1459),[#1483](https://github.com/kedacore/keda/pull/1483))
3636
- Improve performance when fetching pod information ([#1457](https://github.com/kedacore/keda/pull/1457))
37+
- Improve performance when fetching current scaling information on Deployments ([#1458](https://github.com/kedacore/keda/pull/1458))
3738

3839
### Breaking Changes
3940

controllers/scaledobject_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ func init() {
6969
// SetupWithManager initializes the ScaledObjectReconciler instance and starts a new controller managed by the passed Manager instance.
7070
func (r *ScaledObjectReconciler) SetupWithManager(mgr ctrl.Manager) error {
7171
// create Discovery clientset
72+
// TODO If we need to increase the QPS of scaling API calls, copy and tweak this RESTConfig.
7273
clientset, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig())
7374
if err != nil {
7475
r.Log.Error(err, "Not able to create Discovery clientset")

pkg/scaling/executor/scale_scaledobjects.go

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"time"
66

77
"github.com/go-logr/logr"
8+
appsv1 "k8s.io/api/apps/v1"
89
autoscalingv1 "k8s.io/api/autoscaling/v1"
910
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
1012

1113
kedav1alpha1 "github.com/kedacore/keda/v2/api/v1alpha1"
1214
)
@@ -16,18 +18,46 @@ func (e *scaleExecutor) RequestScale(ctx context.Context, scaledObject *kedav1al
1618
"scaledObject.Namespace", scaledObject.Namespace,
1719
"scaleTarget.Name", scaledObject.Spec.ScaleTargetRef.Name)
1820

19-
currentScale, err := e.getScaleTargetScale(ctx, scaledObject)
20-
if err != nil {
21-
logger.Error(err, "Error getting information on the current Scale (ie. replias count) on the scaleTarget")
22-
return
21+
// Get the current replica count. As a special case, Deployments and StatefulSets fetch directly from the object so they can use the informer cache
22+
// to reduce API calls. Everything else uses the scale subresource.
23+
var currentScale *autoscalingv1.Scale
24+
var currentReplicas int32
25+
targetName := scaledObject.Spec.ScaleTargetRef.Name
26+
targetGVKR := scaledObject.Status.ScaleTargetGVKR
27+
switch {
28+
case targetGVKR.Group == "apps" && targetGVKR.Kind == "Deployment":
29+
deployment := &appsv1.Deployment{}
30+
err := e.client.Get(ctx, client.ObjectKey{Name: targetName, Namespace: scaledObject.Namespace}, deployment)
31+
if err != nil {
32+
logger.Error(err, "Error getting information on the current Scale (ie. replicas count) on the scaleTarget")
33+
return
34+
}
35+
currentReplicas = *deployment.Spec.Replicas
36+
case targetGVKR.Group == "apps" && targetGVKR.Kind == "StatefulSet":
37+
statefulSet := &appsv1.StatefulSet{}
38+
err := e.client.Get(ctx, client.ObjectKey{Name: targetName, Namespace: scaledObject.Namespace}, statefulSet)
39+
if err != nil {
40+
logger.Error(err, "Error getting information on the current Scale (ie. replicas count) on the scaleTarget")
41+
return
42+
}
43+
currentReplicas = *statefulSet.Spec.Replicas
44+
default:
45+
var err error
46+
currentScale, err = e.getScaleTargetScale(ctx, scaledObject)
47+
if err != nil {
48+
logger.Error(err, "Error getting information on the current Scale (ie. replicas count) on the scaleTarget")
49+
return
50+
}
51+
currentReplicas = currentScale.Spec.Replicas
2352
}
53+
2454
switch {
25-
case currentScale.Spec.Replicas == 0 && isActive:
55+
case currentReplicas == 0 && isActive:
2656
// current replica count is 0, but there is an active trigger.
2757
// scale the ScaleTarget up
2858
e.scaleFromZero(ctx, logger, scaledObject, currentScale)
2959
case !isActive &&
30-
currentScale.Spec.Replicas > 0 &&
60+
currentReplicas > 0 &&
3161
(scaledObject.Spec.MinReplicaCount == nil || *scaledObject.Spec.MinReplicaCount == 0):
3262
// there are no active triggers, but the ScaleTarget has replicas.
3363
// AND
@@ -37,14 +67,12 @@ func (e *scaleExecutor) RequestScale(ctx context.Context, scaledObject *kedav1al
3767
e.scaleToZero(ctx, logger, scaledObject, currentScale)
3868
case !isActive &&
3969
scaledObject.Spec.MinReplicaCount != nil &&
40-
currentScale.Spec.Replicas < *scaledObject.Spec.MinReplicaCount:
70+
currentReplicas < *scaledObject.Spec.MinReplicaCount:
4171
// there are no active triggers
4272
// AND
4373
// ScaleTarget replicas count is less than minimum replica count specified in ScaledObject
4474
// Let's set ScaleTarget replicas count to correct value
45-
currentScale.Spec.Replicas = *scaledObject.Spec.MinReplicaCount
46-
47-
err := e.updateScaleOnScaleTarget(ctx, scaledObject, currentScale)
75+
_, err := e.updateScaleOnScaleTarget(ctx, scaledObject, currentScale, *scaledObject.Spec.MinReplicaCount)
4876
if err == nil {
4977
logger.Info("Successfully set ScaleTarget replicas count to ScaledObject minReplicaCount",
5078
"ScaleTarget.Replicas", currentScale.Spec.Replicas)
@@ -93,8 +121,7 @@ func (e *scaleExecutor) scaleToZero(ctx context.Context, logger logr.Logger, sca
93121
if scaledObject.Status.LastActiveTime == nil ||
94122
scaledObject.Status.LastActiveTime.Add(cooldownPeriod).Before(time.Now()) {
95123
// or last time a trigger was active was > cooldown period, so scale down.
96-
scale.Spec.Replicas = 0
97-
err := e.updateScaleOnScaleTarget(ctx, scaledObject, scale)
124+
_, err := e.updateScaleOnScaleTarget(ctx, scaledObject, scale, 0)
98125
if err == nil {
99126
logger.Info("Successfully scaled ScaleTarget to 0 replicas")
100127
if err := e.setActiveCondition(ctx, logger, scaledObject, metav1.ConditionFalse, "ScalerNotActive", "Scaling is not performed because triggers are not active"); err != nil {
@@ -118,19 +145,19 @@ func (e *scaleExecutor) scaleToZero(ctx context.Context, logger logr.Logger, sca
118145
}
119146

120147
func (e *scaleExecutor) scaleFromZero(ctx context.Context, logger logr.Logger, scaledObject *kedav1alpha1.ScaledObject, scale *autoscalingv1.Scale) {
121-
currentReplicas := scale.Spec.Replicas
148+
var replicas int32
122149
if scaledObject.Spec.MinReplicaCount != nil && *scaledObject.Spec.MinReplicaCount > 0 {
123-
scale.Spec.Replicas = *scaledObject.Spec.MinReplicaCount
150+
replicas = *scaledObject.Spec.MinReplicaCount
124151
} else {
125-
scale.Spec.Replicas = 1
152+
replicas = 1
126153
}
127154

128-
err := e.updateScaleOnScaleTarget(ctx, scaledObject, scale)
155+
currentReplicas, err := e.updateScaleOnScaleTarget(ctx, scaledObject, scale, replicas)
129156

130157
if err == nil {
131158
logger.Info("Successfully updated ScaleTarget",
132159
"Original Replicas Count", currentReplicas,
133-
"New Replicas Count", scale.Spec.Replicas)
160+
"New Replicas Count", replicas)
134161

135162
// Scale was successful. Update lastScaleTime and lastActiveTime on the scaledObject
136163
if err := e.updateLastActiveTime(ctx, logger, scaledObject); err != nil {
@@ -144,7 +171,20 @@ func (e *scaleExecutor) getScaleTargetScale(ctx context.Context, scaledObject *k
144171
return (*e.scaleClient).Scales(scaledObject.Namespace).Get(ctx, scaledObject.Status.ScaleTargetGVKR.GroupResource(), scaledObject.Spec.ScaleTargetRef.Name, metav1.GetOptions{})
145172
}
146173

147-
func (e *scaleExecutor) updateScaleOnScaleTarget(ctx context.Context, scaledObject *kedav1alpha1.ScaledObject, scale *autoscalingv1.Scale) error {
174+
func (e *scaleExecutor) updateScaleOnScaleTarget(ctx context.Context, scaledObject *kedav1alpha1.ScaledObject, scale *autoscalingv1.Scale, replicas int32) (int32, error) {
175+
if scale == nil {
176+
// Wasn't retrieved earlier, grab it now.
177+
var err error
178+
scale, err = e.getScaleTargetScale(ctx, scaledObject)
179+
if err != nil {
180+
return -1, err
181+
}
182+
}
183+
184+
// Update with requested repliacs.
185+
currentReplicas := scale.Spec.Replicas
186+
scale.Spec.Replicas = replicas
187+
148188
_, err := (*e.scaleClient).Scales(scaledObject.Namespace).Update(ctx, scaledObject.Status.ScaleTargetGVKR.GroupResource(), scale, metav1.UpdateOptions{})
149-
return err
189+
return currentReplicas, err
150190
}

0 commit comments

Comments
 (0)