projects/gke-optimization/binpacker/api/pkg/domain/service/binpacking/binpacking.go (178 lines of code) (raw):

// Copyright 2023 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package binpacking import ( "errors" "fmt" "sort" "github.com/GoogleCloudPlatform/cloud-solutions/projects/sa-tools/gke_optimization/binpacker/api/pkg/domain/model/node" "github.com/golang/glog" ) type Solution struct { Recommendation *Recommendation Rationale *RecommendationRationale } type WorkloadMetrics struct { CPURequest node.CPU CPULimit node.CPU MemoryRequest node.Memory MemoryLimit node.Memory } type Recommendation struct { CPU node.CPU MemoryInGiB node.MemoryInGiB NumNodes int } type RecommendationRationale struct { CPU *CPURationale Memory *MemoryRationale } type CPURationale struct { MaxCPULimit node.CPU TotalCPURequest node.CPU NodeSize node.CPU // vCPUs on GCE Reserved node.CPU SystemResourcesUsage node.CPU CapacityUserWorkloads node.CPU } type MemoryRationale struct { MaxMemoryLimit node.Memory TotalMemoryRequest node.Memory NodeSize node.Memory // Memory on GCE KernelUsage node.Memory Reserved node.Memory EvictionThreshold node.Memory SystemResourcesUsage node.Memory CapacityUserWorkloads node.Memory } // ref. https://cloud.google.com/compute/vm-instance-pricing#e2_custommachinetypepricing const ( UnitPricePerCPUPerHour = 0.028026 UnitPricePerMemoryPerHour = 0.003739 ) func (s Recommendation) String() string { return fmt.Sprintf("CPU: %f, Memory: %f, Num: %d", s.CPU, s.MemoryInGiB, s.NumNodes) } func (r RecommendationRationale) String() string { return fmt.Sprintf("MaxCPULimit: %f, TotalCPURequest: %f, CPUReserved: %f, CPUSystemResourcesUsage: %f, CPUUserWorkloads: %f, MemoryMaxLimit: %d, TotalMemoryRequest: %d, MemoryKernelUsage: %d, MemoryReserved: %d, MemoryEvictionThreshold: %d, MemorySystemResourcesUsage: %d, MemoryUserWorkloads: %d", r.CPU.MaxCPULimit, r.CPU.TotalCPURequest, r.CPU.Reserved, r.CPU.SystemResourcesUsage, r.CPU.CapacityUserWorkloads, r.Memory.MaxMemoryLimit, r.Memory.TotalMemoryRequest, r.Memory.KernelUsage, r.Memory.Reserved, r.Memory.EvictionThreshold, r.Memory.SystemResourcesUsage, r.Memory.CapacityUserWorkloads) } // FindRecommendation calculates and finds the most efficient spec // and number of nodes are required to schedule all workloads func FindRecommendation(workloadsMetrics []WorkloadMetrics, minNodes int) (*Recommendation, error) { // logInputs(cpuRequests, cpuLimits, memoryRequests, memoryLimits) cpuRequests := make([]node.CPU, 0, len(workloadsMetrics)) cpuLimits := make([]node.CPU, 0, len(workloadsMetrics)) memoryRequests := make([]node.Memory, 0, len(workloadsMetrics)) memoryLimits := make([]node.Memory, 0, len(workloadsMetrics)) for _, workloadMetrics := range workloadsMetrics { cpuRequests = append(cpuRequests, workloadMetrics.CPURequest) cpuLimits = append(cpuLimits, workloadMetrics.CPULimit) memoryRequests = append(memoryRequests, workloadMetrics.MemoryRequest) memoryLimits = append(memoryLimits, workloadMetrics.MemoryLimit) } maxCPULimit := node.MaxCPU(cpuLimits) recommendations, err := findRecommendationsByCPU(maxCPULimit, cpuRequests, minNodes) if err != nil { return nil, err } glog.Infof("Found recommendations by CPU: %v", recommendations) maxMemoryLimit := node.MaxMemory(memoryLimits) // Find efficient memory size per recommendation calculated based on CPU for i := range recommendations { efficientMemory, err := findEfficientMemorySize(recommendations[i].CPU, maxMemoryLimit, memoryRequests, recommendations[i].NumNodes) if err != nil { continue } recommendations[i].MemoryInGiB = efficientMemory } glog.Infof("Calculated required memory: %v", recommendations) return getBestRecommendation(recommendations), nil } func GetRationale(workloadsMetrics []WorkloadMetrics, recommendation Recommendation) (*RecommendationRationale, error) { var totalCPURequest node.CPU var totalMemoryRequest node.Memory var maxCPULimit node.CPU var maxMemoryLimit node.Memory for _, workloadMetrics := range workloadsMetrics { totalCPURequest += workloadMetrics.CPURequest totalMemoryRequest += workloadMetrics.MemoryRequest if maxCPULimit < workloadMetrics.CPULimit { maxCPULimit = workloadMetrics.CPULimit } if maxMemoryLimit < workloadMetrics.MemoryLimit { maxMemoryLimit = workloadMetrics.MemoryLimit } } nodes, err := node.NewNodes(recommendation.CPU, node.GiBToBytes(recommendation.MemoryInGiB), recommendation.NumNodes) if err != nil { return nil, err } return &RecommendationRationale{ CPU: &CPURationale{ MaxCPULimit: maxCPULimit, TotalCPURequest: node.RoundCPU(totalCPURequest), NodeSize: recommendation.CPU, Reserved: nodes.ReservedCPU(), SystemResourcesUsage: node.CPUUsageBySystemResources, CapacityUserWorkloads: nodes.CPUCapacityForUserWorkloads(), }, Memory: &MemoryRationale{ MaxMemoryLimit: maxMemoryLimit, TotalMemoryRequest: totalMemoryRequest, NodeSize: node.GiBToBytes(recommendation.MemoryInGiB), KernelUsage: nodes.MemoryKernelUsage(), Reserved: nodes.ReservedMemory(), EvictionThreshold: node.EvictionThreshold, SystemResourcesUsage: node.MemoryUsageBySystemResources, CapacityUserWorkloads: nodes.MemoryCapacityForUserWorkloads(), }, }, nil } // getBestRecommendation evaluates and picks the best recommendation from multiple recommendations func getBestRecommendation(recommendations []Recommendation) *Recommendation { sort.SliceStable(recommendations, func(i, j int) bool { return (float64(recommendations[i].CPU*UnitPricePerCPUPerHour)+float64(recommendations[i].MemoryInGiB*UnitPricePerMemoryPerHour))*float64(recommendations[i].NumNodes) < (float64(recommendations[j].CPU*UnitPricePerCPUPerHour)+float64(recommendations[j].MemoryInGiB*UnitPricePerMemoryPerHour))*float64(recommendations[j].NumNodes) }) return &recommendations[0] } // findRecommendationsByCPU finds what specs of nodes and how many nodes are required // to schedule all cpu requests and max limit func findRecommendationsByCPU(maxLimit node.CPU, cpuRequests []node.CPU, minNumNodes int) ([]Recommendation, error) { var totalCPURequest node.CPU for _, v := range cpuRequests { totalCPURequest += v } requiredCPU := totalCPURequest if maxLimit > totalCPURequest { requiredCPU = maxLimit } recommendationMap := make(map[int]node.CPU, 0) for cpu := node.CPU(node.MinCPUPerNode); cpu <= node.MinRequiredCPU(requiredCPU); cpu += node.CPUUnit { // Memory size is not used at this moment nodes, err := node.NewNodesByCPU(cpu, minNumNodes) if err != nil { return nil, err } if !nodes.SchedulableCPU(maxLimit) { continue } // FirstFitDecreasingByCPU returns error if the biggest request is not schedulable numNodes, err := nodes.FirstFitDecreasingByCPU(cpuRequests) if err != nil { continue } // Only put smallest one if _, exists := recommendationMap[numNodes]; exists { continue } recommendationMap[numNodes] = cpu } recommendations := make([]Recommendation, 0) for numNodes, cpu := range recommendationMap { recommendations = append(recommendations, Recommendation{CPU: cpu, MemoryInGiB: 0, NumNodes: numNodes}) } return recommendations, nil } // findEfficentMemorySize finds the minumum memory size to schedule all memory requests // and the max memory limit in a specific number of nodes func findEfficientMemorySize(cpu node.CPU, maxLimit node.Memory, memoryRequests []node.Memory, numNodes int) (node.MemoryInGiB, error) { minMemoryInGiB, maxMemoryInGiB := node.CalculateMinMaxMemoryInGiBByCPU(cpu) for memoryInGiB := minMemoryInGiB; memoryInGiB <= maxMemoryInGiB; memoryInGiB += node.MemoryUnitInGiB { nodes, err := node.NewNodes(cpu, node.GiBToBytes(memoryInGiB), numNodes) if err != nil { return 0, err } if !nodes.SchedulableMemory(maxLimit) { continue } if nodes.CanScheduleAllMemoryUsages(memoryRequests) { return memoryInGiB, nil } } return 0, errors.New("failed to find efficient memory size") }