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 }