cluster-autoscaler/cloudprovider/gce/gce_price_model.go (180 lines of code) (raw):

/* Copyright 2016 The Kubernetes Authors. 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 gce import ( "math" "strconv" "time" apiv1 "k8s.io/api/core/v1" "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/gce/localssdsize" "k8s.io/autoscaler/cluster-autoscaler/utils/gpu" "k8s.io/autoscaler/cluster-autoscaler/utils/units" klog "k8s.io/klog/v2" ) // GcePriceModel implements PriceModel interface for GCE. type GcePriceModel struct { PriceInfo PriceInfo localSSDSizeProvider localssdsize.LocalSSDSizeProvider } // NewGcePriceModel gets a new instance of GcePriceModel func NewGcePriceModel(info PriceInfo, localSSDSizeProvider localssdsize.LocalSSDSizeProvider) *GcePriceModel { return &GcePriceModel{ PriceInfo: info, localSSDSizeProvider: localSSDSizeProvider, } } const ( preemptibleLabel = "cloud.google.com/gke-preemptible" spotLabel = "cloud.google.com/gke-spot" ephemeralStorageLocalSsdLabel = "cloud.google.com/gke-ephemeral-storage-local-ssd" bootDiskTypeLabel = "cloud.google.com/gke-boot-disk" ) // DefaultBootDiskSizeGB is 100 GB. const DefaultBootDiskSizeGB = 100 // NodePrice returns a price of running the given node for a given period of time. // All prices are in USD. func (model *GcePriceModel) NodePrice(node *apiv1.Node, startTime time.Time, endTime time.Time) (float64, error) { price := 0.0 basePriceFound := false machineType := "" if node.Labels != nil { if _machineType, found := getInstanceTypeFromLabels(node.Labels); found { machineType = _machineType } } // Base instance price priceMapToUse := model.PriceInfo.InstancePrices() if hasPreemptiblePricing(node) { priceMapToUse = model.PriceInfo.PreemptibleInstancePrices() } if basePricePerHour, found := priceMapToUse[machineType]; found { price = basePricePerHour * getHours(startTime, endTime) basePriceFound = true } else { klog.Warningf("Pricing information not found for instance type %v; will fallback to default pricing", machineType) } if !basePriceFound { price = model.getBasePrice(node.Status.Capacity, machineType, startTime, endTime) price = price * model.getPreemptibleDiscount(node) } // Ephemeral Storage // Local SSD price if node.Labels[ephemeralStorageLocalSsdLabel] == "true" || node.Annotations[EphemeralStorageLocalSsdAnnotation] == "true" { localSsdCount, _ := strconv.ParseFloat(node.Annotations[LocalSsdCountAnnotation], 64) localSsdPrice := model.PriceInfo.LocalSsdPricePerHour() if hasPreemptiblePricing(node) { localSsdPrice = model.PriceInfo.SpotLocalSsdPricePerHour() } price += localSsdCount * float64(model.localSSDSizeProvider.SSDSizeInGiB(machineType)) * localSsdPrice * getHours(startTime, endTime) } // Boot disk price bootDiskSize, _ := strconv.ParseInt(node.Annotations[BootDiskSizeAnnotation], 10, 64) if bootDiskSize == 0 { klog.V(5).Infof("Boot disk size is not found for node %s, using default size %v", node.Name, DefaultBootDiskSizeGB) bootDiskSize = DefaultBootDiskSizeGB } bootDiskType := node.Annotations[BootDiskTypeAnnotation] if val, ok := node.Labels[bootDiskTypeLabel]; ok { bootDiskType = val } if bootDiskType == "" { klog.V(5).Infof("Boot disk type is not found for node %s, using default type %s", node.Name, DefaultBootDiskType) bootDiskType = DefaultBootDiskType } bootDiskPrice := model.PriceInfo.BootDiskPricePerHour()[bootDiskType] price += bootDiskPrice * float64(bootDiskSize) * getHours(startTime, endTime) // GPUs if gpuRequest, found := node.Status.Capacity[gpu.ResourceNvidiaGPU]; found { gpuPrice := model.PriceInfo.BaseGpuPricePerHour() if node.Labels != nil { priceMapToUse := model.PriceInfo.GpuPrices() if hasPreemptiblePricing(node) { priceMapToUse = model.PriceInfo.PreemptibleGpuPrices() } if gpuType, found := node.Labels[GPULabel]; found { if _, found := priceMapToUse[gpuType]; found { gpuPrice = priceMapToUse[gpuType] } else { klog.Warningf("Pricing information not found for GPU type %v; will fallback to default pricing", gpuType) } } } price += float64(gpuRequest.MilliValue()) / 1000.0 * gpuPrice * getHours(startTime, endTime) } return price, nil } func (model *GcePriceModel) getPreemptibleDiscount(node *apiv1.Node) float64 { if !hasPreemptiblePricing(node) { return 1.0 } instanceType, found := getInstanceTypeFromLabels(node.Labels) if !found { return 1.0 } instanceFamily, _ := GetMachineFamily(instanceType) discountMap := model.PriceInfo.PredefinedPreemptibleDiscount() if IsCustomMachine(instanceType) { discountMap = model.PriceInfo.CustomPreemptibleDiscount() } if _, found := discountMap[instanceFamily]; found { return discountMap[instanceFamily] } return preemptibleDiscount } // PodPrice returns a theoretical minimum price of running a pod for a given // period of time on a perfectly matching machine. func (model *GcePriceModel) PodPrice(pod *apiv1.Pod, startTime time.Time, endTime time.Time) (float64, error) { price := 0.0 for _, container := range pod.Spec.Containers { price += model.getBasePrice(container.Resources.Requests, "", startTime, endTime) price += model.getAdditionalPrice(container.Resources.Requests, startTime, endTime) } return price, nil } func (model *GcePriceModel) getBasePrice(resources apiv1.ResourceList, instanceType string, startTime time.Time, endTime time.Time) float64 { if len(resources) == 0 { return 0 } hours := getHours(startTime, endTime) instanceFamily, _ := GetMachineFamily(instanceType) isCustom := IsCustomMachine(instanceType) price := 0.0 cpu := resources[apiv1.ResourceCPU] cpuPrice := model.PriceInfo.BaseCpuPricePerHour() cpuPriceMap := model.PriceInfo.PredefinedCpuPricePerHour() if isCustom { cpuPriceMap = model.PriceInfo.CustomCpuPricePerHour() } if _, found := cpuPriceMap[instanceFamily]; found { cpuPrice = cpuPriceMap[instanceFamily] } price += float64(cpu.MilliValue()) / 1000.0 * cpuPrice * hours mem := resources[apiv1.ResourceMemory] memPrice := model.PriceInfo.BaseMemoryPricePerHourPerGb() memPriceMap := model.PriceInfo.PredefinedMemoryPricePerHourPerGb() if isCustom { memPriceMap = model.PriceInfo.CustomMemoryPricePerHourPerGb() } if _, found := memPriceMap[instanceFamily]; found { memPrice = memPriceMap[instanceFamily] } price += float64(mem.Value()) / float64(units.GiB) * memPrice * hours ephemeralStorage := resources[apiv1.ResourceEphemeralStorage] // For simplification using a fixed price for default boot disk. ephemeralStoragePrice := model.PriceInfo.BootDiskPricePerHour()[DefaultBootDiskType] price += float64(ephemeralStorage.Value()) / float64(units.GiB) * ephemeralStoragePrice * hours return price } func (model *GcePriceModel) getAdditionalPrice(resources apiv1.ResourceList, startTime time.Time, endTime time.Time) float64 { if len(resources) == 0 { return 0 } hours := getHours(startTime, endTime) price := 0.0 gpu := resources[gpu.ResourceNvidiaGPU] price += float64(gpu.MilliValue()) / 1000.0 * model.PriceInfo.BaseGpuPricePerHour() * hours return price } func getHours(startTime time.Time, endTime time.Time) float64 { minutes := math.Ceil(float64(endTime.Sub(startTime)) / float64(time.Minute)) hours := minutes / 60.0 return hours } // hasPreemptiblePricing returns whether we should use preemptible pricing for a node, based on labels. Spot VMs have // dynamic pricing, which is different than the static pricing for Preemptible VMs we use here. However it should be close // enough in practice and we really only look at prices in comparison with each other. Spot VMs will always be cheaper // than corresponding non-preemptible VMs. So for the purposes of pricing, Spot VMs are treated the same as // Preemptible VMs. func hasPreemptiblePricing(node *apiv1.Node) bool { if node.Labels == nil { return false } return node.Labels[preemptibleLabel] == "true" || node.Labels[spotLabel] == "true" } func getInstanceTypeFromLabels(labels map[string]string) (string, bool) { machineType, found := labels[apiv1.LabelInstanceTypeStable] if !found { machineType, found = labels[apiv1.LabelInstanceType] } return machineType, found }