pkg/trimaran/loadvariationriskbalancing/analysis.go (102 lines of code) (raw):
/*
Copyright 2021 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 loadvariationriskbalancing
import (
"math"
"github.com/paypal/load-watcher/pkg/watcher"
v1 "k8s.io/api/core/v1"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/scheduler/framework"
)
/*
Calculation of risk score for resources given measured data
*/
// resourceStats : statistics data for a resource
type resourceStats struct {
// average used (absolute)
usedAvg float64
// standard deviation used (absolute)
usedStdev float64
// req of pod
req float64
// node capacity
capacity float64
}
// computeScore : compute score given usage statistics
// - risk = [ average + margin * stDev^{1/sensitivity} ] / 2
// - score = ( 1 - risk ) * maxScore
func (rs *resourceStats) computeScore(margin float64, sensitivity float64) float64 {
if rs.capacity <= 0 {
klog.Errorf("invalid resource capacity %f!", rs.capacity)
return 0
}
// make sure values are within bounds
rs.req = math.Max(rs.req, 0)
rs.usedAvg = math.Max(math.Min(rs.usedAvg, rs.capacity), 0)
rs.usedStdev = math.Max(math.Min(rs.usedStdev, rs.capacity), 0)
// calculate average factor
mu := (rs.usedAvg + rs.req) / rs.capacity
mu = math.Max(math.Min(mu, 1), 0)
// calculate deviation factor
sigma := rs.usedStdev / rs.capacity
sigma = math.Max(math.Min(sigma, 1), 0)
// apply root power
if sensitivity >= 0 {
sigma = math.Pow(sigma, 1/sensitivity)
}
// apply multiplier
sigma *= margin
sigma = math.Max(math.Min(sigma, 1), 0)
// evaluate overall risk factor
risk := (mu + sigma) / 2
klog.V(6).Infof("mu=%f; sigma=%f; margin=%f; sensitivity=%f; risk=%f", mu, sigma, margin, sensitivity, risk)
return (1. - risk) * float64(framework.MaxNodeScore)
}
// createResourceStats : get resource statistics data from measurements for a node
func createResourceStats(metrics []watcher.Metric, node *v1.Node, podRequest *framework.Resource,
resourceName v1.ResourceName, watcherType string) (rs *resourceStats, isValid bool) {
// get resource usage statistics
nodeUtil, nodeStd, metricFound := getResourceData(metrics, watcherType)
if !metricFound {
klog.V(6).Infof("resource %s usage statistics for node %s: no valid data", watcherType, node.GetName())
return nil, false
}
// get resource capacity
rs = &resourceStats{}
allocatableResources := node.Status.Allocatable
am := allocatableResources[resourceName]
if resourceName == v1.ResourceCPU {
rs.capacity = float64(am.MilliValue())
rs.req = float64(podRequest.MilliCPU)
} else {
rs.capacity = float64(am.Value())
var megaFactor = float64(1. / 1024. / 1024.)
rs.capacity *= megaFactor
rs.req = float64(podRequest.Memory) * megaFactor
}
// calculate absolute usage statistics
rs.usedAvg = nodeUtil * rs.capacity / 100
rs.usedStdev = nodeStd * rs.capacity / 100
klog.V(6).Infof("resource %s usage statistics for node %s: capacity=%f; req=%f; usedAvg=%f; usedStdev=%f",
watcherType, node.GetName(), rs.capacity, rs.req, rs.usedAvg, rs.usedStdev)
return rs, true
}
// getResourceData : get data from measurements for a given resource type
func getResourceData(metrics []watcher.Metric, resourceType string) (avg float64, stDev float64, isValid bool) {
// for backward compatibility of LoadWatcher:
// average data metric without operator specified
avgFound := false
for _, metric := range metrics {
if metric.Type == resourceType {
if metric.Operator == watcher.Average {
avg = metric.Value
avgFound = true
} else if metric.Operator == watcher.Std {
stDev = metric.Value
} else if (metric.Operator == "" || metric.Operator == watcher.Latest) && !avgFound {
avg = metric.Value
}
isValid = true
}
}
return avg, stDev, isValid
}
// getResourceRequested : calculate the resource demand of a pod (CPU and Memory)
func getResourceRequested(pod *v1.Pod) *framework.Resource {
// add up demand of all containers
result := &framework.Resource{}
for _, container := range pod.Spec.Containers {
result.Add(container.Resources.Requests)
}
// take max_resource(sum_pod, any_init_container)
for _, container := range pod.Spec.InitContainers {
for rName, rQuantity := range container.Resources.Requests {
switch rName {
case v1.ResourceCPU:
if cpu := rQuantity.MilliValue(); cpu > result.MilliCPU {
result.MilliCPU = cpu
}
case v1.ResourceMemory:
if mem := rQuantity.Value(); mem > result.Memory {
result.Memory = mem
}
default:
}
}
}
// add any pod overhead
if pod.Spec.Overhead != nil {
result.Add(pod.Spec.Overhead)
}
return result
}