|
8 | 8 | "sync" |
9 | 9 | "time" |
10 | 10 |
|
| 11 | + gpuconst "github.com/NVIDIA/KAI-scheduler/pkg/common/constants" |
11 | 12 | "github.com/go-logr/logr" |
12 | 13 | "github.com/prometheus/client_golang/api" |
13 | 14 | v1 "github.com/prometheus/client_golang/api/prometheus/v1" |
@@ -58,6 +59,9 @@ type NodeCollector struct { |
58 | 59 | logger logr.Logger |
59 | 60 | metrics *TelemetryMetrics |
60 | 61 | 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 |
61 | 65 | } |
62 | 66 |
|
63 | 67 | // NewNodeCollector creates a new collector for node resources |
@@ -116,6 +120,7 @@ func NewNodeCollector( |
116 | 120 | excludedNodes: excludedNodesMap, |
117 | 121 | logger: logger.WithName("node-collector"), |
118 | 122 | metrics: metrics, |
| 123 | + nodeToPodsMap: make(map[string]map[string]*corev1.Pod), |
119 | 124 | } |
120 | 125 | } |
121 | 126 |
|
@@ -187,8 +192,35 @@ func (c *NodeCollector) Start(ctx context.Context) error { |
187 | 192 | // Create node informer |
188 | 193 | c.nodeInformer = c.informerFactory.Core().V1().Nodes().Informer() |
189 | 194 |
|
| 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 | + |
190 | 222 | // Add event handlers |
191 | | - _, err := c.nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ |
| 223 | + _, err = c.nodeInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ |
192 | 224 | AddFunc: func(obj interface{}) { |
193 | 225 | node := obj.(*corev1.Node) |
194 | 226 | c.handleNodeEvent(node, EventTypeAdd) |
@@ -216,7 +248,7 @@ func (c *NodeCollector) Start(ctx context.Context) error { |
216 | 248 |
|
217 | 249 | // Wait for cache sync |
218 | 250 | 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) { |
220 | 252 | return fmt.Errorf("timed out waiting for caches to sync") |
221 | 253 | } |
222 | 254 | c.logger.Info("Informer caches synced successfully") |
@@ -245,6 +277,120 @@ func (c *NodeCollector) Start(ctx context.Context) error { |
245 | 277 | return nil |
246 | 278 | } |
247 | 279 |
|
| 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 | + |
248 | 394 | // handleNodeEvent processes node add, update, and delete events |
249 | 395 | func (c *NodeCollector) handleNodeEvent(node *corev1.Node, eventType EventType) { |
250 | 396 | if c.isExcluded(node.Name) { |
@@ -520,6 +666,12 @@ func (c *NodeCollector) collectAllNodeResources(ctx context.Context) { |
520 | 666 | resourceData["gpuUsage"] = gpuMetrics["GPUUsage"] |
521 | 667 | } |
522 | 668 |
|
| 669 | + workloadResources := c.calculateNodeWorkloadResources(node.Name) |
| 670 | + |
| 671 | + for k, v := range workloadResources { |
| 672 | + resourceData[k] = v |
| 673 | + } |
| 674 | + |
523 | 675 | // Send node resource metrics to the batch channel for batching |
524 | 676 | c.batchChan <- CollectedResource{ |
525 | 677 | ResourceType: NodeResource, |
@@ -742,6 +894,12 @@ func (c *NodeCollector) Stop() error { |
742 | 894 | c.batcher.stop() // This will close resourceChan when done |
743 | 895 | c.logger.Info("Node collector batcher stopped") |
744 | 896 | } |
| 897 | + |
| 898 | + // 5. Clear nodeToPodsMap |
| 899 | + c.podMapMutex.Lock() |
| 900 | + c.nodeToPodsMap = make(map[string]map[string]*corev1.Pod) |
| 901 | + c.podMapMutex.Unlock() |
| 902 | + |
745 | 903 | // resourceChan is closed by the batcher's defer func. |
746 | 904 |
|
747 | 905 | return nil |
|
0 commit comments