diff --git a/deploy/craned/deployment.yaml b/deploy/craned/deployment.yaml index c93a32079..8fec48a0b 100644 --- a/deploy/craned/deployment.yaml +++ b/deploy/craned/deployment.yaml @@ -94,7 +94,7 @@ data: ehpa.default-min-replicas: "2" ehpa.max-replicas-factor: "3" ehpa.min-cpu-usage-threshold: "10" - ehpa.fluctuation-threshold: "3" + ehpa.fluctuation-threshold: "1.5" ehpa.min-cpu-target-utilization: "30" ehpa.max-cpu-target-utilization: "75" ehpa.reference-hpa: "true" \ No newline at end of file diff --git a/pkg/controller/recommendation/recommendation_controller.go b/pkg/controller/recommendation/recommendation_controller.go index 2d4562d4a..2f3ccbb69 100644 --- a/pkg/controller/recommendation/recommendation_controller.go +++ b/pkg/controller/recommendation/recommendation_controller.go @@ -96,7 +96,7 @@ func (c *Controller) ShouldRecommend(recommendation *analysisv1alph1.Recommendat } func (c *Controller) DoRecommend(ctx context.Context, recommendation *analysisv1alph1.Recommendation) { - klog.V(4).Info("Starting to process Recommendation %s", klog.KObj(recommendation)) + klog.V(4).Infof("Starting to process Recommendation %s", klog.KObj(recommendation)) newStatus := recommendation.Status.DeepCopy() diff --git a/pkg/controller/recommendation/updater.go b/pkg/controller/recommendation/updater.go index c2248d6b9..03507b1c8 100644 --- a/pkg/controller/recommendation/updater.go +++ b/pkg/controller/recommendation/updater.go @@ -56,7 +56,13 @@ func (c *Controller) UpdateRecommendation(ctx context.Context, recommendation *a annotation = map[string]string{} } - annotation[known.RecommendationValueAnnotation] = value + switch recommendation.Spec.Type { + case analysisapi.AnalysisTypeResource: + annotation[known.ResourceRecommendationValueAnnotation] = value + case analysisapi.AnalysisTypeHPA: + annotation[known.HPARecommendationValueAnnotation] = value + } + unstructed.SetAnnotations(annotation) err = c.Client.Update(ctx, unstructed) if err != nil { diff --git a/pkg/known/annotation.go b/pkg/known/annotation.go index 821d0460d..cc0ee510f 100644 --- a/pkg/known/annotation.go +++ b/pkg/known/annotation.go @@ -1,5 +1,6 @@ package known const ( - RecommendationValueAnnotation = "analysis.crane.io/recommendation-value" + HPARecommendationValueAnnotation = "analysis.crane.io/hpa-recommendation" + ResourceRecommendationValueAnnotation = "analysis.crane.io/resource-recommendation" ) diff --git a/pkg/recommend/advisor/ehpa.go b/pkg/recommend/advisor/ehpa.go index db3df4f18..0fe211668 100644 --- a/pkg/recommend/advisor/ehpa.go +++ b/pkg/recommend/advisor/ehpa.go @@ -82,21 +82,26 @@ func (a *EHPAAdvisor) Advise(proposed *types.ProposedRecommendation) error { return fmt.Errorf("EHPAAdvisor checkMinCpuUsageThreshold failed: %v", err) } - err = a.checkFluctuation(tsListPrediction) + medianMin, medianMax, err := a.minMaxMedians(tsListPrediction) if err != nil { - return fmt.Errorf("EHPAAdvisor checkFluctuation failed: %v", err) + return fmt.Errorf("EHPAAdvisor minMaxMedians failed: %v", err) } - minReplicas, err := a.proposeMinReplicas() + err = a.checkFluctuation(medianMin, medianMax) if err != nil { - return fmt.Errorf("EHPAAdvisor proposeMinReplicas failed: %v", err) + return fmt.Errorf("EHPAAdvisor checkFluctuation failed: %v", err) } - targetUtilization, err := a.proposeTargetUtilization() + targetUtilization, requestTotal, err := a.proposeTargetUtilization() if err != nil { return fmt.Errorf("EHPAAdvisor proposeTargetUtilization failed: %v", err) } + minReplicas, err := a.proposeMinReplicas(medianMin, requestTotal) + if err != nil { + return fmt.Errorf("EHPAAdvisor proposeMinReplicas failed: %v", err) + } + maxReplicas, err := a.proposeMaxReplicas(cpuUsages, targetUtilization, minReplicas) if err != nil { return fmt.Errorf("EHPAAdvisor proposeMaxReplicas failed: %v", err) @@ -167,13 +172,7 @@ func (a *EHPAAdvisor) checkMinCpuUsageThreshold(cpuMax float64) error { return nil } -// checkFluctuation check if the time series fluctuation is reach to ehpa.fluctuation-threshold -func (a *EHPAAdvisor) checkFluctuation(predictionTs []*common.TimeSeries) error { - fluctuationThreshold, err := strconv.ParseFloat(a.Context.ConfigProperties["ehpa.fluctuation-threshold"], 64) - if err != nil { - return err - } - +func (a *EHPAAdvisor) minMaxMedians(predictionTs []*common.TimeSeries) (float64, float64, error) { // aggregate with time's hour cpuUsagePredictionMap := make(map[int][]float64) for _, sample := range predictionTs[0].Samples { @@ -192,7 +191,7 @@ func (a *EHPAAdvisor) checkFluctuation(predictionTs []*common.TimeSeries) error for _, usageInHour := range cpuUsagePredictionMap { medianUsage, err := stats.Median(usageInHour) if err != nil { - return err + return 0., 0., err } medianUsages = append(medianUsages, medianUsage) } @@ -208,12 +207,22 @@ func (a *EHPAAdvisor) checkFluctuation(predictionTs []*common.TimeSeries) error medianMin = value } } + klog.V(4).Infof("EHPAAdvisor minMaxMedians medianMax %f, medianMin %f, medianUsages %v", medianMax, medianMin, medianUsages) + + return medianMin, medianMax, nil +} + +// checkFluctuation check if the time series fluctuation is reach to ehpa.fluctuation-threshold +func (a *EHPAAdvisor) checkFluctuation(medianMin, medianMax float64) error { + fluctuationThreshold, err := strconv.ParseFloat(a.Context.ConfigProperties["ehpa.fluctuation-threshold"], 64) + if err != nil { + return err + } if medianMin == 0 { return fmt.Errorf("Mean cpu usage is zero. ") } - klog.V(4).Infof("EHPAAdvisor checkFluctuation, medianMax %f medianMin %f medianUsages %v", medianMax, medianMin, medianUsages) fluctuation := medianMax / medianMin if fluctuation < fluctuationThreshold { return fmt.Errorf("Target cpu fluctuation %f is under ehpa.fluctuation-threshold %f. ", fluctuation, fluctuationThreshold) @@ -225,15 +234,15 @@ func (a *EHPAAdvisor) checkFluctuation(predictionTs []*common.TimeSeries) error // proposeTargetUtilization use the 99 percentile cpu usage to propose target utilization, // since we think if pod have reach the top usage before, maybe this is a suitable target to running. // Considering too high or too low utilization are both invalid, we will be capping target utilization finally. -func (a *EHPAAdvisor) proposeTargetUtilization() (int32, error) { +func (a *EHPAAdvisor) proposeTargetUtilization() (int32, int64, error) { minCpuTargetUtilization, err := strconv.ParseInt(a.Context.ConfigProperties["ehpa.min-cpu-target-utilization"], 10, 32) if err != nil { - return 0, err + return 0, 0, err } maxCpuTargetUtilization, err := strconv.ParseInt(a.Context.ConfigProperties["ehpa.max-cpu-target-utilization"], 10, 32) if err != nil { - return 0, err + return 0, 0, err } percentilePredictor := a.Predictors[predictionapi.AlgorithmTypePercentile] @@ -249,21 +258,21 @@ func (a *EHPAAdvisor) proposeTargetUtilization() (int32, error) { cpuConfig, queryExpr) if err != nil { - return 0, err + return 0, 0, err } if len(tsList) < 1 || len(tsList[0].Samples) < 1 { - return 0, fmt.Errorf("no value retured for queryExpr: %s", queryExpr) + return 0, 0, fmt.Errorf("no value retured for queryExpr: %s", queryExpr) } cpuUsage += tsList[0].Samples[0].Value } - requestsPod, err := utils.CalculatePodTemplateRequests(a.PodTemplate, corev1.ResourceCPU) + requestTotal, err := utils.CalculatePodTemplateRequests(a.PodTemplate, corev1.ResourceCPU) if err != nil { - return 0, err + return 0, 0, err } - klog.V(4).Infof("EHPAAdvisor propose targetUtilization, cpuUsage %f requestsPod %d", cpuUsage, requestsPod) - targetUtilization := int32(math.Ceil((cpuUsage * 1000 / float64(requestsPod)) * 100)) + klog.V(4).Infof("EHPAAdvisor propose targetUtilization, cpuUsage %f requestsPod %d", cpuUsage, requestTotal) + targetUtilization := int32(math.Ceil((cpuUsage * 1000 / float64(requestTotal)) * 100)) // capping if targetUtilization < int32(minCpuTargetUtilization) { @@ -275,16 +284,21 @@ func (a *EHPAAdvisor) proposeTargetUtilization() (int32, error) { targetUtilization = int32(maxCpuTargetUtilization) } - return targetUtilization, nil + return targetUtilization, requestTotal, nil } // proposeMinReplicas calculate min replicas based on ehpa.default-min-replicas -func (a *EHPAAdvisor) proposeMinReplicas() (int32, error) { +func (a *EHPAAdvisor) proposeMinReplicas(medianMin float64, requestTotal int64) (int32, error) { defaultMinReplicas, err := strconv.ParseInt(a.Context.ConfigProperties["ehpa.default-min-replicas"], 10, 32) if err != nil { return 0, err } + maxCpuTargetUtilization, err := strconv.ParseInt(a.Context.ConfigProperties["ehpa.max-cpu-target-utilization"], 10, 32) + if err != nil { + return 0, err + } + minReplicas := int32(defaultMinReplicas) // minReplicas should be larger than 0 @@ -292,6 +306,11 @@ func (a *EHPAAdvisor) proposeMinReplicas() (int32, error) { minReplicas = 1 } + min := int32(math.Ceil(medianMin / (float64(maxCpuTargetUtilization) / 100. * float64(requestTotal) / 1000.))) + if min > minReplicas { + minReplicas = min + } + return minReplicas, nil } diff --git a/pkg/recommend/advisor/ehpa_test.go b/pkg/recommend/advisor/ehpa_test.go index 12045448c..8d3861013 100644 --- a/pkg/recommend/advisor/ehpa_test.go +++ b/pkg/recommend/advisor/ehpa_test.go @@ -50,9 +50,11 @@ func TestCheckFluctuation(t *testing.T) { }, } + medianMin, medianMax, _ := a.minMaxMedians(tsList) + for _, test := range tests { a.Context.ConfigProperties["ehpa.fluctuation-threshold"] = test.threshold - err := a.checkFluctuation(tsList) + err := a.checkFluctuation(medianMin, medianMax) if err != nil && !test.expectError { t.Errorf("Failed to checkFluctuation: %v", err) }