[ 
https://issues.apache.org/jira/browse/YUNIKORN-3375?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Dale Richardson updated YUNIKORN-3375:
--------------------------------------
    Description: 
When a Node object is deleted and re-created while its pods keep running, the 
shim and the core end up permanently disagreeing about what is on that node. 
The shim has the pods placed; the core believes the node is empty and has given 
the queue its quota back. The queue's usage stays understated for as long as 
the pods run.

*What works (this issue is not about these)*:
- The orphaned pod tracking works. On node removal the pods are orphaned, and 
when the node comes back the adoption path puts them back on the node in the 
shim cache correctly.
- There is no node-level double allocation. The shim's own predicates run 
against the shim cache, which contains the adopted pods, so a genuinely full 
node rejects new placements. We verified this.

*The gap*: adoption only restores the shim's view. Nothing restores the core's.
# On node removal the core removes the allocations from the applications and 
decrements the queue's allocated resources ({{removeNodeAllocations}} -> 
{{queue.DecAllocatedResource}}), then notifies the shim with {{TerminationType 
STOPPED_BY_RM}}. The shim callback correctly only forgets the pod in its cache 
and does not fail the task -- the pod is still running, and dispatching the 
release would delete it. The task stays Bound.
# On node re-add, the node registration cannot carry allocations 
({{existingAllocations}} was removed from the SI in 
[YUNIKORN-2780|https://issues.apache.org/jira/browse/YUNIKORN-2780]), and the 
adoption path never sends an allocation update either: {{updateYuniKornPod}} 
finds the still-Bound task and only calls {{SetTaskPod}}.
# No reconciliation exists. The divergence lasts until each pod terminates, or 
until a scheduler restart (restart recovery does re-report).

*Why we believe this is an oversight and not a design decision*: the same 
adoption loop DOES send an allocation update to the core for non-YuniKorn pods 
({{updateForeignPod}} -> {{CreateAllocationForForeignPod}}, tracking added by 
[YUNIKORN-2834|https://issues.apache.org/jira/browse/YUNIKORN-2834]). Only the 
YuniKorn-managed branch is silent, because the surviving Bound task suppresses 
it.

*Impact*:
- Queue accounting is silently wrong: a queue with max=X can end up running 
more than X (the surviving pods plus a full X of new admissions elsewhere). 
User quotas, fair-share ordering, preemption inputs, autoscaler calculations, 
metrics and the web UI all compute against understated usage.
- If ALL of an application's allocations were on the removed node, it is worse: 
the core application is left empty, moves Completing -> Completed (~30s 
default), and is removed from the partition entirely. The shim still holds the 
application with Bound tasks; the core no longer knows it exists.
- The core also wastes scheduling cycles proposing a node it believes is empty 
while the shim rejects every attempt.

*Reachability*: {{kubectl delete node}} on a live node (or automation doing the 
equivalent) followed by kubelet self-re-registration recreates the Node object 
within seconds. The pod-GC controller quarantines orphaned pods for ~40s before 
deleting them, so the pods surviving a brief flap is the normal outcome. 
Cluster-autoscaler is not a trigger (it drains first). Rare but ordinary 
operations; when it fires it affects every pod on the node.

*Reproduction*: deterministic via the 
[YUNIKORN-3377|https://issues.apache.org/jira/browse/YUNIKORN-3377] 
fault-injection test ({{TestChaosNodeFlap}}, oracle O5): after a flap, e.g. one 
node shows 40 pods placed in the shim and zero allocations in the core, on 
every seed.

*Suggested fix*: on adoption of a YuniKorn-managed pod whose task is Bound, 
re-report the allocation the way the foreign-pod branch and restart recovery 
already do ({{UpdateAllocation}} with the node ID set -> the core's 
{{MarkPreviouslyAllocated}} path). Because the core application may already 
have been removed (see impact above), the fix must follow the recovery sequence 
-- re-register the application first, then report the allocations -- rather 
than sending the allocation update alone.


  was:
When a Node object is deleted and re-created while its pods keep running, the 
shim and the core permanently disagree about the node's allocations, and queue 
accounting is silently corrupted.

*Mechanism* (all verified in code):
 # Node delete: the shim orphans the node's bound pods 
({{{}Context.deleteNodeInternal{}}} -> {{{}SchedulerCache.RemoveNode{}}}); the 
core removes the node, removes the allocations from the applications and 
*decrements queue allocated resources* 
({{{}partition.removeNodeAllocations{}}}), then notifies the shim with 
{{{}TerminationType STOPPED_BY_RM{}}}.
 # The shim callback intentionally only forgets the pod in the cache and does 
not fail the task ({{{}scheduler_callback.go{}}} ~117) – this filter is correct 
and must stay: dispatching the release would delete running pods.
 # Node re-add: {{registerNodes}} sends a bare {{si.NodeInfo}} (it cannot carry 
allocations – {{existingAllocations}} is a reserved/removed SI field), and the 
orphan-adoption path re-attaches the pods to the shim cache via 
{{updateYuniKornPod}} -> {{SetTaskPod}} on the still-Bound task – {*}nothing 
re-reports the allocations to the core{*}.
 # No reconciliation exists. The divergence lasts until each pod terminates, or 
until a scheduler restart (restart recovery does re-report).

{*}The asymmetry that shows this is an oversight{*}: the same adoption path 
already re-reports *foreign* pods to the core ({{{}updateForeignPod{}}} -> 
{{CreateAllocationForForeignPod}} -> {{{}UpdateAllocation{}}}). Only the 
YuniKorn-managed branch is silent, because the existing Bound task suppresses 
task creation.

*Impact* :
 - Node-level double allocation does NOT occur: the shim predicate callback 
runs {{NodeResourcesFit}} against the shim cache, which contains the adopted 
pods, so a genuinely full node rejects new placements (kubelet admission is a 
second backstop).
 - The real damage is {*}accounting integrity{*}: the queue's allocated 
resource was decremented while the pods keep running, and is never 
re-incremented. A queue with max=X can end up running more than X (survivors 
plus a full X of new admissions); user quotas, fair-share ordering, preemption 
inputs, autoscaler outstanding-request calculations, metrics and the web UI are 
all silently wrong. The core also wastes scheduling cycles proposing a node it 
believes is empty while the shim rejects every attempt.

{*}Reachability{*}: {{kubectl delete node}} on a live node (or automation doing 
the equivalent) followed by kubelet self-re-registration recreates the Node 
object within seconds; the pod-GC controller quarantines orphaned pods for ~40s 
before deleting them, so pods surviving a brief flap is the normal outcome, not 
a race. Cluster-autoscaler is not a trigger (it drains first). Rare but 
ordinary operations; when it fires it affects every pod on the node, for an 
unbounded duration on long-running workloads.

{*}Reproduction{*}: deterministic in a fault-injection test built on the 
MockScheduler harness (bind churn + node remove/re-add through the informer 
path): after settle, pods placed in the shim cache have no matching core 
allocation (e.g. one flapped node: shim 40 pods, core 0 allocations). 3/3 
flapped runs reproduce; 0/2 static controls. Test to be contributed.

{*}Suggested fix{*}: on adoption of a YuniKorn-managed pod whose task is Bound, 
re-report the allocation the same way the foreign-pod branch and restart 
recovery already do ({{{}UpdateAllocation{}}} with NodeID set -> the core's 
{{MarkPreviouslyAllocated}} path). Alternatively (or additionally) reconsider 
overloading {{STOPPED_BY_RM}} for core-initiated node-removal releases, so the 
shim can distinguish them.


> Node delete/re-add silently corrupts queue accounting: adopted pods are never 
> re-reported to the core
> -----------------------------------------------------------------------------------------------------
>
>                 Key: YUNIKORN-3375
>                 URL: https://issues.apache.org/jira/browse/YUNIKORN-3375
>             Project: Apache YuniKorn
>          Issue Type: Bug
>          Components: shim - kubernetes
>            Reporter: Dale Richardson
>            Priority: Major
>
> When a Node object is deleted and re-created while its pods keep running, the 
> shim and the core end up permanently disagreeing about what is on that node. 
> The shim has the pods placed; the core believes the node is empty and has 
> given the queue its quota back. The queue's usage stays understated for as 
> long as the pods run.
> *What works (this issue is not about these)*:
> - The orphaned pod tracking works. On node removal the pods are orphaned, and 
> when the node comes back the adoption path puts them back on the node in the 
> shim cache correctly.
> - There is no node-level double allocation. The shim's own predicates run 
> against the shim cache, which contains the adopted pods, so a genuinely full 
> node rejects new placements. We verified this.
> *The gap*: adoption only restores the shim's view. Nothing restores the 
> core's.
> # On node removal the core removes the allocations from the applications and 
> decrements the queue's allocated resources ({{removeNodeAllocations}} -> 
> {{queue.DecAllocatedResource}}), then notifies the shim with 
> {{TerminationType STOPPED_BY_RM}}. The shim callback correctly only forgets 
> the pod in its cache and does not fail the task -- the pod is still running, 
> and dispatching the release would delete it. The task stays Bound.
> # On node re-add, the node registration cannot carry allocations 
> ({{existingAllocations}} was removed from the SI in 
> [YUNIKORN-2780|https://issues.apache.org/jira/browse/YUNIKORN-2780]), and the 
> adoption path never sends an allocation update either: {{updateYuniKornPod}} 
> finds the still-Bound task and only calls {{SetTaskPod}}.
> # No reconciliation exists. The divergence lasts until each pod terminates, 
> or until a scheduler restart (restart recovery does re-report).
> *Why we believe this is an oversight and not a design decision*: the same 
> adoption loop DOES send an allocation update to the core for non-YuniKorn 
> pods ({{updateForeignPod}} -> {{CreateAllocationForForeignPod}}, tracking 
> added by 
> [YUNIKORN-2834|https://issues.apache.org/jira/browse/YUNIKORN-2834]). Only 
> the YuniKorn-managed branch is silent, because the surviving Bound task 
> suppresses it.
> *Impact*:
> - Queue accounting is silently wrong: a queue with max=X can end up running 
> more than X (the surviving pods plus a full X of new admissions elsewhere). 
> User quotas, fair-share ordering, preemption inputs, autoscaler calculations, 
> metrics and the web UI all compute against understated usage.
> - If ALL of an application's allocations were on the removed node, it is 
> worse: the core application is left empty, moves Completing -> Completed 
> (~30s default), and is removed from the partition entirely. The shim still 
> holds the application with Bound tasks; the core no longer knows it exists.
> - The core also wastes scheduling cycles proposing a node it believes is 
> empty while the shim rejects every attempt.
> *Reachability*: {{kubectl delete node}} on a live node (or automation doing 
> the equivalent) followed by kubelet self-re-registration recreates the Node 
> object within seconds. The pod-GC controller quarantines orphaned pods for 
> ~40s before deleting them, so the pods surviving a brief flap is the normal 
> outcome. Cluster-autoscaler is not a trigger (it drains first). Rare but 
> ordinary operations; when it fires it affects every pod on the node.
> *Reproduction*: deterministic via the 
> [YUNIKORN-3377|https://issues.apache.org/jira/browse/YUNIKORN-3377] 
> fault-injection test ({{TestChaosNodeFlap}}, oracle O5): after a flap, e.g. 
> one node shows 40 pods placed in the shim and zero allocations in the core, 
> on every seed.
> *Suggested fix*: on adoption of a YuniKorn-managed pod whose task is Bound, 
> re-report the allocation the way the foreign-pod branch and restart recovery 
> already do ({{UpdateAllocation}} with the node ID set -> the core's 
> {{MarkPreviouslyAllocated}} path). Because the core application may already 
> have been removed (see impact above), the fix must follow the recovery 
> sequence -- re-register the application first, then report the allocations -- 
> rather than sending the allocation update alone.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to