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

     1  package resource
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"reflect"
     7  	"strings"
     8  	"time"
     9  
    10  	corev1 "k8s.io/api/core/v1"
    11  	"k8s.io/apimachinery/pkg/api/resource"
    12  	recommendermodel "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/recommender/model"
    13  	"k8s.io/klog/v2"
    14  	"sigs.k8s.io/yaml"
    15  
    16  	predictionapi "github.com/gocrane/api/prediction/v1alpha1"
    17  
    18  	"github.com/gocrane/crane/pkg/metricnaming"
    19  	"github.com/gocrane/crane/pkg/oom"
    20  	"github.com/gocrane/crane/pkg/prediction/config"
    21  	"github.com/gocrane/crane/pkg/recommend/types"
    22  	"github.com/gocrane/crane/pkg/recommendation/framework"
    23  	"github.com/gocrane/crane/pkg/utils"
    24  )
    25  
    26  const callerFormat = "ResourceRecommendationCaller-%s-%s"
    27  
    28  type PatchResource struct {
    29  	Spec PatchResourceSpec `json:"spec,omitempty"`
    30  }
    31  
    32  type PatchResourceSpec struct {
    33  	Template PatchResourcePodTemplateSpec `json:"template"`
    34  }
    35  
    36  type PatchResourcePodTemplateSpec struct {
    37  	Spec PatchResourcePodSpec `json:"spec,omitempty"`
    38  }
    39  
    40  type PatchResourcePodSpec struct {
    41  	// +patchMergeKey=name
    42  	// +patchStrategy=merge
    43  	Containers []corev1.Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name"`
    44  }
    45  
    46  func (rr *ResourceRecommender) PreRecommend(ctx *framework.RecommendationContext) error {
    47  	return nil
    48  }
    49  
    50  func (rr *ResourceRecommender) makeCpuConfig() *config.Config {
    51  	return &config.Config{
    52  		Percentile: &predictionapi.Percentile{
    53  			Aggregated:        true,
    54  			HistoryLength:     rr.CpuModelHistoryLength,
    55  			SampleInterval:    rr.CpuSampleInterval,
    56  			MarginFraction:    rr.CpuRequestMarginFraction,
    57  			TargetUtilization: rr.CpuTargetUtilization,
    58  			Percentile:        rr.CpuRequestPercentile,
    59  			Histogram: predictionapi.HistogramConfig{
    60  				HalfLife:   "24h",
    61  				BucketSize: rr.CpuHistogramBucketSize,
    62  				MaxValue:   rr.CpuHistogramMaxValue,
    63  			},
    64  		},
    65  	}
    66  }
    67  
    68  func (rr *ResourceRecommender) makeMemConfig() *config.Config {
    69  	return &config.Config{
    70  		Percentile: &predictionapi.Percentile{
    71  			Aggregated:        true,
    72  			HistoryLength:     rr.MemHistoryLength,
    73  			SampleInterval:    rr.MemSampleInterval,
    74  			MarginFraction:    rr.MemMarginFraction,
    75  			Percentile:        rr.MemPercentile,
    76  			TargetUtilization: rr.MemTargetUtilization,
    77  			Histogram: predictionapi.HistogramConfig{
    78  				HalfLife:   "48h",
    79  				BucketSize: rr.MemHistogramBucketSize,
    80  				MaxValue:   rr.MemHistogramMaxValue,
    81  			},
    82  		},
    83  	}
    84  }
    85  
    86  func (rr *ResourceRecommender) Recommend(ctx *framework.RecommendationContext) error {
    87  	predictor := ctx.PredictorMgr.GetPredictor(predictionapi.AlgorithmTypePercentile)
    88  	if predictor == nil {
    89  		return fmt.Errorf("predictor %v not found", predictionapi.AlgorithmTypePercentile)
    90  	}
    91  
    92  	resourceRecommendation := &types.ResourceRequestRecommendation{}
    93  
    94  	var newContainers []corev1.Container
    95  	var oldContainers []corev1.Container
    96  
    97  	oomRecords, err := ctx.OOMRecorder.GetOOMRecord()
    98  	if err != nil {
    99  		return err
   100  	}
   101  
   102  	namespace := ctx.Object.GetNamespace()
   103  	for _, c := range ctx.Pods[0].Spec.Containers {
   104  		cr := types.ContainerRecommendation{
   105  			ContainerName: c.Name,
   106  			Target:        map[corev1.ResourceName]string{},
   107  		}
   108  
   109  		caller := fmt.Sprintf(callerFormat, klog.KObj(ctx.Recommendation), ctx.Recommendation.UID)
   110  		metricNamer := metricnaming.ResourceToContainerMetricNamer(namespace, ctx.Recommendation.Spec.TargetRef.APIVersion,
   111  			ctx.Recommendation.Spec.TargetRef.Kind, ctx.Recommendation.Spec.TargetRef.Name, c.Name, corev1.ResourceCPU, caller)
   112  		klog.Infof("%s: CPU query for resource request recommendation: %s", ctx.String(), metricNamer.BuildUniqueKey())
   113  		cpuConfig := rr.makeCpuConfig()
   114  		tsList, err := utils.QueryPredictedValuesOnce(ctx.Recommendation, predictor, caller, cpuConfig, metricNamer)
   115  		if err != nil {
   116  			return err
   117  		}
   118  		if len(tsList) < 1 || len(tsList[0].Samples) < 1 {
   119  			return fmt.Errorf("no value retured for queryExpr: %s", metricNamer.BuildUniqueKey())
   120  		}
   121  		v := int64(tsList[0].Samples[0].Value * 1000)
   122  		cpuQuantity := resource.NewMilliQuantity(v, resource.DecimalSI)
   123  		klog.Infof("%s: container %s recommended cpu %s", ctx.String(), c.Name, cpuQuantity.String())
   124  
   125  		metricNamer = metricnaming.ResourceToContainerMetricNamer(namespace, ctx.Recommendation.Spec.TargetRef.APIVersion,
   126  			ctx.Recommendation.Spec.TargetRef.Kind, ctx.Recommendation.Spec.TargetRef.Name, c.Name, corev1.ResourceMemory, caller)
   127  		klog.Infof("%s Memory query for resource request recommendation: %s", ctx.String(), metricNamer.BuildUniqueKey())
   128  		memConfig := rr.makeMemConfig()
   129  		tsList, err = utils.QueryPredictedValuesOnce(ctx.Recommendation, predictor, caller, memConfig, metricNamer)
   130  		if err != nil {
   131  			return err
   132  		}
   133  		if len(tsList) < 1 || len(tsList[0].Samples) < 1 {
   134  			return fmt.Errorf("no value retured for queryExpr: %s", metricNamer.BuildUniqueKey())
   135  		}
   136  		v = int64(tsList[0].Samples[0].Value)
   137  		if v <= 0 {
   138  			return fmt.Errorf("no enough metrics")
   139  		}
   140  		memQuantity := resource.NewQuantity(v, resource.BinarySI)
   141  		klog.Infof("%s: container %s recommended memory %s", ctx.String(), c.Name, memQuantity.String())
   142  
   143  		// Use oom protected memory if exist
   144  		if rr.OOMProtection {
   145  			oomProtectMem := rr.MemoryOOMProtection(oomRecords, namespace, ctx.Object.GetName(), c.Name)
   146  			if oomProtectMem != nil && !oomProtectMem.IsZero() && oomProtectMem.Cmp(*memQuantity) > 0 {
   147  				klog.Infof("%s: container %s using oomProtect Memory %s", ctx.String(), c.Name, oomProtectMem.String())
   148  				memQuantity = oomProtectMem
   149  			}
   150  		}
   151  
   152  		// Resource Specification enabled
   153  		if rr.Specification {
   154  			normalizedCpu, normalizedMem := GetNormalizedResource(cpuQuantity, memQuantity, rr.SpecificationConfigs)
   155  			klog.Infof("GetNormalizedResource currentCpu %s normalizedCpu %s currentMem %s normalizedMem %s", cpuQuantity.String(), normalizedCpu.String(), memQuantity.String(), normalizedMem.String())
   156  			if normalizedCpu.Value() > 0 && normalizedMem.Value() > 0 {
   157  				cpuQuantity = &normalizedCpu
   158  				memQuantity = &normalizedMem
   159  			}
   160  		}
   161  
   162  		cr.Target[corev1.ResourceCPU] = cpuQuantity.String()
   163  		cr.Target[corev1.ResourceMemory] = memQuantity.String()
   164  
   165  		newContainerSpec := corev1.Container{
   166  			Name: c.Name,
   167  			Resources: corev1.ResourceRequirements{
   168  				Requests: corev1.ResourceList{
   169  					corev1.ResourceCPU:    *cpuQuantity,
   170  					corev1.ResourceMemory: *memQuantity,
   171  				},
   172  			},
   173  		}
   174  
   175  		oldContainerSpec := corev1.Container{
   176  			Name: c.Name,
   177  			Resources: corev1.ResourceRequirements{
   178  				Requests: corev1.ResourceList{
   179  					corev1.ResourceCPU:    c.Resources.Requests[corev1.ResourceCPU],
   180  					corev1.ResourceMemory: c.Resources.Requests[corev1.ResourceMemory],
   181  				},
   182  			},
   183  		}
   184  
   185  		newContainers = append(newContainers, newContainerSpec)
   186  		oldContainers = append(oldContainers, oldContainerSpec)
   187  
   188  		resourceRecommendation.Containers = append(resourceRecommendation.Containers, cr)
   189  	}
   190  
   191  	value := types.ProposedRecommendation{
   192  		ResourceRequest: resourceRecommendation,
   193  	}
   194  
   195  	valueBytes, err := yaml.Marshal(value)
   196  	if err != nil {
   197  		return fmt.Errorf("%s yaml marshal failed: %v", rr.Name(), err)
   198  	}
   199  
   200  	ctx.Recommendation.Status.RecommendedValue = string(valueBytes)
   201  
   202  	var newPatch PatchResource
   203  	newPatch.Spec.Template.Spec.Containers = newContainers
   204  	newPatchBytes, err := json.Marshal(newPatch)
   205  	if err != nil {
   206  		return fmt.Errorf("marshal newPatch failed %s. ", err)
   207  	}
   208  
   209  	var oldPatch PatchResource
   210  	oldPatch.Spec.Template.Spec.Containers = oldContainers
   211  	oldPatchBytes, err := json.Marshal(oldPatch)
   212  	if err != nil {
   213  		return fmt.Errorf("marshal oldPatch failed %s. ", err)
   214  	}
   215  
   216  	if reflect.DeepEqual(&newPatch, &oldPatch) {
   217  		ctx.Recommendation.Status.Action = "None"
   218  	} else {
   219  		ctx.Recommendation.Status.Action = "Patch"
   220  	}
   221  
   222  	ctx.Recommendation.Status.RecommendedInfo = string(newPatchBytes)
   223  	ctx.Recommendation.Status.CurrentInfo = string(oldPatchBytes)
   224  
   225  	return nil
   226  }
   227  
   228  // Policy add some logic for result of recommend phase.
   229  func (rr *ResourceRecommender) Policy(ctx *framework.RecommendationContext) error {
   230  	return nil
   231  }
   232  
   233  func (rr *ResourceRecommender) MemoryOOMProtection(oomRecords []oom.OOMRecord, namespace string, workloadName string, containerName string) *resource.Quantity {
   234  	var oomRecord *oom.OOMRecord
   235  	for _, record := range oomRecords {
   236  		// use oomRecord for all pods in workload
   237  		if strings.HasPrefix(record.Pod, workloadName) && containerName == record.Container && namespace == record.Namespace {
   238  			oomRecord = &record
   239  			break
   240  		}
   241  	}
   242  
   243  	// ignore too old oom events
   244  	if oomRecord != nil && time.Since(oomRecord.OOMAt) <= (time.Hour*24*7) {
   245  		memoryOOM := oomRecord.Memory.Value()
   246  		var memoryNeeded recommendermodel.ResourceAmount
   247  
   248  		memoryNeeded = recommendermodel.ResourceAmountMax(recommendermodel.ResourceAmount(memoryOOM)+recommendermodel.MemoryAmountFromBytes(recommendermodel.OOMMinBumpUp),
   249  			recommendermodel.ScaleResource(recommendermodel.ResourceAmount(memoryOOM), rr.OOMBumpRatio))
   250  
   251  		return resource.NewQuantity(int64(memoryNeeded), resource.BinarySI)
   252  	}
   253  
   254  	return nil
   255  }