mirror of
https://github.com/kubernetes-sigs/node-feature-discovery.git
synced 2025-03-31 04:04:51 +00:00
There have been recent changes made to the noderesourcetopology API storing the proto file generated using go-to-protobuf tool and this code inports the proto generated in the API in the topology-updater.proto The PRs corresponding to the changes are as follows: https://github.com/k8stopologyawareschedwg/noderesourcetopology-api/pull/9 https://github.com/k8stopologyawareschedwg/noderesourcetopology-api/pull/13 Commands used to generate topology-updater.pb.go file: go install github.com/golang/protobuf/protoc-gen-go@v1.4.3 go mod vendor protoc --go_opt=paths=source_relative --go_out=plugins=grpc:. pkg/topologyupdater/topology-updater.proto -I. -Ivendor As part of implmentation of this patch, reserved (non-allocatable) CPUs are evaluated by performing a difference between all the CPUs on a system (determined by using ghw) and allocatable CPUs (determined by querying GetAllocatableResources podResource API endpoint). When aggregator creates the NUMA zones, it will skip the zone creation if there are no allocatable resources. In this update we creates those missing zone with zero allocatable/available resources so we won't have holes in the array of reported zones. Co-Authored-by: Talor Itzhak <titzhak@redhat.com> Signed-off-by: Swati Sehgal <swsehgal@redhat.com>
332 lines
11 KiB
Go
332 lines
11 KiB
Go
/*
|
|
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 resourcemonitor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jaypipes/ghw"
|
|
topologyv1alpha1 "github.com/k8stopologyawareschedwg/noderesourcetopology-api/pkg/apis/topology/v1alpha1"
|
|
v1 "k8s.io/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/api/resource"
|
|
"k8s.io/klog/v2"
|
|
podresourcesapi "k8s.io/kubelet/pkg/apis/podresources/v1"
|
|
"sigs.k8s.io/node-feature-discovery/source"
|
|
)
|
|
|
|
const (
|
|
// obtained these values from node e2e tests : https://github.com/kubernetes/kubernetes/blob/82baa26905c94398a0d19e1b1ecf54eb8acb6029/test/e2e_node/util.go#L70
|
|
defaultPodResourcesTimeout = 10 * time.Second
|
|
)
|
|
|
|
type nodeResources struct {
|
|
perNUMAAllocatable map[int]map[v1.ResourceName]int64
|
|
// mapping: resourceName -> resourceID -> nodeID
|
|
resourceID2NUMAID map[string]map[string]int
|
|
topo *ghw.TopologyInfo
|
|
reservedCPUIDPerNUMA map[int][]string
|
|
}
|
|
|
|
type resourceData struct {
|
|
available int64
|
|
allocatable int64
|
|
capacity int64
|
|
}
|
|
|
|
func NewResourcesAggregator(podResourceClient podresourcesapi.PodResourcesListerClient) (ResourcesAggregator, error) {
|
|
var err error
|
|
|
|
topo, err := ghw.Topology(ghw.WithPathOverrides(ghw.PathOverrides{
|
|
"/sys": string(source.SysfsDir),
|
|
}))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), defaultPodResourcesTimeout)
|
|
defer cancel()
|
|
|
|
// Pod Resource API client
|
|
resp, err := podResourceClient.GetAllocatableResources(ctx, &podresourcesapi.AllocatableResourcesRequest{})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't receive response: %v.Get(_) = _, %w", podResourceClient, err)
|
|
}
|
|
|
|
return NewResourcesAggregatorFromData(topo, resp), nil
|
|
}
|
|
|
|
// NewResourcesAggregatorFromData is used to aggregate resource information based on the received data from underlying hardware and podresource API
|
|
func NewResourcesAggregatorFromData(topo *ghw.TopologyInfo, resp *podresourcesapi.AllocatableResourcesResponse) ResourcesAggregator {
|
|
allDevs := getContainerDevicesFromAllocatableResources(resp, topo)
|
|
return &nodeResources{
|
|
topo: topo,
|
|
resourceID2NUMAID: makeResourceMap(len(topo.Nodes), allDevs),
|
|
perNUMAAllocatable: makeNodeAllocatable(allDevs),
|
|
reservedCPUIDPerNUMA: makeReservedCPUMap(topo.Nodes, allDevs),
|
|
}
|
|
}
|
|
|
|
// Aggregate provides the mapping (numa zone name) -> Zone from the given PodResources.
|
|
func (noderesourceData *nodeResources) Aggregate(podResData []PodResources) topologyv1alpha1.ZoneList {
|
|
perNuma := make(map[int]map[v1.ResourceName]*resourceData)
|
|
for nodeID := range noderesourceData.topo.Nodes {
|
|
nodeRes, ok := noderesourceData.perNUMAAllocatable[nodeID]
|
|
if ok {
|
|
perNuma[nodeID] = make(map[v1.ResourceName]*resourceData)
|
|
for resName, resCap := range nodeRes {
|
|
if resName == "cpu" {
|
|
perNuma[nodeID][resName] = &resourceData{
|
|
allocatable: resCap,
|
|
available: resCap,
|
|
capacity: resCap + int64(len(noderesourceData.reservedCPUIDPerNUMA[nodeID])),
|
|
}
|
|
} else {
|
|
perNuma[nodeID][resName] = &resourceData{
|
|
allocatable: resCap,
|
|
available: resCap,
|
|
capacity: resCap,
|
|
}
|
|
}
|
|
}
|
|
// NUMA node doesn't have any allocatable resources, but yet it exists in the topology
|
|
// thus all its CPUs are reserved
|
|
} else {
|
|
perNuma[nodeID] = make(map[v1.ResourceName]*resourceData)
|
|
perNuma[nodeID]["cpu"] = &resourceData{
|
|
allocatable: int64(0),
|
|
available: int64(0),
|
|
capacity: int64(len(noderesourceData.reservedCPUIDPerNUMA[nodeID])),
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, podRes := range podResData {
|
|
for _, contRes := range podRes.Containers {
|
|
for _, res := range contRes.Resources {
|
|
noderesourceData.updateAvailable(perNuma, res)
|
|
}
|
|
}
|
|
}
|
|
|
|
zones := make(topologyv1alpha1.ZoneList, 0)
|
|
for nodeID, resList := range perNuma {
|
|
zone := topologyv1alpha1.Zone{
|
|
Name: makeZoneName(nodeID),
|
|
Type: "Node",
|
|
Resources: make(topologyv1alpha1.ResourceInfoList, 0),
|
|
}
|
|
|
|
costs, err := makeCostsPerNumaNode(noderesourceData.topo.Nodes, nodeID)
|
|
if err != nil {
|
|
klog.Infof("cannot find costs for NUMA node %d: %v", nodeID, err)
|
|
} else {
|
|
zone.Costs = topologyv1alpha1.CostList(costs)
|
|
}
|
|
|
|
for name, resData := range resList {
|
|
allocatableQty := *resource.NewQuantity(resData.allocatable, resource.DecimalSI)
|
|
capacityQty := *resource.NewQuantity(resData.capacity, resource.DecimalSI)
|
|
availableQty := *resource.NewQuantity(resData.available, resource.DecimalSI)
|
|
zone.Resources = append(zone.Resources, topologyv1alpha1.ResourceInfo{
|
|
Name: name.String(),
|
|
Available: availableQty,
|
|
Allocatable: allocatableQty,
|
|
Capacity: capacityQty,
|
|
})
|
|
}
|
|
zones = append(zones, zone)
|
|
}
|
|
return zones
|
|
}
|
|
|
|
// getContainerDevicesFromAllocatableResources normalize all compute resources to ContainerDevices.
|
|
// This is helpful because cpuIDs are not represented as ContainerDevices, but with a different format;
|
|
// Having a consistent representation of all the resources as ContainerDevices makes it simpler for
|
|
func getContainerDevicesFromAllocatableResources(availRes *podresourcesapi.AllocatableResourcesResponse, topo *ghw.TopologyInfo) []*podresourcesapi.ContainerDevices {
|
|
var contDevs []*podresourcesapi.ContainerDevices
|
|
contDevs = append(contDevs, availRes.GetDevices()...)
|
|
|
|
cpuIDToNodeIDMap := MakeLogicalCoreIDToNodeIDMap(topo)
|
|
|
|
cpusPerNuma := make(map[int][]string)
|
|
for _, cpuID := range availRes.GetCpuIds() {
|
|
nodeID, ok := cpuIDToNodeIDMap[int(cpuID)]
|
|
if !ok {
|
|
klog.Infof("cannot find the NUMA node for CPU %d", cpuID)
|
|
continue
|
|
}
|
|
|
|
cpuIDList := cpusPerNuma[nodeID]
|
|
cpuIDList = append(cpuIDList, fmt.Sprintf("%d", cpuID))
|
|
cpusPerNuma[nodeID] = cpuIDList
|
|
}
|
|
|
|
for nodeID, cpuList := range cpusPerNuma {
|
|
contDevs = append(contDevs, &podresourcesapi.ContainerDevices{
|
|
ResourceName: string(v1.ResourceCPU),
|
|
DeviceIds: cpuList,
|
|
Topology: &podresourcesapi.TopologyInfo{
|
|
Nodes: []*podresourcesapi.NUMANode{
|
|
{ID: int64(nodeID)},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
return contDevs
|
|
}
|
|
|
|
// updateAvailable computes the actually available resources.
|
|
// This function assumes the available resources are initialized to be equal to the allocatable.
|
|
func (noderesourceData *nodeResources) updateAvailable(numaData map[int]map[v1.ResourceName]*resourceData, ri ResourceInfo) {
|
|
for _, resID := range ri.Data {
|
|
resName := string(ri.Name)
|
|
resMap, ok := noderesourceData.resourceID2NUMAID[resName]
|
|
if !ok {
|
|
klog.Infof("unknown resource %q", ri.Name)
|
|
continue
|
|
}
|
|
nodeID, ok := resMap[resID]
|
|
if !ok {
|
|
klog.Infof("unknown resource %q: %q", resName, resID)
|
|
continue
|
|
}
|
|
numaData[nodeID][ri.Name].available--
|
|
}
|
|
}
|
|
|
|
// makeZoneName returns the canonical name of a NUMA zone from its ID.
|
|
func makeZoneName(nodeID int) string {
|
|
return fmt.Sprintf("node-%d", nodeID)
|
|
}
|
|
|
|
// makeNodeAllocatable computes the node allocatable as mapping (NUMA node ID) -> Resource -> Allocatable (amount, int).
|
|
// The computation is done assuming all the resources to represent the allocatable for are represented on a slice
|
|
// of ContainerDevices. No special treatment is done for CPU IDs. See getContainerDevicesFromAllocatableResources.
|
|
func makeNodeAllocatable(devices []*podresourcesapi.ContainerDevices) map[int]map[v1.ResourceName]int64 {
|
|
perNUMAAllocatable := make(map[int]map[v1.ResourceName]int64)
|
|
// initialize with the capacities
|
|
for _, device := range devices {
|
|
resourceName := device.GetResourceName()
|
|
for _, node := range device.GetTopology().GetNodes() {
|
|
nodeID := int(node.GetID())
|
|
nodeRes, ok := perNUMAAllocatable[nodeID]
|
|
if !ok {
|
|
nodeRes = make(map[v1.ResourceName]int64)
|
|
}
|
|
nodeRes[v1.ResourceName(resourceName)] += int64(len(device.GetDeviceIds()))
|
|
perNUMAAllocatable[nodeID] = nodeRes
|
|
}
|
|
}
|
|
return perNUMAAllocatable
|
|
}
|
|
|
|
func MakeLogicalCoreIDToNodeIDMap(topo *ghw.TopologyInfo) map[int]int {
|
|
core2node := make(map[int]int)
|
|
for _, node := range topo.Nodes {
|
|
for _, core := range node.Cores {
|
|
for _, procID := range core.LogicalProcessors {
|
|
core2node[procID] = node.ID
|
|
}
|
|
}
|
|
}
|
|
return core2node
|
|
}
|
|
|
|
// makeResourceMap creates the mapping (resource name) -> (device ID) -> (NUMA node ID) from the given slice of ContainerDevices.
|
|
// this is useful to quickly learn the NUMA ID of a given (resource, device).
|
|
func makeResourceMap(numaNodes int, devices []*podresourcesapi.ContainerDevices) map[string]map[string]int {
|
|
resourceMap := make(map[string]map[string]int)
|
|
|
|
for _, device := range devices {
|
|
resourceName := device.GetResourceName()
|
|
_, ok := resourceMap[resourceName]
|
|
if !ok {
|
|
resourceMap[resourceName] = make(map[string]int)
|
|
}
|
|
for _, node := range device.GetTopology().GetNodes() {
|
|
nodeID := int(node.GetID())
|
|
for _, deviceID := range device.GetDeviceIds() {
|
|
resourceMap[resourceName][deviceID] = nodeID
|
|
}
|
|
}
|
|
}
|
|
return resourceMap
|
|
}
|
|
|
|
// makeCostsPerNumaNode builds the cost map to reach all the known NUMA zones (mapping (numa zone) -> cost) starting from the given NUMA zone.
|
|
func makeCostsPerNumaNode(nodes []*ghw.TopologyNode, nodeIDSrc int) ([]topologyv1alpha1.CostInfo, error) {
|
|
nodeSrc := findNodeByID(nodes, nodeIDSrc)
|
|
if nodeSrc == nil {
|
|
return nil, fmt.Errorf("unknown node: %d", nodeIDSrc)
|
|
}
|
|
nodeCosts := make([]topologyv1alpha1.CostInfo, 0)
|
|
for nodeIDDst, dist := range nodeSrc.Distances {
|
|
// TODO: this assumes there are no holes (= no offline node) in the distance vector
|
|
nodeCosts = append(nodeCosts, topologyv1alpha1.CostInfo{
|
|
Name: makeZoneName(nodeIDDst),
|
|
Value: int64(dist),
|
|
})
|
|
}
|
|
return nodeCosts, nil
|
|
}
|
|
|
|
func findNodeByID(nodes []*ghw.TopologyNode, nodeID int) *ghw.TopologyNode {
|
|
for _, node := range nodes {
|
|
if node.ID == nodeID {
|
|
return node
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func makeReservedCPUMap(nodes []*ghw.TopologyNode, devices []*podresourcesapi.ContainerDevices) map[int][]string {
|
|
reservedCPUsPerNuma := make(map[int][]string)
|
|
cpus := getCPUs(devices)
|
|
for _, node := range nodes {
|
|
nodeID := node.ID
|
|
for _, core := range node.Cores {
|
|
for _, cpu := range core.LogicalProcessors {
|
|
cpuID := fmt.Sprintf("%d", cpu)
|
|
_, ok := cpus[cpuID]
|
|
if !ok {
|
|
cpuIDList, ok := reservedCPUsPerNuma[nodeID]
|
|
if !ok {
|
|
cpuIDList = make([]string, 0)
|
|
}
|
|
cpuIDList = append(cpuIDList, cpuID)
|
|
reservedCPUsPerNuma[nodeID] = cpuIDList
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return reservedCPUsPerNuma
|
|
}
|
|
|
|
func getCPUs(devices []*podresourcesapi.ContainerDevices) map[string]int {
|
|
cpuMap := make(map[string]int)
|
|
for _, device := range devices {
|
|
if device.GetResourceName() == "cpu" {
|
|
for _, devId := range device.DeviceIds {
|
|
cpuMap[devId] = int(device.Topology.Nodes[0].ID)
|
|
}
|
|
}
|
|
}
|
|
return cpuMap
|
|
}
|