Skip to content

Commit e98f5e7

Browse files
authored
Merge pull request #90 from devzero-inc/tzvonimir/node-minute-bucket-metrics
Add container metrics to node metrics
2 parents 2be5b5f + d1f699c commit e98f5e7

1 file changed

Lines changed: 160 additions & 2 deletions

File tree

internal/collector/node_collector.go

Lines changed: 160 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"sync"
99
"time"
1010

11+
gpuconst "github.com/NVIDIA/KAI-scheduler/pkg/common/constants"
1112
"github.com/go-logr/logr"
1213
"github.com/prometheus/client_golang/api"
1314
v1 "github.com/prometheus/client_golang/api/prometheus/v1"
@@ -58,6 +59,9 @@ type NodeCollector struct {
5859
logger logr.Logger
5960
metrics *TelemetryMetrics
6061
mu sync.RWMutex
62+
nodeToPodsMap map[string]map[string]*corev1.Pod // Maps node name -> pod key -> pod object
63+
podInformer cache.SharedIndexInformer
64+
podMapMutex sync.RWMutex
6165
}
6266

6367
// NewNodeCollector creates a new collector for node resources
@@ -116,6 +120,7 @@ func NewNodeCollector(
116120
excludedNodes: excludedNodesMap,
117121
logger: logger.WithName("node-collector"),
118122
metrics: metrics,
123+
nodeToPodsMap: make(map[string]map[string]*corev1.Pod),
119124
}
120125
}
121126

