zedkube: detect and recover stuck kubelet volume mounts - #6197
zedkube: detect and recover stuck kubelet volume mounts#6197eriknordmark wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #6197 +/- ##
==========================================
+ Coverage 22.76% 23.10% +0.34%
==========================================
Files 509 520 +11
Lines 93274 95106 +1832
==========================================
+ Hits 21230 21977 +747
- Misses 70313 71214 +901
- Partials 1731 1915 +184 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
a366dea to
bd56b88
Compare
| now := time.Now() | ||
| var wedged []string | ||
| for i := range pods.Items { | ||
| if desc, ok := z.podMountWedge(pods.Items[i], now); ok { |
There was a problem hiding this comment.
Should we here check only if the pod is scheduled on this node?
There was a problem hiding this comment.
It is filtered, just not here — podMountWedge early-returns unless p.Spec.NodeName == z.nodeName, so pods scheduled elsewhere are skipped. But you're right that it belongs on the LIST rather than in the loop; I'll fold it into the field selector below so we stop pulling every pod in the namespace.
There was a problem hiding this comment.
Done in bff6975 — the node filter is now on the LIST itself, so pods on other nodes never come back over the API. The p.Spec.NodeName != z.nodeName check stays in podMountWedge as a guard.
| } | ||
| ctx, cancel := context.WithTimeout(context.Background(), kubeAPITimeout) | ||
| defer cancel() | ||
| pods, err := clientset.CoreV1().Pods(kubeapi.EVEKubeNameSpace).List(ctx, metav1.ListOptions{}) |
There was a problem hiding this comment.
maybe using:
podList, err := clientset.CoreV1().Pods(kubeapi.EVEKubeNameSpace).List(ctx, metav1.ListOptions{
FieldSelector: "status.phase!=Running",
})
There was a problem hiding this comment.
Agreed, and this matches what drain.go and sriov_devplugin.go already do (FieldSelector: "spec.nodeName=" + nodeName) — this LIST is the outlier. I'll use spec.nodeName=<node>,status.phase=Pending: status.phase=Pending rather than !=Running, since the detector only ever considers Pending pods. The client-side phase/node checks stay in podMountWedge as a guard.
There was a problem hiding this comment.
Done in bff6975:
pods, err := clientset.CoreV1().Pods(kubeapi.EVEKubeNameSpace).List(ctx, metav1.ListOptions{
FieldSelector: "spec.nodeName=" + z.nodeName +
",status.phase=" + string(corev1.PodPending),
})gofmt, go build/go vet -tags kubevirt,k and the zedkube package tests pass.
bd56b88 to
2deedcc
Compare
2deedcc to
7d3a45b
Compare
In cluster mode a Longhorn volume can be attached to the node (block device present, VolumeAttachment reporting Attached) while kubelet never issues NodeStage for it, so the consuming pod sits in ContainerCreating /Init forever with no FailedMount event and the app never runs. The stall is in kubelet's volume manager (shipped via k3s), not Longhorn, CDI or EVE, and only a fresh kubelet clears it. Nothing detected or recovered from this before. Add a zedkube check that flags a pod left Pending past a threshold on this node whose Longhorn PVC is attached-but-unmounted and shows no container/init startup error (image pull, crash, or missing secret/config). When one is found it restarts k3s: it resets cluster-init.sh's restart backoff and sends SIGTERM to the k3s server process, which the supervisor then relaunches, so kubelet returns with a fresh volume manager. Recovery is rate-limited per episode (bounded attempts plus a cooldown) and every restart logs a distinctive MOUNT-WEDGE-RECOVERY marker. A build-time flag disables the action and leaves only detection logging. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
7d3a45b to
53333f5
Compare
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Field data: 12 recoveries over ~2.5 days, 10 of them on orphaned CDI upload podsRan this branch's detector on a single-node EVE-k lab device ( The interesting result is what it fired on:
2 of 12 were doing real work. #7 is the best evidence for the detector's value: a 10 of 12 were futile. Two PVCs ( The per-episode cap does not bound total restartsRows 8–11 are the part worth acting on. The orphan walked 1/3 → 2/3 → 3/3 fifteen minutes apart, — and then a device reboot reset SuggestionThis is direct support for the orphan-reaping follow-up already noted on this PR. Concretely,
A cheap alternative that would have suppressed all 10 futile restarts without new logic: treat VerificationDeleting the two orphan PVCs and their upload pods on 07-28 12:35 PDT left |
Follow-up: confirmed — the 10 futile recoveries were entirely the two stale PVCsDeleted both orphan PVCs (+ their scratch PVCs and upload pods) at 07-28 12:35 PDT, leaving
So the 10/12 attribution holds: those recoveries were caused solely by two This doesn't change the suggestion — it strengthens it. The detector's recovery is sound; what it |
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit d9bb1f8)
This branch merged lf-edge#6197 at head 7d3a45b; the PR has since been amended to 53333f5, adding CreateContainerConfigError to the container-error exclusion the mount-wedge detector consults. Without it a CDI upload pod stalled on a missing per-upload secret is misread as a kubelet mount wedge and the detector restarts k3s three times to no effect - observed in resize soak 20260724-163249 iter-012. The README SHA is updated to match. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> (cherry picked from commit 4219af7)
The mount-wedge detector listed every pod in the EVE namespace and discarded the irrelevant ones in the loop, so on a multi-node cluster it pulled other nodes' pods over the API on every tick. Only a Pending pod scheduled on this node can exhibit the wedge, so ask the apiserver for exactly that set, matching how the drain and SR-IOV device-plugin paths already restrict their LISTs. The per-pod node and phase checks stay as a guard. Signed-off-by: eriknordmark <erik@zededa.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: eriknordmark <erik@zededa.com>
Description
In cluster mode a Longhorn volume can be attached to a node (block device
present under
/dev/longhorn, itsVolumeAttachmentreportingAttached)while kubelet never issues
NodeStagefor it. The consuming pod — a CDIimporter or a virt-launcher — then sits in
ContainerCreating/Initindefinitely with no
FailedMountevent, and the app never runs. The stallis in kubelet's volume manager (shipped via k3s) — not Longhorn, CDI or EVE —
and only a fresh kubelet clears it. Nothing on the device detected or recovered
from this before.
This adds a
zedkubecheck (checkStuckVolumeMount) that flags a pod leftPendingpast a threshold on this node whose Longhorn PVC isattached-but-unmounted and shows no container/init image-pull or crash error
(those are excluded as different failure modes). When it finds one it restarts
k3s so kubelet comes back with a fresh volume manager: it resets
cluster-init.sh's restart backoff (
K3S_MANUAL_START_FLAG) and sendsSIGTERMto the
k3s serverprocess, which the supervisor then relaunches. This ispossible because pillar runs in the host PID namespace and shares the
/runbind with the kube container.
Recovery is rate-limited per episode — node-scoped bounded attempts
(
stuckMountMaxRecover) plus a cooldown (stuckMountSuppressWindow) — so thedetector cannot thrash k3s restarts faster than kubelet can recover. Every
restart logs a distinctive, greppable
MOUNT-WEDGE-RECOVERYmarker at Warnlevel with the attempt count, the signaled PIDs, and the wedged pod/PV. A
build-time flag (
stuckMountDryRun) disables the action and leaves onlydetection logging.
The dry-run form of this check was field-validated first: on a lab EVE-k device
it fired on a real CDI upload-pod wedge (attach succeeded, device present, no
FailedMount,ContainerCreating~10m) with no false positives, confirming thedetection signature before the disruptive restart was enabled here.
How to test and validate this PR
On an EVE-k device, when a pod is stuck
ContainerCreatingon anattached-but-unmounted Longhorn volume for more than ~5 minutes,
zedkuberestarts k3s and logs:
k3s/kubelet come back within ~1 minute and the previously wedged pod proceeds
to mount and start. With no wedge present the check logs nothing and resets its
per-episode counter. Grep the device newlog for
MOUNT-WEDGE-RECOVERYto seeevery restart.
Changelog notes
No user-facing changes.
PR Backports
Checklist