diff --git a/apis/autoscaling/v1alpha1/intelligenthorizontalpodautoscaler_types.go b/apis/autoscaling/v1alpha1/intelligenthorizontalpodautoscaler_types.go index 58137ab..ce00a4a 100644 --- a/apis/autoscaling/v1alpha1/intelligenthorizontalpodautoscaler_types.go +++ b/apis/autoscaling/v1alpha1/intelligenthorizontalpodautoscaler_types.go @@ -197,6 +197,10 @@ type IntelligentHorizontalPodAutoscalerBehavior struct { // ScalingBehavior defines the scaling behavior for one direction. type ScalingBehavior struct { + // Disabled means if the scaling of this direction is disabled. + // +optional + Disabled bool `json:"disabled,omitempty"` + // GrayStrategy is the configuration of the strategy for gray change of replicas. // If not set, gray change will be disabled. // +optional diff --git a/apis/autoscaling/v1alpha1/replicaprofile_types.go b/apis/autoscaling/v1alpha1/replicaprofile_types.go index 31ae185..82008be 100644 --- a/apis/autoscaling/v1alpha1/replicaprofile_types.go +++ b/apis/autoscaling/v1alpha1/replicaprofile_types.go @@ -59,6 +59,14 @@ type ReplicaProfileSpec struct { // +optional Paused bool `json:"paused,omitempty"` + // AllowedScalingDirection is the allowed scaling direction. + // Note that it only cares about online replicas. + // It defaults to Both. + // +optional + // +kubebuilder:validation:Enum=Both;Neither;Up;Down + // +kubebuilder:default=Both + AllowedScalingDirection ScalingDirection `json:"allowedScalingDirection"` + // Behavior configures the behavior of ReplicaProfile. // If not set, default behavior will be set. // +optional @@ -66,6 +74,15 @@ type ReplicaProfileSpec struct { Behavior ReplicaProfileBehavior `json:"behavior"` } +type ScalingDirection string + +const ( + ScalingDirectionBoth ScalingDirection = "Both" + ScalingDirectionNeither ScalingDirection = "Neither" + ScalingDirectionUp ScalingDirection = "Up" + ScalingDirectionDown ScalingDirection = "Down" +) + // ReplicaProfileBehavior defines the behavior of ReplicaProfile. type ReplicaProfileBehavior struct { // PodSorter is used to decide the priority of pods when scaling. diff --git a/config/crd/bases/autoscaling.kapacitystack.io_intelligenthorizontalpodautoscalers.yaml b/config/crd/bases/autoscaling.kapacitystack.io_intelligenthorizontalpodautoscalers.yaml index 9b00d23..e72cf0f 100644 --- a/config/crd/bases/autoscaling.kapacitystack.io_intelligenthorizontalpodautoscalers.yaml +++ b/config/crd/bases/autoscaling.kapacitystack.io_intelligenthorizontalpodautoscalers.yaml @@ -121,6 +121,10 @@ spec: description: ScaleDown is the behavior configuration for scaling down. properties: + disabled: + description: Disabled means if the scaling of this direction + is disabled. + type: boolean grayStrategy: description: GrayStrategy is the configuration of the strategy for gray change of replicas. If not set, gray change will @@ -171,6 +175,10 @@ spec: description: ScaleUp is the behavior configuration for scaling up. properties: + disabled: + description: Disabled means if the scaling of this direction + is disabled. + type: boolean grayStrategy: description: GrayStrategy is the configuration of the strategy for gray change of replicas. If not set, gray change will diff --git a/config/crd/bases/autoscaling.kapacitystack.io_replicaprofiles.yaml b/config/crd/bases/autoscaling.kapacitystack.io_replicaprofiles.yaml index 06b81ee..0f30766 100644 --- a/config/crd/bases/autoscaling.kapacitystack.io_replicaprofiles.yaml +++ b/config/crd/bases/autoscaling.kapacitystack.io_replicaprofiles.yaml @@ -39,6 +39,16 @@ spec: spec: description: ReplicaProfileSpec defines the desired state of ReplicaProfile. properties: + allowedScalingDirection: + default: Both + description: AllowedScalingDirection is the allowed scaling direction. + Note that it only cares about online replicas. It defaults to Both. + enum: + - Both + - Neither + - Up + - Down + type: string behavior: default: podSorter: diff --git a/controllers/autoscaling/intelligenthorizontalpodautoscaler_controller.go b/controllers/autoscaling/intelligenthorizontalpodautoscaler_controller.go index 9faab77..576e1ae 100644 --- a/controllers/autoscaling/intelligenthorizontalpodautoscaler_controller.go +++ b/controllers/autoscaling/intelligenthorizontalpodautoscaler_controller.go @@ -245,6 +245,7 @@ func (r *IntelligentHorizontalPodAutoscalerReconciler) Reconcile(ctx context.Con rp.Spec.CutoffReplicas = replicaData.CutoffReplicas rp.Spec.StandbyReplicas = replicaData.StandbyReplicas rp.Spec.Paused = ihpa.Spec.Paused + rp.Spec.AllowedScalingDirection = getAllowedScalingDirection(ihpa) if ihpa.Spec.Behavior.ReplicaProfile != nil { rp.Spec.Behavior = *ihpa.Spec.Behavior.ReplicaProfile } else { @@ -317,16 +318,31 @@ func newReplicaProfile(ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutosca }, }, Spec: autoscalingv1alpha1.ReplicaProfileSpec{ - ScaleTargetRef: ihpa.Spec.ScaleTargetRef, - OnlineReplicas: replicaData.OnlineReplicas, - CutoffReplicas: replicaData.CutoffReplicas, - StandbyReplicas: replicaData.StandbyReplicas, - Paused: ihpa.Spec.Paused, - Behavior: behavior, + ScaleTargetRef: ihpa.Spec.ScaleTargetRef, + OnlineReplicas: replicaData.OnlineReplicas, + CutoffReplicas: replicaData.CutoffReplicas, + StandbyReplicas: replicaData.StandbyReplicas, + Paused: ihpa.Spec.Paused, + AllowedScalingDirection: getAllowedScalingDirection(ihpa), + Behavior: behavior, }, } } +func getAllowedScalingDirection(ihpa *autoscalingv1alpha1.IntelligentHorizontalPodAutoscaler) autoscalingv1alpha1.ScalingDirection { + up, down := !ihpa.Spec.Behavior.ScaleUp.Disabled, !ihpa.Spec.Behavior.ScaleDown.Disabled + if up && down { + return autoscalingv1alpha1.ScalingDirectionBoth + } + if up { + return autoscalingv1alpha1.ScalingDirectionUp + } + if down { + return autoscalingv1alpha1.ScalingDirectionDown + } + return autoscalingv1alpha1.ScalingDirectionNeither +} + func defaultReplicaProfileBehavior() autoscalingv1alpha1.ReplicaProfileBehavior { return autoscalingv1alpha1.ReplicaProfileBehavior{ PodSorter: autoscalingv1alpha1.PodSorter{ diff --git a/controllers/autoscaling/replicaprofile_controller.go b/controllers/autoscaling/replicaprofile_controller.go index bc1a95c..832ffdf 100644 --- a/controllers/autoscaling/replicaprofile_controller.go +++ b/controllers/autoscaling/replicaprofile_controller.go @@ -159,7 +159,7 @@ func (r *ReplicaProfileReconciler) Reconcile(ctx context.Context, req ctrl.Reque rp.Status.CutoffReplicas = int32(len(currentRunningPods[autoscalingv1alpha1.PodStateCutoff])) rp.Status.StandbyReplicas = int32(len(currentRunningPods[autoscalingv1alpha1.PodStateStandby])) - ensured := rp.Status.OnlineReplicas == rp.Spec.OnlineReplicas && + ensured := isOnlineReplicasEnsured(rp) && rp.Status.CutoffReplicas == rp.Spec.CutoffReplicas && rp.Status.StandbyReplicas == rp.Spec.StandbyReplicas if ensured { @@ -218,7 +218,11 @@ func (r *ReplicaProfileReconciler) Reconcile(ctx context.Context, req ctrl.Reque return ctrl.Result{}, err } - sm := pod.NewStateManager(rp, podSorter, currentRunningPods) + sm := pod.NewStateManager(map[autoscalingv1alpha1.PodState]int32{ + autoscalingv1alpha1.PodStateOnline: getDesiredOnlineReplicas(rp), + autoscalingv1alpha1.PodStateCutoff: rp.Spec.CutoffReplicas, + autoscalingv1alpha1.PodStateStandby: rp.Spec.StandbyReplicas, + }, currentRunningPods, podSorter) change, err := sm.CalculateStateChange(ctx) if err != nil { l.Error(err, "failed to calculate state change") @@ -309,7 +313,7 @@ func (r *ReplicaProfileReconciler) Reconcile(ctx context.Context, req ctrl.Reque } // Scale replicas if needed - desiredReplicas := rp.Spec.OnlineReplicas + rp.Spec.CutoffReplicas + rp.Spec.StandbyReplicas + desiredReplicas := getDesiredOnlineReplicas(rp) + rp.Spec.CutoffReplicas + rp.Spec.StandbyReplicas if desiredReplicas != scale.Spec.Replicas { l.Info("rescale target workload", "oldReplicas", scale.Spec.Replicas, "newReplicas", desiredReplicas) r.Eventf(rp, corev1.EventTypeNormal, "UpdateScale", "rescale target workload from %d to %d replicas", scale.Spec.Replicas, desiredReplicas) @@ -511,3 +515,29 @@ func (r *ReplicaProfileReconciler) setPodState(ctx context.Context, p *corev1.Po func setReplicaProfileCondition(rp *autoscalingv1alpha1.ReplicaProfile, conditionType autoscalingv1alpha1.ReplicaProfileConditionType, status metav1.ConditionStatus, reason, message string) { rp.Status.Conditions = util.SetConditionInList(rp.Status.Conditions, string(conditionType), status, rp.Generation, reason, message) } + +func isOnlineReplicasEnsured(rp *autoscalingv1alpha1.ReplicaProfile) bool { + switch rp.Spec.AllowedScalingDirection { + case autoscalingv1alpha1.ScalingDirectionUp: + return rp.Status.OnlineReplicas >= rp.Spec.OnlineReplicas + case autoscalingv1alpha1.ScalingDirectionDown: + return rp.Status.OnlineReplicas <= rp.Spec.OnlineReplicas + case autoscalingv1alpha1.ScalingDirectionNeither: + return true + default: + return rp.Status.OnlineReplicas == rp.Spec.OnlineReplicas + } +} + +func getDesiredOnlineReplicas(rp *autoscalingv1alpha1.ReplicaProfile) int32 { + switch rp.Spec.AllowedScalingDirection { + case autoscalingv1alpha1.ScalingDirectionUp: + return util.MaxInt32(rp.Spec.OnlineReplicas, rp.Status.OnlineReplicas) + case autoscalingv1alpha1.ScalingDirectionDown: + return util.MinInt32(rp.Spec.OnlineReplicas, rp.Status.OnlineReplicas) + case autoscalingv1alpha1.ScalingDirectionNeither: + return rp.Status.OnlineReplicas + default: + return rp.Spec.OnlineReplicas + } +} diff --git a/pkg/pod/state.go b/pkg/pod/state.go index a5e3d6f..4cfc7ec 100644 --- a/pkg/pod/state.go +++ b/pkg/pod/state.go @@ -106,30 +106,21 @@ func newStateInfo() *stateInfo { // StateManager provides a method to calculate pod state change. type StateManager struct { - rp *autoscalingv1alpha1.ReplicaProfile - sorter sorter.Interface statesInfo map[autoscalingv1alpha1.PodState]*stateInfo podNameMap map[string]*corev1.Pod + sorter sorter.Interface } // NewStateManager build a state manager to calculate pod state change based on given spec and status. -func NewStateManager(rp *autoscalingv1alpha1.ReplicaProfile, sorter sorter.Interface, currentRunningPods map[autoscalingv1alpha1.PodState][]*corev1.Pod) *StateManager { +func NewStateManager(desiredReplicas map[autoscalingv1alpha1.PodState]int32, currentRunningPods map[autoscalingv1alpha1.PodState][]*corev1.Pod, sorter sorter.Interface) *StateManager { sm := &StateManager{ - rp: rp, - sorter: sorter, statesInfo: make(map[autoscalingv1alpha1.PodState]*stateInfo, len(defaultStatesOrdered)), podNameMap: make(map[string]*corev1.Pod), + sorter: sorter, } for _, state := range defaultStatesOrdered { info := newStateInfo() - switch state { - case autoscalingv1alpha1.PodStateOnline: - info.DesiredReplicas = int(rp.Spec.OnlineReplicas) - case autoscalingv1alpha1.PodStateCutoff: - info.DesiredReplicas = int(rp.Spec.CutoffReplicas) - case autoscalingv1alpha1.PodStateStandby: - info.DesiredReplicas = int(rp.Spec.StandbyReplicas) - } + info.DesiredReplicas = int(desiredReplicas[state]) for _, pod := range currentRunningPods[state] { info.CurrentPodNames.Insert(pod.Name) sm.podNameMap[pod.Name] = pod diff --git a/pkg/pod/state_test.go b/pkg/pod/state_test.go index 8fb7b6d..2abc348 100644 --- a/pkg/pod/state_test.go +++ b/pkg/pod/state_test.go @@ -250,7 +250,11 @@ func TestCalculateStateChange(t *testing.T) { for _, testCase := range testCases { statefulSet := &workload.StatefulSet{} - stateManager := NewStateManager(testCase.rp, statefulSet, currentPodMap) + stateManager := NewStateManager(map[autoscalingv1alpha1.PodState]int32{ + autoscalingv1alpha1.PodStateOnline: testCase.rp.Spec.OnlineReplicas, + autoscalingv1alpha1.PodStateCutoff: testCase.rp.Spec.CutoffReplicas, + autoscalingv1alpha1.PodStateStandby: testCase.rp.Spec.StandbyReplicas, + }, currentPodMap, statefulSet) stateChange, err := stateManager.CalculateStateChange(context.Background()) assert.Nil(t, err)