github.com/gocrane/crane@v0.11.0/pkg/recommendation/recommender/replicas/recommend.go (about)

     1  package replicas
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"math"
     7  	"time"
     8  
     9  	"github.com/montanaflynn/stats"
    10  	corev1 "k8s.io/api/core/v1"
    11  	"k8s.io/klog/v2"
    12  	"sigs.k8s.io/yaml"
    13  
    14  	predictionapi "github.com/gocrane/api/prediction/v1alpha1"
    15  
    16  	"github.com/gocrane/crane/pkg/prediction/config"
    17  	"github.com/gocrane/crane/pkg/recommend/types"
    18  	"github.com/gocrane/crane/pkg/recommendation/framework"
    19  	"github.com/gocrane/crane/pkg/utils"
    20  )
    21  
    22  type PatchReplicas struct {
    23  	Spec PatchReplicasSpec `json:"spec,omitempty"`
    24  }
    25  
    26  type PatchReplicasSpec struct {
    27  	Replicas *int32 `json:"replicas,omitempty"`
    28  }
    29  
    30  func (rr *ReplicasRecommender) PreRecommend(ctx *framework.RecommendationContext) error {
    31  	// we load algorithm config in this phase
    32  	// TODO(chrisydxie) support configuration
    33  	config := &config.Config{
    34  		DSP: &predictionapi.DSP{
    35  			SampleInterval: "1m",
    36  			HistoryLength:  "7d",
    37  			Estimators:     predictionapi.Estimators{},
    38  		},
    39  	}
    40  	ctx.AlgorithmConfig = config
    41  	return nil
    42  }
    43  
    44  func (rr *ReplicasRecommender) Recommend(ctx *framework.RecommendationContext) error {
    45  	p := ctx.PredictorMgr.GetPredictor(predictionapi.AlgorithmTypeDSP)
    46  	timeNow := time.Now()
    47  	caller := fmt.Sprintf(rr.Name(), klog.KObj(ctx.RecommendationRule), ctx.RecommendationRule.UID)
    48  
    49  	// get workload cpu usage
    50  	tsListPrediction, err := utils.QueryPredictedTimeSeriesOnce(p, caller,
    51  		ctx.AlgorithmConfig,
    52  		ctx.MetricNamer,
    53  		timeNow,
    54  		timeNow.Add(time.Hour*24*7))
    55  
    56  	if err != nil {
    57  		klog.Warningf("%s: query predicted time series failed: %v ", ctx.String(), err)
    58  	}
    59  
    60  	if len(tsListPrediction) != 1 {
    61  		klog.Warningf("%s: prediction metrics data is unexpected, List length is %d ", ctx.String(), len(tsListPrediction))
    62  	}
    63  
    64  	ctx.ResultValues = tsListPrediction
    65  	return nil
    66  }
    67  
    68  // Policy add some logic for result of recommend phase.
    69  func (rr *ReplicasRecommender) Policy(ctx *framework.RecommendationContext) error {
    70  	minReplicas, _, _, err := rr.GetMinReplicas(ctx)
    71  	if err != nil {
    72  		return err
    73  	}
    74  
    75  	replicasRecommendation := &types.ReplicasRecommendation{
    76  		Replicas: &minReplicas,
    77  	}
    78  
    79  	result := types.ProposedRecommendation{
    80  		ReplicasRecommendation: replicasRecommendation,
    81  	}
    82  
    83  	resultBytes, err := yaml.Marshal(result)
    84  	if err != nil {
    85  		return fmt.Errorf("%s proposeMinReplicas failed: %v", rr.Name(), err)
    86  	}
    87  
    88  	ctx.Recommendation.Status.RecommendedValue = string(resultBytes)
    89  
    90  	var newPatch PatchReplicas
    91  	newPatch.Spec.Replicas = &minReplicas
    92  	newPatchBytes, err := json.Marshal(newPatch)
    93  	if err != nil {
    94  		return fmt.Errorf("marshal newPatch failed %s. ", err)
    95  	}
    96  
    97  	var oldPatch PatchReplicas
    98  	oldPatch.Spec.Replicas = &ctx.Scale.Spec.Replicas
    99  	oldPatchBytes, err := json.Marshal(oldPatch)
   100  	if err != nil {
   101  		return fmt.Errorf("marshal oldPatch failed %s. ", err)
   102  	}
   103  
   104  	ctx.Recommendation.Status.RecommendedInfo = string(newPatchBytes)
   105  	ctx.Recommendation.Status.CurrentInfo = string(oldPatchBytes)
   106  	ctx.Recommendation.Status.Action = "Patch"
   107  
   108  	return nil
   109  }
   110  
   111  // ProposeMinReplicas calculate min replicas based on default-min-replicas
   112  func (rr *ReplicasRecommender) ProposeMinReplicas(resourceUsage float64, requestTotal int64, targetUtilization float64) (int32, error) {
   113  	minReplicas := int32(rr.DefaultMinReplicas)
   114  
   115  	// minReplicas should be larger than 0
   116  	if minReplicas < 1 {
   117  		minReplicas = 1
   118  	}
   119  
   120  	min := int32(math.Ceil(resourceUsage / (targetUtilization * float64(requestTotal) / 1000.)))
   121  	if min > minReplicas {
   122  		minReplicas = min
   123  	}
   124  
   125  	return minReplicas, nil
   126  }
   127  
   128  func (rr *ReplicasRecommender) GetMinReplicas(ctx *framework.RecommendationContext) (int32, float64, float64, error) {
   129  	var cpuUsages []float64
   130  	var cpuMax float64
   131  	// combine values from historic and prediction
   132  	for _, sample := range ctx.InputValue(string(corev1.ResourceCPU))[0].Samples {
   133  		cpuUsages = append(cpuUsages, sample.Value)
   134  		if sample.Value > cpuMax {
   135  			cpuMax = sample.Value
   136  		}
   137  	}
   138  
   139  	if len(ctx.ResultValues) >= 1 {
   140  		for _, sample := range ctx.ResultValues[0].Samples {
   141  			cpuUsages = append(cpuUsages, sample.Value)
   142  			if sample.Value > cpuMax {
   143  				cpuMax = sample.Value
   144  			}
   145  		}
   146  	}
   147  
   148  	// apply policy for predicted values
   149  	percentileCpu, err := stats.Percentile(cpuUsages, rr.CpuPercentile)
   150  	if err != nil {
   151  		return 0, 0, 0, fmt.Errorf("%s get percentileCpu failed: %v", rr.Name(), err)
   152  	}
   153  
   154  	requestTotalCpu, err := utils.CalculatePodTemplateRequests(&ctx.PodTemplate, corev1.ResourceCPU)
   155  	if err != nil {
   156  		return 0, 0, 0, fmt.Errorf("%s CalculatePodTemplateRequests cpu failed: %v", rr.Name(), err)
   157  	}
   158  
   159  	klog.Infof("%s: WorkloadCpuUsage Percentile %f PodCpuRequest %d CPUTargetUtilization %f", ctx.String(), percentileCpu, requestTotalCpu, rr.CPUTargetUtilization)
   160  	minReplicasCpu, err := rr.ProposeMinReplicas(percentileCpu, requestTotalCpu, rr.CPUTargetUtilization)
   161  	if err != nil {
   162  		return 0, 0, 0, fmt.Errorf("%s proposeMinReplicas for cpu failed: %v", rr.Name(), err)
   163  	}
   164  
   165  	var memUsages []float64
   166  	// combine values from historic and prediction
   167  	for _, sample := range ctx.InputValue(string(corev1.ResourceMemory))[0].Samples {
   168  		memUsages = append(memUsages, sample.Value)
   169  	}
   170  
   171  	percentileMem, err := stats.Percentile(memUsages, rr.MemPercentile)
   172  	if err != nil {
   173  		return 0, 0, 0, fmt.Errorf("%s get percentileMem failed: %v", rr.Name(), err)
   174  	}
   175  
   176  	requestTotalMem, err := utils.CalculatePodTemplateRequests(&ctx.PodTemplate, corev1.ResourceMemory)
   177  	if err != nil {
   178  		return 0, 0, 0, fmt.Errorf("%s CalculatePodTemplateRequests failed: %v", rr.Name(), err)
   179  	}
   180  
   181  	klog.Infof("%s: WorkloadMemoryUsage Percentile %f PodMemoryRequest %f MemTargetUtilization %f", ctx.String(), percentileMem, float64(requestTotalMem)/1000, rr.MemTargetUtilization)
   182  	minReplicasMem, err := rr.ProposeMinReplicas(percentileMem, requestTotalMem, rr.MemTargetUtilization)
   183  	if err != nil {
   184  		return 0, 0, 0, fmt.Errorf("%s proposeMinReplicas for cpu failed: %v", rr.Name(), err)
   185  	}
   186  
   187  	if minReplicasMem > minReplicasCpu {
   188  		minReplicasCpu = minReplicasMem
   189  	}
   190  
   191  	return minReplicasCpu, cpuMax, percentileCpu, nil
   192  }