@@ -187,8 +192,35 @@ func (c *NodeCollector) Start(ctx context.Context) error {
187192
// Create node informer
188193
c.nodeInformer = c.informerFactory.Core().V1().Nodes().Informer()
189194

195+
c.podInformer = c.informerFactory.Core().V1().Pods().Informer()
196+
197+
// Add pod event handlers
198+
_, err := c.podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
199+
AddFunc: func(obj interface{}) {
200+
pod := obj.(*corev1.Pod)
201+
c.handlePodEvent(pod, EventTypeAdd)
202+
},
203+
UpdateFunc: func(oldObj, newObj interface{}) {
204+
oldPod := oldObj.(*corev1.Pod)
205+
newPod := newObj.(*corev1.Pod)
206+
c.handlePodEvent(newPod, EventTypeUpdate)
207+
208+
// If node assignment changed, handle as delete for old node
209+
if oldPod.Spec.NodeName != newPod.Spec.NodeName {
210+
c.removePodFromNode(oldPod)
211+
}
212+
},
213+
DeleteFunc: func(obj interface{}) {
214+
pod := obj.(*corev1.Pod)
215+
c.handlePodEvent(pod, EventTypeDelete)
216+
},
217+
})
218+
if err != nil {
219+
return fmt.Errorf("failed to add pod event handler: %w", err)
220+
}
221+
190222
// Add event handlers
191-
_, err := c.nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
223+
_, err = c.nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
192224
AddFunc: func(obj interface{}) {
193225
node := obj.(*corev1.Node)
194226
c.handleNodeEvent(node, EventTypeAdd)
@@ -216,7 +248,7 @@ func (c *NodeCollector) Start(ctx context.Context) error {
216248

217249
// Wait for cache sync
218250
c.logger.Info("Waiting for informer caches to sync")
219-
if !cache.WaitForCacheSync(c.stopCh, c.nodeInformer.HasSynced) {
251+
if !cache.WaitForCacheSync(c.stopCh, c.nodeInformer.HasSynced, c.podInformer.HasSynced) {
220252
return fmt.Errorf("timed out waiting for caches to sync")
221253
}
222254
c.logger.Info("Informer caches synced successfully")
@@ -245,6 +277,120 @@ func (c *NodeCollector) Start(ctx context.Context) error {
245277
return nil
246278
}
247279

280+
// Add these new methods for pod event handling
281+
func (c *NodeCollector) handlePodEvent(pod *corev1.Pod, eventType EventType) {
282+
// Skip pods not assigned to nodes yet
283+
if pod.Spec.NodeName == "" {
284+
return
285+
}
286+
287+
// Skip excluded nodes
288+
if c.isExcluded(pod.Spec.NodeName) {
289+
return
290+
}
291+
292+
switch eventType {
293+
case EventTypeAdd, EventTypeUpdate:
294+
c.addPodToNode(pod)
295+
case EventTypeDelete:
296+
c.removePodFromNode(pod)
297+
}
298+
}
299+
300+
// addPodToNode add pod to node
301+
func (c *NodeCollector) addPodToNode(pod *corev1.Pod) {
302+
c.podMapMutex.Lock()
303+
defer c.podMapMutex.Unlock()
304+
305+
nodeName := pod.Spec.NodeName
306+
podKey := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
307+
308+
// Initialize the pod map for this node if it doesn't exist
309+
if _, exists := c.nodeToPodsMap[nodeName]; !exists {
310+
c.nodeToPodsMap[nodeName] = make(map[string]*corev1.Pod)
311+
}
312+
313+
c.nodeToPodsMap[nodeName][podKey] = pod
314+
}
315+
316+
// removePodFromNode removes pod from existing node
317+
func (c *NodeCollector) removePodFromNode(pod *corev1.Pod) {
318+
c.podMapMutex.Lock()
319+
defer c.podMapMutex.Unlock()
320+
321+
nodeName := pod.Spec.NodeName
322+
podKey := fmt.Sprintf("%s/%s", pod.Namespace, pod.Name)
323+
324+
// Remove the pod from the node map
325+
if podMap, exists := c.nodeToPodsMap[nodeName]; exists {
326+
delete(podMap, podKey)
327+
}
328+
}
329+
330+
// Calculate resource requests and limits for a node
331+
func (c *NodeCollector) calculateNodeWorkloadResources(nodeName string) map[string]interface{} {
332+
c.podMapMutex.RLock()
333+
defer c.podMapMutex.RUnlock()
334+
335+
result := map[string]interface{}{
336+
"cpuRequestsMillis": int64(0),
337+
"cpuLimitsMillis": int64(0),
338+
"memoryRequestsBytes": int64(0),
339+
"memoryLimitsBytes": int64(0),
340+
"gpuRequestCount": int64(0),
341+
"gpuLimitCount": int64(0),
342+
}
343+
344+
// Check if we have pods for this node
345+
podMap, exists := c.nodeToPodsMap[nodeName]
346+
if !exists {
347+
return result
348+
}
349+
350+
// Calculate total requests and limits
351+
for _, pod := range podMap {
352+
// Skip pods not in Running or Pending phase
353+
if pod.Status.Phase != corev1.PodRunning && pod.Status.Phase != corev1.PodPending {
354+
continue
355+
}
356+
357+
// Calculate resources for containers
358+
for _, container := range pod.Spec.Containers {
359+
// CPU requests
360+
if val, ok := container.Resources.Requests[corev1.ResourceCPU]; ok {
361+
result["cpuRequestsMillis"] = result["cpuRequestsMillis"].(int64) + val.MilliValue()
362+
}
363+
364+
// CPU limits
365+
if val, ok := container.Resources.Limits[corev1.ResourceCPU]; ok {
366+
result["cpuLimitsMillis"] = result["cpuLimitsMillis"].(int64) + val.MilliValue()
367+
}
368+
369+
// Memory requests
370+
if val, ok := container.Resources.Requests[corev1.ResourceMemory]; ok {
371+
result["memoryRequestsBytes"] = result["memoryRequestsBytes"].(int64) + val.Value()
372+
}
373+
374+
// Memory limits
375+
if val, ok := container.Resources.Limits[corev1.ResourceMemory]; ok {
376+
result["memoryLimitsBytes"] = result["memoryLimitsBytes"].(int64) + val.Value()
377+
}
378+
379+
// GPU requests
380+
if val, ok := container.Resources.Requests[gpuconst.GpuResource]; ok {
381+
result["gpuRequestCount"] = result["gpuRequestCount"].(int64) + val.Value()
382+
}
383+
384+
// GPU limits
385+
if val, ok := container.Resources.Limits[gpuconst.GpuResource]; ok {
386+
result["gpuLimitCount"] = result["gpuLimitCount"].(int64) + val.Value()
387+
}
388+
}
389+
}
390+
391+
return result
392+
}
393+
248394
// handleNodeEvent processes node add, update, and delete events
249395
func (c *NodeCollector) handleNodeEvent(node *corev1.Node, eventType EventType) {
250396
if c.isExcluded(node.Name) {
@@ -520,6 +666,12 @@ func (c *NodeCollector) collectAllNodeResources(ctx context.Context) {
520666
resourceData["gpuUsage"] = gpuMetrics["GPUUsage"]
521667
}
522668

669+
workloadResources := c.calculateNodeWorkloadResources(node.Name)
670+
671+
for k, v := range workloadResources {
672+
resourceData[k] = v
673+
}
674+
523675
// Send node resource metrics to the batch channel for batching
524676
c.batchChan <- CollectedResource{
525677
ResourceType: NodeResource,
@@ -742,6 +894,12 @@ func (c *NodeCollector) Stop() error {
742894
c.batcher.stop() // This will close resourceChan when done
743895
c.logger.Info("Node collector batcher stopped")
744896
}
897+
898+
// 5. Clear nodeToPodsMap
899+
c.podMapMutex.Lock()
900+
c.nodeToPodsMap = make(map[string]map[string]*corev1.Pod)
901+
c.podMapMutex.Unlock()
902+
745903
// resourceChan is closed by the batcher's defer func.
746904

747905
return nil

0 commit comments

Comments
 (0)