projects/gke-optimization/binpacker/api/pkg/domain/model/node/node.go (193 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 node import ( "errors" "fmt" "math" "github.com/golang/glog" ) var ( ErrCPUNegative = errors.New("negative CPU") ErrMemoryNegative = errors.New("negative memory") ErrCPUOverLimit = errors.New("over CPU limit") ErrMemoryOverLimit = errors.New("over memory limit") ErrMemoryOutOfRange = errors.New("memory out of range") ) type CPU float64 type Memory int64 type MemoryInGiB float64 type MemoryInMiB float64 type Node struct { cpuOnGCE CPU memoryOnGCE Memory remainingCPU CPU remainingMemory Memory } const ( MinCPUPerNode = 2.0 // N2 custom MaxCPUPerNode = 80.0 // N2 custom MinMemoryInGiBPerNode = 1.0 // N2 custom MaxMemoryInGiBPerNode = 640.0 // N2 custom MinMemoryPerNode = MinMemoryInGiBPerNode * 1024 * 1024 * 1024 MaxMemoryPerNode = MaxMemoryInGiBPerNode * 1024 * 1024 * 1024 CPUUnit = 2 CPUUsageBySystemResources = 1.0 MemoryUnitInGiB = 0.25 MemoryUsageRatioByKernel = 0.05 MemoryUsageBySystemResources = 900 * 1024 * 1024 EvictionThreshold = 100 * 1024 * 1024 ) // ScheduleCPU assigns cpu for the node and return true if it's scheduled func (n *Node) ScheduleCPU(cpuUsage CPU) bool { if n.remainingCPU >= cpuUsage { n.remainingCPU -= cpuUsage return true } return false } // ScheduleMemory assigns memory for the node and return true if it's scheduled func (n *Node) ScheduleMemory(memoryUsage Memory) bool { if n.remainingMemory >= memoryUsage { n.remainingMemory -= memoryUsage return true } return false } // SchedulableCPU checks whether the specific cpu is assignable func (n *Node) SchedulableCPU(cpuUsage CPU) bool { return n.remainingCPU >= cpuUsage } // SchedulableMemory checks whether the specific memory is assignable func (n *Node) SchedulableMemory(memoryUsage Memory) bool { return n.remainingMemory >= memoryUsage } // RoundCPU rounds cpu with 3 decimal places as it is defined in Kubernetes world func RoundCPU(cpu CPU) CPU { return CPU(math.Round(float64(cpu)*1000) / 1000) } // GiBToBytes transforms the memory size in GiB to byte func GiBToBytes(memoryInGiB MemoryInGiB) Memory { return Memory(memoryInGiB * 1024 * 1024 * 1024) } // MiBToBytes transforms the memory size in MiB to byte func MiBToBytes(memoryInMiB MemoryInMiB) Memory { return Memory(memoryInMiB * 1024 * 1024) } // ByteToGiB transforms the memory size in byte to GiB func ByteToGiB(memory Memory) MemoryInGiB { return MemoryInGiB(memory) / 1024 / 1024 / 1024 } // MinRequeredCPU finds the minimum CPU of a node to assign the cpuUsage func MinRequiredCPU(cpuUsage CPU) CPU { var start int if cpuUsage <= 0 { start = MinCPUPerNode } else { start = int(math.Ceil(float64(cpuUsage))) if start%2 != 0 { start++ } } for cpu := start; cpu <= MaxCPUPerNode; cpu += CPUUnit { // Error won't happen here since "cpu" is normalized node, err := newNode(CPU(cpu), GiBToBytes(MemoryInGiB(cpu))) if err != nil { glog.Errorf("Failed to create a new node with %d cpus and %f GiB memory", cpu, MemoryInGiB(cpu)) } if node.SchedulableCPU(cpuUsage) { return CPU(cpu) } } return MaxCPUPerNode } // CalculateMinMaxMemoryInGiBByCPU calculates the range of the memory size // based on the specified CPU // ref. https://cloud.google.com/compute/docs/general-purpose-machines#custom_machine_types func CalculateMinMaxMemoryInGiBByCPU(cpu CPU) (MemoryInGiB, MemoryInGiB) { minMemory := MemoryInGiB(cpu / 2) if minMemory < MinMemoryInGiBPerNode { minMemory = MinMemoryInGiBPerNode } maxMemory := MemoryInGiB(cpu * 8) if maxMemory > MaxMemoryInGiBPerNode { maxMemory = MaxMemoryInGiBPerNode } return minMemory, maxMemory } // validMemorySizeByCPU checks whether specified CPU and memory size are valid func validMemorySizeByCPU(cpu CPU, memory Memory) bool { memoryInGiB := ByteToGiB(memory) minMemoryInGiB, maxMemoryInGiB := CalculateMinMaxMemoryInGiBByCPU(cpu) return minMemoryInGiB <= memoryInGiB && memoryInGiB <= maxMemoryInGiB } func newNode(cpuOnGCE CPU, memoryOnGCE Memory) (*Node, error) { if cpuOnGCE < MinCPUPerNode { return nil, fmt.Errorf("cpu must be greater than or equal to %f, got %f: %w", MinCPUPerNode, cpuOnGCE, ErrCPUNegative) } if cpuOnGCE > MaxCPUPerNode { return nil, fmt.Errorf("cpu must be less than or equal to %f, got %f: %w", MaxCPUPerNode, cpuOnGCE, ErrCPUOverLimit) } if memoryOnGCE < MinMemoryPerNode { return nil, fmt.Errorf("memory must be greater than or equal to %f, got %d: %w", MinMemoryPerNode, memoryOnGCE, ErrMemoryNegative) } if memoryOnGCE > MaxMemoryPerNode { return nil, fmt.Errorf("memory must be less than or equal to %f, got %d: %w", MaxMemoryPerNode, memoryOnGCE, ErrMemoryOverLimit) } if !validMemorySizeByCPU(cpuOnGCE, memoryOnGCE) { return nil, fmt.Errorf("memory size is out of range based on the number of cpu: %w", ErrMemoryOutOfRange) } remainingCPU := calculateRemainingCPU(cpuOnGCE) remainingMemory := calculateRemainingMemory(memoryOnGCE) return &Node{ cpuOnGCE, memoryOnGCE, remainingCPU, remainingMemory, }, nil } func calculateRemainingCPU(cpuOnGCE CPU) CPU { return RoundCPU(cpuOnGCE - reservedCPU(cpuOnGCE) - CPUUsageBySystemResources) } func calculateRemainingMemory(memoryOnGCE Memory) Memory { return memoryOnGCE - calculateKernelUsageMemory(memoryOnGCE) - reservedMemory(memoryOnGCE) - EvictionThreshold - MemoryUsageBySystemResources } func calculateKernelUsageMemory(memoryOnGCE Memory) Memory { return Memory(float64(memoryOnGCE) * MemoryUsageRatioByKernel) } // reservedCPU calculates the amount of reserved CPU resources // from the amount of CPU resources on Google Compute Engine // ref. https://cloud.google.com/kubernetes-engine/docs/concepts/plan-node-sizes?hl=ja#cpu_reservations func reservedCPU(cpuOnGCE CPU) CPU { var reserved CPU if cpuOnGCE < 1 { return RoundCPU(cpuOnGCE * 0.06) } reserved += 1 * 0.06 if cpuOnGCE < 2 { reserved += (cpuOnGCE - 1) * 0.01 return RoundCPU(reserved) } reserved += 1 * 0.01 if cpuOnGCE < 4 { reserved += (cpuOnGCE - 2) * 0.005 return RoundCPU(reserved) } reserved += 2 * 0.005 reserved += (cpuOnGCE - 4) * 0.0025 return RoundCPU(reserved) } // reservedMemory calculates the amount of reserved memory resources // from the amount of memory resources on Google Compute Engine // ref. https://cloud.google.com/kubernetes-engine/docs/concepts/plan-node-sizes?hl=ja#memory_reservations func reservedMemory(memoryOnGCE Memory) Memory { var reserved Memory if memoryOnGCE < GiBToBytes(1) { return MiBToBytes(255) } if memoryOnGCE < GiBToBytes(4) { return Memory(float64(memoryOnGCE) * 0.25) } reserved += GiBToBytes(1) // 4GiB * 25% if memoryOnGCE < GiBToBytes(8) { reserved += Memory(float64(memoryOnGCE-GiBToBytes(4)) * 0.2) return reserved } reserved += GiBToBytes(4 * 0.2) if memoryOnGCE < GiBToBytes(16) { reserved += Memory(float64(memoryOnGCE-GiBToBytes(8)) * 0.1) return reserved } reserved += GiBToBytes(8 * 0.1) if memoryOnGCE < GiBToBytes(128) { reserved += Memory(float64(memoryOnGCE-GiBToBytes(16)) * 0.06) return reserved } reserved += GiBToBytes(112 * 0.06) reserved += Memory(float64(memoryOnGCE-GiBToBytes(128)) * 0.02) return reserved }