-
Notifications
You must be signed in to change notification settings - Fork 828
fix(scheduler): serialize Filter device selection to prevent double G… #2559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -278,24 +278,25 @@ func validateConfig(config *Config) error { | |
| return fmt.Errorf("all configurations are empty") | ||
| } | ||
|
|
||
| func InitDevices() { | ||
| func InitDevices() error { | ||
| if len(device.DevicesMap) > 0 { | ||
| klog.Info("Devices are already initialized, skipping initialization") | ||
| return | ||
| return nil | ||
| } | ||
| klog.Infof("Loading device configuration from file: %s", configFile) | ||
| config, err := LoadConfig(configFile) | ||
| if err != nil { | ||
| klog.Fatalf("Failed to load device config file %s: %v", configFile, err) | ||
| return fmt.Errorf("failed to load device config file %s: %w", configFile, err) | ||
| } | ||
| klog.Infof("Loaded config: %v", config) | ||
| err = InitDevicesWithConfig(config) | ||
| if err != nil { | ||
| klog.Fatalf("Failed to initialize devices: %v", err) | ||
| return fmt.Errorf("failed to initialize devices: %w", err) | ||
| } | ||
|
Comment on lines
+281
to
295
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Make failed device initialization atomic.
Also applies to: 461-464 🤖 Prompt for AI Agents |
||
| return nil | ||
| } | ||
|
|
||
| func InitDefaultDevices() { | ||
| func InitDefaultDevices() error { | ||
| configMapdata := ` | ||
| nvidia: | ||
| resourceCountName: "nvidia.com/gpu" | ||
|
|
@@ -454,14 +455,13 @@ vnpus: | |
| var yamlData Config | ||
| err := yaml.Unmarshal([]byte(configMapdata), &yamlData) | ||
| if err != nil { | ||
| klog.Fatalf("Failed to unmarshal default config: %v", err) | ||
| return | ||
| return fmt.Errorf("failed to unmarshal default config: %w", err) | ||
| } | ||
|
|
||
| // Initialize devices with configuration | ||
| if err := InitDevicesWithConfig(&yamlData); err != nil { | ||
| klog.Fatalf("Failed to initialize devices with default config: %v", err) | ||
| return fmt.Errorf("failed to initialize devices with default config: %w", err) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func GlobalFlagSet() *flag.FlagSet { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,10 @@ type Scheduler struct { | |
|
|
||
| lock sync.RWMutex | ||
| synced bool | ||
|
|
||
| // filterLock serializes device selection in Filter so concurrent requests | ||
| // cannot reserve the same device (issue #2232). | ||
| filterLock sync.Mutex | ||
| } | ||
|
|
||
| func NewScheduler() *Scheduler { | ||
|
|
@@ -1030,30 +1034,75 @@ func (s *Scheduler) Filter(args extenderv1.ExtenderArgs) (*extenderv1.ExtenderFi | |
| if args.Nodes != nil { | ||
| return s.filterSimulation(args, resourceReqs) | ||
| } | ||
| klog.V(2).InfoS("Choosing live filter path", | ||
| "pod", klog.KObj(args.Pod), | ||
| "reason", "request does not contain full nodes", | ||
| "nodeNamesLen", nodeNamesLen(args.NodeNames)) | ||
| // selectAndCommitDevice holds filterLock during selection; the annotation | ||
| // patch below runs outside it (issue #2232). | ||
| selection, err := s.selectAndCommitDevice(args, resourceReqs) | ||
| if err != nil { | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", err) | ||
| return nil, err | ||
| } | ||
| if selection.chosen == nil { | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", fmt.Errorf("no available node, %d nodes do not meet", len(*args.NodeNames))) | ||
| return &extenderv1.ExtenderFilterResult{ | ||
| FailedNodes: selection.failedNodes, | ||
| }, nil | ||
| } | ||
| m := selection.chosen | ||
| // Patch the annotation outside the lock; roll back the cache on failure. | ||
| if err = util.PatchPodAnnotations(args.Pod, selection.annotations); err != nil { | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", err) | ||
| if selection.added { | ||
| s.quotaManager.RmUsage(args.Pod, selection.effectiveDevices) | ||
| } | ||
| s.podManager.DelPod(args.Pod) | ||
| return nil, err | ||
| } | ||
|
Comment on lines
+1055
to
+1063
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win The rollback path treats the pod cache and the quota inconsistently, and no test binds them together.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| successMsg := genSuccessMsg(len(*args.NodeNames), m.NodeID, selection.nodeList) | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringSucceed, successMsg, nil) | ||
| res := extenderv1.ExtenderFilterResult{NodeNames: &[]string{m.NodeID}} | ||
| return &res, nil | ||
| } | ||
|
|
||
| // filterSelection holds a Filter reservation; chosen is nil when no node fits | ||
| // (failedNodes then carries the per-node reasons). | ||
| type filterSelection struct { | ||
| chosen *policy.NodeScore | ||
| annotations map[string]string | ||
| added bool | ||
| nodeList []*policy.NodeScore | ||
| failedNodes map[string]string | ||
| effectiveDevices device.PodDevices | ||
| } | ||
|
|
||
| // selectAndCommitDevice selects a device and commits it to the cache under | ||
| // filterLock. It does not publish the pod annotation; the caller patches it. | ||
| func (s *Scheduler) selectAndCommitDevice(args extenderv1.ExtenderArgs, resourceReqs device.PodDeviceRequests) (*filterSelection, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. which k8s version, and where in kube-scheduler's source does that happen? without that this fix may be locking against a race that never occurs in real scheduling. |
||
| s.filterLock.Lock() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this lock is global, not per node or per device. a big concurrent deployment create now queues through one lock for every node. was a per node lock considered, not only per pod uid? |
||
| defer s.filterLock.Unlock() | ||
|
|
||
| selection := &filterSelection{} | ||
| if pi, ok := s.podManager.TakeAndDeletePod(args.Pod); ok { | ||
| s.quotaManager.RmUsage(args.Pod, pi.Devices) | ||
| } | ||
| nodeUsage, _, failedNodes, err := s.getNodesUsage(args.NodeNames, args.Pod) | ||
| if err != nil { | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", err) | ||
| return nil, err | ||
| } | ||
| if len(failedNodes) != 0 { | ||
| klog.V(5).InfoS("Nodes failed during usage retrieval", "nodes", failedNodes) | ||
| } | ||
| nodeScores, err := s.calcScore(nodeUsage, resourceReqs, args.Pod, failedNodes) | ||
| if err != nil { | ||
| err := fmt.Errorf("calcScore failed %v for pod %v", err, args.Pod.Name) | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", err) | ||
| return nil, err | ||
| return nil, fmt.Errorf("calcScore failed %v for pod %v", err, args.Pod.Name) | ||
| } | ||
| if len((*nodeScores).NodeList) == 0 { | ||
| klog.V(4).InfoS("No available nodes meet the required scores", "pod", args.Pod.Name) | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", fmt.Errorf("no available node, %d nodes do not meet", len(*args.NodeNames))) | ||
| return &extenderv1.ExtenderFilterResult{ | ||
| FailedNodes: failedNodes, | ||
| }, nil | ||
| selection.failedNodes = failedNodes | ||
| return selection, nil | ||
| } | ||
| klog.V(4).Infoln("nodeScores_len=", len((*nodeScores).NodeList)) | ||
| sort.Sort(nodeScores) | ||
|
|
@@ -1066,33 +1115,20 @@ func (s *Scheduler) Filter(args extenderv1.ExtenderArgs) (*extenderv1.ExtenderFi | |
| annotations := make(map[string]string) | ||
| annotations[util.AssignedNodeAnnotations] = m.NodeID | ||
| annotations[util.AssignedTimeAnnotations] = strconv.FormatInt(time.Now().Unix(), 10) | ||
|
|
||
| for _, val := range device.GetDevices() { | ||
| val.PatchAnnotations(args.Pod, &annotations, m.Devices) | ||
| } | ||
|
|
||
| rawDevices := m.Devices | ||
| effectiveDevices := device.CollapseInitContainerUsage(args.Pod, rawDevices) | ||
| if args.Nodes == nil { | ||
| added := s.podManager.AddPod(args.Pod, m.NodeID, effectiveDevices) | ||
| if added { | ||
| s.quotaManager.AddUsage(args.Pod, effectiveDevices) // use collapsed | ||
| } | ||
| err = util.PatchPodAnnotations(args.Pod, annotations) | ||
| if err != nil { | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringFailed, "", err) | ||
| if added { | ||
| s.quotaManager.RmUsage(args.Pod, effectiveDevices) | ||
| } | ||
| s.podManager.DelPod(args.Pod) | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| successMsg := genSuccessMsg(len(*args.NodeNames), m.NodeID, nodeScores.NodeList) | ||
| s.recordScheduleFilterResultEvent(args.Pod, EventReasonFilteringSucceed, successMsg, nil) | ||
| res := extenderv1.ExtenderFilterResult{NodeNames: &[]string{m.NodeID}} | ||
| return &res, nil | ||
| // Collapse init-container usage so the cache reflects the effective footprint. | ||
| effectiveDevices := device.CollapseInitContainerUsage(args.Pod, m.Devices) | ||
| selection.chosen = m | ||
| selection.annotations = annotations | ||
| selection.nodeList = nodeScores.NodeList | ||
| selection.effectiveDevices = effectiveDevices | ||
| selection.added = s.podManager.AddPod(args.Pod, m.NodeID, effectiveDevices) | ||
| if selection.added { | ||
| s.quotaManager.AddUsage(args.Pod, effectiveDevices) | ||
| } | ||
| return selection, nil | ||
| } | ||
|
|
||
| func (s *Scheduler) filterSimulation(args extenderv1.ExtenderArgs, resourceReqs device.PodDeviceRequests) (*extenderv1.ExtenderFilterResult, error) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this initdevices error change is not about the filterlock race. this repo closed a past pr for bundling unrelated changes together. should this be its own pr?