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  }