github.com/kubewharf/katalyst-core@v0.5.3/pkg/agent/qrm-plugins/util/util.go (about) 1 /* 2 Copyright 2022 The Katalyst Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package util 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "io/ioutil" 23 "math" 24 "sort" 25 "strings" 26 27 v1 "k8s.io/api/core/v1" 28 "k8s.io/klog/v2" 29 pluginapi "k8s.io/kubelet/pkg/apis/resourceplugin/v1alpha1" 30 31 apiconsts "github.com/kubewharf/katalyst-api/pkg/consts" 32 "github.com/kubewharf/katalyst-core/pkg/config/generic" 33 "github.com/kubewharf/katalyst-core/pkg/util/asyncworker" 34 "github.com/kubewharf/katalyst-core/pkg/util/general" 35 "github.com/kubewharf/katalyst-core/pkg/util/machine" 36 ) 37 38 // GetQuantityFromResourceReq parses resources quantity into value, 39 // since pods with reclaimed_cores and un-reclaimed_cores have different 40 // representations, we may to adapt to both cases. 41 func GetQuantityFromResourceReq(req *pluginapi.ResourceRequest) (int, float64, error) { 42 if len(req.ResourceRequests) != 1 { 43 return 0, 0, fmt.Errorf("invalid req.ResourceRequests length: %d", len(req.ResourceRequests)) 44 } 45 46 for key := range req.ResourceRequests { 47 switch key { 48 case string(v1.ResourceCPU): 49 return general.Max(int(math.Ceil(req.ResourceRequests[key])), 0), req.ResourceRequests[key], nil 50 case string(apiconsts.ReclaimedResourceMilliCPU): 51 return general.Max(int(math.Ceil(req.ResourceRequests[key]/1000.0)), 0), req.ResourceRequests[key] / 1000.0, nil 52 case string(v1.ResourceMemory), string(apiconsts.ReclaimedResourceMemory), string(apiconsts.ResourceNetBandwidth): 53 return general.Max(int(math.Ceil(req.ResourceRequests[key])), 0), req.ResourceRequests[key], nil 54 default: 55 return 0, 0, fmt.Errorf("invalid request resource name: %s", key) 56 } 57 } 58 59 return 0, 0, fmt.Errorf("unexpected end") 60 } 61 62 // IsDebugPod returns true if the pod annotations show up any configurable debug key 63 func IsDebugPod(podAnnotations map[string]string, podDebugAnnoKeys []string) bool { 64 for _, debugKey := range podDebugAnnoKeys { 65 if _, exists := podAnnotations[debugKey]; exists { 66 return true 67 } 68 } 69 70 return false 71 } 72 73 // GetKatalystQoSLevelFromResourceReq retrieves QoS Level for a given request 74 func GetKatalystQoSLevelFromResourceReq(qosConf *generic.QoSConfiguration, req *pluginapi.ResourceRequest) (qosLevel string, err error) { 75 if req == nil { 76 err = fmt.Errorf("GetKatalystQoSLevelFromResourceReq got nil resource request") 77 return 78 } 79 80 var getErr error 81 qosLevel, getErr = qosConf.GetQoSLevel(nil, req.Annotations) 82 if getErr != nil { 83 err = fmt.Errorf("resource type mismatches: %v", getErr) 84 return 85 } 86 87 // setting annotations and labels to only keep katalyst QoS related values 88 if req.Annotations == nil { 89 req.Annotations = make(map[string]string) 90 } 91 req.Annotations[apiconsts.PodAnnotationQoSLevelKey] = qosLevel 92 req.Annotations = qosConf.FilterQoSAndEnhancementMap(req.Annotations) 93 94 if req.Labels == nil { 95 req.Labels = make(map[string]string) 96 } 97 req.Labels[apiconsts.PodAnnotationQoSLevelKey] = qosLevel 98 req.Labels = qosConf.FilterQoSMap(req.Labels) 99 return 100 } 101 102 // HintToIntArray transforms TopologyHint to int slices 103 func HintToIntArray(hint *pluginapi.TopologyHint) []int { 104 if hint == nil { 105 return []int{} 106 } 107 108 result := make([]int, 0, len(hint.Nodes)) 109 for _, node := range hint.Nodes { 110 result = append(result, int(node)) 111 } 112 113 return result 114 } 115 116 // GetTopologyAwareQuantityFromAssignments returns TopologyAwareQuantity based on assignments 117 func GetTopologyAwareQuantityFromAssignments(assignments map[int]machine.CPUSet) []*pluginapi.TopologyAwareQuantity { 118 if assignments == nil { 119 return nil 120 } 121 122 topologyAwareQuantityList := make([]*pluginapi.TopologyAwareQuantity, 0, len(assignments)) 123 124 numaNodes := make([]int, 0, len(assignments)) 125 for numaNode := range assignments { 126 numaNodes = append(numaNodes, numaNode) 127 } 128 sort.Ints(numaNodes) 129 130 for _, numaNode := range numaNodes { 131 cpus := assignments[numaNode] 132 topologyAwareQuantityList = append(topologyAwareQuantityList, &pluginapi.TopologyAwareQuantity{ 133 ResourceValue: float64(cpus.Size()), 134 Node: uint64(numaNode), 135 }) 136 } 137 138 return topologyAwareQuantityList 139 } 140 141 // GetTopologyAwareQuantityFromAssignmentsSize returns TopologyAwareQuantity based on assignments, 142 // and assignments will use resource size (instead of resource struct) 143 func GetTopologyAwareQuantityFromAssignmentsSize(assignments map[int]uint64) []*pluginapi.TopologyAwareQuantity { 144 if assignments == nil { 145 return nil 146 } 147 148 topologyAwareQuantityList := make([]*pluginapi.TopologyAwareQuantity, 0, len(assignments)) 149 150 numaNodes := make([]int, 0, len(assignments)) 151 for numaNode := range assignments { 152 numaNodes = append(numaNodes, numaNode) 153 } 154 sort.Ints(numaNodes) 155 156 for _, numaNode := range numaNodes { 157 topologyAwareQuantityList = append(topologyAwareQuantityList, &pluginapi.TopologyAwareQuantity{ 158 ResourceValue: float64(assignments[numaNode]), 159 Node: uint64(numaNode), 160 }) 161 } 162 163 return topologyAwareQuantityList 164 } 165 166 // PackResourceHintsResponse returns the standard QRM ResourceHintsResponse 167 func PackResourceHintsResponse(req *pluginapi.ResourceRequest, resourceName string, 168 resourceHints map[string]*pluginapi.ListOfTopologyHints, 169 ) (*pluginapi.ResourceHintsResponse, error) { 170 if req == nil { 171 return nil, fmt.Errorf("PackResourceHintsResponse got nil request") 172 } 173 174 return &pluginapi.ResourceHintsResponse{ 175 PodUid: req.PodUid, 176 PodNamespace: req.PodNamespace, 177 PodName: req.PodName, 178 ContainerName: req.ContainerName, 179 ContainerType: req.ContainerType, 180 ContainerIndex: req.ContainerIndex, 181 PodRole: req.PodRole, 182 PodType: req.PodType, 183 ResourceName: resourceName, 184 ResourceHints: resourceHints, 185 Labels: general.DeepCopyMap(req.Labels), 186 Annotations: general.DeepCopyMap(req.Annotations), 187 NativeQosClass: req.NativeQosClass, 188 }, nil 189 } 190 191 // GetNUMANodesCountToFitCPUReq is used to calculate the amount of numa nodes 192 // we need if we try to allocate cpu cores among them, assuming that all numa nodes 193 // contain the same cpu capacity 194 func GetNUMANodesCountToFitCPUReq(cpuReq int, cpuTopology *machine.CPUTopology) (int, int, error) { 195 if cpuTopology == nil { 196 return 0, 0, fmt.Errorf("GetNumaNodesToFitCPUReq got nil cpuTopology") 197 } 198 199 numaCount := cpuTopology.CPUDetails.NUMANodes().Size() 200 if numaCount == 0 { 201 return 0, 0, fmt.Errorf("there is no NUMA in cpuTopology") 202 } 203 204 if cpuTopology.NumCPUs%numaCount != 0 { 205 return 0, 0, fmt.Errorf("invalid NUMAs count: %d with CPUs count: %d", numaCount, cpuTopology.NumCPUs) 206 } 207 208 cpusPerNUMA := cpuTopology.NumCPUs / numaCount 209 numaCountNeeded := int(math.Ceil(float64(cpuReq) / float64(cpusPerNUMA))) 210 if numaCountNeeded == 0 { 211 return 0, 0, fmt.Errorf("zero numaCountNeeded") 212 } else if numaCountNeeded > numaCount { 213 return 0, 0, fmt.Errorf("invalid cpu req: %d in topology with NUMAs count: %d and CPUs count: %d", cpuReq, numaCount, cpuTopology.NumCPUs) 214 } 215 216 cpusCountNeededPerNUMA := int(math.Ceil(float64(cpuReq) / float64(numaCountNeeded))) 217 return numaCountNeeded, cpusCountNeededPerNUMA, nil 218 } 219 220 // GetNUMANodesCountToFitMemoryReq is used to calculate the amount of numa nodes 221 // we need if we try to allocate memory among them, assuming that all numa nodes 222 // contain the same memory capacity 223 func GetNUMANodesCountToFitMemoryReq(memoryReq, bytesPerNUMA uint64, numaCount int) (int, uint64, error) { 224 if bytesPerNUMA == 0 { 225 return 0, 0, fmt.Errorf("zero bytesPerNUMA") 226 } 227 228 numaCountNeeded := int(math.Ceil(float64(memoryReq) / float64(bytesPerNUMA))) 229 230 if numaCountNeeded == 0 { 231 return 0, 0, fmt.Errorf("zero numaCountNeeded") 232 } else if numaCountNeeded > numaCount { 233 return 0, 0, fmt.Errorf("invalid memory req: %d in topology with NUMAs count: %d and bytesPerNUMA: %d", memoryReq, numaCount, bytesPerNUMA) 234 } 235 236 bytesNeededPerNUMA := uint64(math.Ceil(float64(memoryReq) / float64(numaCountNeeded))) 237 return numaCountNeeded, bytesNeededPerNUMA, nil 238 } 239 240 // GetHintsFromExtraStateFile 241 // if you want to specify cpuset.mems for specific pods (eg. for existing pods) when switching 242 // to katalyst the first time, you can provide an extra hints state file with content like below: 243 /* 244 { 245 "memoryEntries": { 246 "dp-18a916b04c-bdc9d5fd9-8m7vr-0": "0-1", 247 "dp-18a916b04c-bdc9d5fd9-h9tgp-0": "5,7", 248 "dp-47320a8d77-f46d6cbc7-5r27s-0": "2-3", 249 "dp-d7e988f508-5f66655c5-8n2tf-0": "4,6" 250 }, 251 } 252 */ 253 func GetHintsFromExtraStateFile(podName, resourceName, extraHintsStateFileAbsPath string, 254 availableNUMAs machine.CPUSet, 255 ) (map[string]*pluginapi.ListOfTopologyHints, error) { 256 if extraHintsStateFileAbsPath == "" { 257 return nil, nil 258 } 259 260 fileBytes, err := ioutil.ReadFile(extraHintsStateFileAbsPath) 261 if err != nil { 262 return nil, fmt.Errorf("read extra hints state file failed with error: %v", err) 263 } 264 265 extraState := make(map[string]interface{}) 266 err = json.Unmarshal(fileBytes, &extraState) 267 if err != nil { 268 return nil, fmt.Errorf("unmarshal extra state file content failed with error: %v", err) 269 } 270 271 memoryEntries, typeOk := extraState["memoryEntries"].(map[string]interface{}) 272 if !typeOk { 273 return nil, fmt.Errorf("memory entries with invalid type: %T", extraState["memoryEntries"]) 274 } 275 276 extraPodName := fmt.Sprintf("%s-0", podName) 277 if memoryEntries[extraPodName] == nil { 278 return nil, fmt.Errorf("extra state file hasn't memory entry for pod: %s", extraPodName) 279 } 280 281 memoryEntry, typeOk := memoryEntries[extraPodName].(string) 282 if !typeOk { 283 return nil, fmt.Errorf("memory entry with invalid type: %T", memoryEntries[extraPodName]) 284 } 285 286 numaSet, err := machine.Parse(memoryEntry) 287 if err != nil { 288 return nil, fmt.Errorf("parse memory entry: %s failed with error: %v", memoryEntry, err) 289 } 290 291 if !numaSet.IsSubsetOf(availableNUMAs) { 292 return nil, fmt.Errorf("NUMAs: %s in extra state file isn't subset of available NUMAs: %s", numaSet.String(), availableNUMAs.String()) 293 } 294 295 allocatedNumaNodes := numaSet.ToSliceUInt64() 296 klog.InfoS("[GetHintsFromExtraStateFile] get hints from extra state file", 297 "podName", podName, 298 "resourceName", resourceName, 299 "hint", allocatedNumaNodes) 300 301 hints := map[string]*pluginapi.ListOfTopologyHints{ 302 resourceName: { 303 Hints: []*pluginapi.TopologyHint{ 304 { 305 Nodes: allocatedNumaNodes, 306 Preferred: true, 307 }, 308 }, 309 }, 310 } 311 return hints, nil 312 } 313 314 func GetContainerAsyncWorkName(podUID, containerName, topic string) string { 315 return strings.Join([]string{podUID, containerName, topic}, asyncworker.WorkNameSeperator) 316 } 317 318 func GetCgroupAsyncWorkName(cgroup, topic string) string { 319 return strings.Join([]string{cgroup, topic}, asyncworker.WorkNameSeperator) 320 } 321 322 func GetAsyncWorkNameByPrefix(prefix, topic string) string { 323 return strings.Join([]string{prefix, topic}, asyncworker.WorkNameSeperator) 324 }