Skip to content

[17.0-stable] zedkube: detect and recover stuck kubelet volume mounts - #6487

Open
eriknordmark wants to merge 5 commits into
lf-edge:17.0-stablefrom
eriknordmark:kubelet-mount-wedge-detector-17.0-stable
Open

[17.0-stable] zedkube: detect and recover stuck kubelet volume mounts#6487
eriknordmark wants to merge 5 commits into
lf-edge:17.0-stablefrom
eriknordmark:kubelet-mount-wedge-detector-17.0-stable

Conversation

@eriknordmark

Copy link
Copy Markdown
Contributor

Description

Backport of #6197 to 17.0-stable.

In cluster mode a Longhorn volume can be attached to a node while kubelet never
issues NodeStage for it, leaving the consuming pod in ContainerCreating with
no FailedMount event and the app never running. zedkube now detects that
signature — pod Pending past a threshold on this node, its Longhorn PVC attached
with the /dev/longhorn device present but unmounted, no image-pull or crash
error — and restarts k3s so kubelet comes back with a fresh volume manager.
Recovery requires that some app instance still references the volume, so a stale
CDI upload PVC that nothing is waiting for is logged rather than acted on, and is
rate-limited per episode.

Five of the source PR's six commits are cherry-picked with -x, in order:
092d7a326, f16bfce19, 41756b7e9, 922d041b8, ece6e50ab. The sixth,
Makefile: raise the non-k rootfs ceiling to 291MB, is omitted: it lifts
ROOTFS_MAXSIZE_MB from 290 to 291 for a master rootfs baseline that no longer
fits under the old ceiling, and this branch has its own baseline. It can be added
if the size check fails here.

One adaptation: const testNodeName = "this-node" is defined in
stuckmount_test.go. On master it comes from cmd/zedkube/vmirsaffinity_test.go,
which this branch does not have; the value is master's. Nothing else differs —
every added and removed line matches the source PR's diff byte for byte.

How to test and validate this PR

Covered by unit tests: go test -tags k ./cmd/zedkube/ in pkg/pillar (44 tests
pass on this branch, 13 of them the stuck-mount and restart-mechanism tests).
Note the k build tag — without it the package reports "no test files".

Neutering the app-reference gate fails exactly the tests that guard it
(TestCheckStuckVolumeMountOrphanNoRestart,
TestPodMountWedge/volume_belongs_to_no_app_config), so the suite does
discriminate rather than passing vacuously.

On an EVE-k device, a pod stuck ContainerCreating on an attached-but-unmounted
Longhorn volume for more than ~5 minutes triggers a k3s restart logged with a
MOUNT-WEDGE-RECOVERY marker at Warn level. On this branch the restart uses the
kube-init supervisor socket where that daemon is present and a SIGTERM to
k3s server otherwise; the mechanism is chosen per attempt. Grep all of
/persist/newlog for MOUNT-WEDGE-RECOVERY, not just collect/, which holds
only the last ~5 minutes. Hardware validation of both the recovery and the
app-reference gate is in #6197.

Changelog notes

No user-facing changes.

PR Backports

  • 17.0-stable: This PR.
  • 16.0-stable: No.
  • 14.5-stable: No.
  • 13.4-stable: No.

Checklist

  • I've provided a proper description
  • I've added the proper documentation
  • I've tested my PR on amd64 device (via zedkube: detect and recover stuck kubelet volume mounts #6197)
  • I've tested my PR on arm64 device
  • I've written the test verification instructions
  • I've set the proper labels to this PR
  • I've added a reference link to the original PR
  • PR's title follows the template

eriknordmark and others added 5 commits September 8, 2026 17:00
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>
(cherry picked from commit 092d7a3)
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>
(cherry picked from commit f16bfce)
The detector decides whether to restart k3s -- a disruptive, node-wide
action -- from a pod's phase, age, container error reasons and its
volume's attach state, and then rate-limits itself per wedge episode.
None of that was covered.

Add tests for the wedge signature, one per condition that must keep a
Pending pod from being called wedged (including the missing-Secret pod,
which looks identical from the outside but is not helped by a fresh
kubelet), for the per-episode attempt cap and its cooldown, and for the
cmdline match that decides which process gets SIGTERM. The episode test
also pins that a cleared wedge resets the attempt count, since otherwise
the cap would disarm recovery for the lifetime of the process.

Reaching that logic requires the cluster and host lookups to sit behind
indirections a test can replace, and the tick body to be separable from
the clientset it runs against. The /proc scan is likewise split from the
signaling, so process matching is exercisable without terminating
anything.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 41756b7)
The mount-wedge recovery restarted k3s by touching cluster-init.sh's manual-start
flag and sending SIGTERM. On an image where the Go kube-init daemon supervises k3s
instead, that flag is read by nobody and the daemon's own pre-restart hooks are
skipped, so the recovery would go quietly ineffective while the detector kept
reporting wedges.

Ask whichever supervisor is present. kube-init exposes a control socket that takes
a "restart" verb, so prefer it when the socket exists and fall back to the flag
plus SIGTERM when it does not. Choosing per attempt rather than once at start-up
keeps one pillar binary correct on both, including across an upgrade that swaps the
supervisor underneath a running device.

The socket reply is a single line whose ERR prefix, not the connection, reports
refusal, so an ERR and a silent daemon are both failures rather than a restart
assumed to have happened.

Signed-off-by: eriknordmark <erik@zededa.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 922d041)
A Longhorn volume left behind by a torn-down app keeps the mount-wedge
signature forever: attached to the node, block device present, and a
CDI upload pod sitting Pending because nothing will ever send it the
image. Restarting k3s cannot change that, so each attempt bought a
control-plane outage and no progress -- and because the per-episode
attempt cap lives in memory, every reboot re-armed the detector and the
device paid for it again. A lab run recorded ten such futile recoveries
out of twelve, driven by two stale volumes.

Require that some app instance still references the volume before
treating it as wedged. The name a VolumeRefConfig resolves to is the
same one VolumeStatus already resolves to, and zedkube subscribes to
AppInstanceConfig, so this is a local lookup with no apiserver call --
placed ahead of the PVC and VolumeAttachment lookups, it also keeps
those off the stale path entirely. A volume nothing references is
counted and reported once, rather than every tick, pointing the operator
at the stale object instead of at kubelet.

An empty subscription makes every volume unwanted, which suppresses
recovery instead of triggering it.

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>
(cherry picked from commit ece6e50)
@eriknordmark eriknordmark added the bug Something isn't working label Sep 8, 2026
@codecov

codecov Bot commented Sep 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.31959% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 21.89%. Comparing base (e69cda3) to head (acc19ef).

Files with missing lines Patch % Lines
pkg/pillar/cmd/zedkube/stuckmount.go 77.65% 37 Missing and 5 partials ⚠️
pkg/pillar/cmd/zedkube/zedkube.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@               Coverage Diff               @@
##           17.0-stable    #6487      +/-   ##
===============================================
+ Coverage        21.38%   21.89%   +0.50%     
===============================================
  Files              500      511      +11     
  Lines            92650    94541    +1891     
===============================================
+ Hits             19813    20697     +884     
- Misses           71191    72011     +820     
- Partials          1646     1833     +187     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@eriknordmark

Copy link
Copy Markdown
Contributor Author

@andrewd-zededa @naiming-zededa @zedi-pramodh @rene @milan-zededa — review please.

Backport of #6197, approved there by @rene. All checks have been green since 2026-09-08 and the PR is mergeable; an approval is the only thing outstanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant