Commit 51edfea
[LEP-5065] feat(nfs): detect NFS hangs from kmsg and flip node Unhealthy (#1245)
## Summary
Adds a kmsg-based detection path to the `nfs` component that catches NFS
hangs the existing read/write prober cannot — specifically the case
where
the NFS client kernel module is spinning on its own writeback locks
while
kubelet's main thread keeps reporting Ready.
When a hang is detected, the component flips to `Unhealthy` with a
`SuggestedActions{REBOOT_SYSTEM}`, which lets cluster-operator's
auto-fixer cordon and reboot the node automatically. Repeated hangs
across reboots are upgraded to `HARDWARE_INSPECTION` via the existing
`eventstore.EvaluateSuggestedActions` machinery, so this does not
introduce a reboot loop.
## Motivation
A worker node ran for 3.5 days in a state where:
- kubelet's main goroutine kept writing logs and reporting heartbeats
(node `Ready=True`)
- one CPU core was 100% pinned inside `nfs_lock_and_join_requests` →
`_raw_spin_lock`, triggered by a user-issued `mksquashfs` writing a
large file to `/nfs/home`
- any goroutine touching the filesystem (kubelet's pod admit / volume
mount / cAdvisor) blocked forever
- new pods could be scheduled to the node but never admitted
dmesg reported `watchdog: BUG: soft lockup` every ~28 seconds from the
first 26-second stuck event onward, but nothing was listening for it.
The current `components/cpu` matcher already recognises `soft lockup`
text but only records it as an event — it never affects health state.
This change makes that signal actionable, scoped to NFS so the
generic `soft lockup` semantics in `components/cpu` are unchanged.
## What's in this PR
Two commits, each independently buildable and testable:
1. **`feat(nfs): add kmsg matcher and hang evaluator for NFS hang
detection`**
Pure additions, no wiring:
- `kmsg_matcher.go`: four NFS-specific regexes (server not responding,
server OK recovery, lock reclaim failed, NFS writeback functions
in stack traces) + a `Match` function for `kmsg.Syncer`.
- `hang_evaluator.go`: `collectNFSHangEvents()` applies conjunction
rules — lock-reclaim failed counts unconditionally, "not responding"
is cancelled by a later "OK", writeback stack hints require ≥ 2
occurrences in the lookback window (which, combined with the
300-second dedup, means ≥ 5 minutes of sustained hang and filters
out one-shot sysrq dumps).
2. **`feat(nfs): wire kmsg-based hang detection into component health`**
Hooks the detector into the component:
- New fields `eventBucket`, `kmsgSyncer`, `rebootEventStore`,
`lookbackPeriod` (3 days, aligned with `eventstore.DefaultRetention`).
- `newComponent()` testable inner constructor; `kmsg.Syncer` started
on `linux + euid==0 + EventStore!=nil` with
`WithCacheKeyTruncateSeconds(300)`.
- `Check()` evaluates kmsg events before the prober. If a hang is
present, query reboot history, sort ascending (see note below),
and let `EvaluateSuggestedActions` decide whether to suggest
reboot or hardware inspection. If a reboot already happened after
the hang, fall through to the prober for live re-check rather than
reporting stale state.
- `Start()` now runs `Check()` immediately on the first iteration
instead of waiting one tick, matching `components/disk`.
- `Close()` shuts the syncer before the bucket.
- `Events()` returns bucket contents instead of `nil`.
- `checkResult.suggestedActions` surfaced through `HealthStates()`.
## Notable design points
- **Ordering quirk**: `Bucket.Get` and
`RebootEventStore.GetRebootEvents`
both return events in **descending** order (latest first), but
`eventstore.EvaluateSuggestedActions` expects **ascending** input
(see `pkg/eventstore/suggested_actions_test.go` case 4 where
`failureEvents[0]` is the earliest). This PR sorts ascending before
calling. `components/disk` happens to pass the descending slice
directly — that may be a latent issue worth addressing in a separate
PR but is out of scope here.
- **Lookback = 3 days** (not longer), because `EventStore` doesn't
support
per-bucket retention overrides and the global default is 3 days. Within
this window we get accurate reboot-vs-hang ordering; beyond it we'd
just be reading nothing.
- **kmsg `Message` preserves leading whitespace** (parsed by
`strings.SplitN(line, ";", 2)` in `pkg/kmsg/watcher.go`), so the
writeback regex uses `(^|\s)…` to tolerate the leading space typical
of kernel stack trace lines.
## Testing
```text
go test ./components/nfs/ -short -race -count=1
ok github.com/leptonai/gpud/components/nfs 1.7s
56 tests pass (all pre-existing + 25 newly added across matcher,
evaluator, and component layers). Highlights:
- Reboot-loop guard: TestCheckKmsgHangWithRebootLoop — two hangs
with two interleaved reboots upgrades to HARDWARE_INSPECTION.
- No-loop safety: TestCheckKmsgHangAfterReboot — hang followed by
a later reboot returns nil from EvaluateSuggestedActions and falls
through to the prober.
- Robustness: TestCheckNilRebootEventStore — nil reboot store
does not panic.
- Probe skip: TestCheckSkipsProberWhenHangDetected — confirms the
kmsg path returns before a probe is issued on a known-hung mount.
- Cold-start latency: TestStartImmediateFirstCheck — Start()
runs Check() before the first ticker fire (also fixes pre-existing
behaviour where the first health state would only appear after a
full minute).
Risk & limitations
- Repeated sysrq within 5 minutes could trigger a false positive
(two writeback stack hints needed). This is a controlled human
action and the resulting cordon is a safe side-effect.
- gpud's own probe goroutines are still vulnerable to NFS hangs
that occur mid-execution: the existing 5-second ctx.WithTimeout
cannot cancel an in-kernel NFS syscall. This PR mitigates by
skipping new probes on known-hung mounts but does not rescue
already-stuck goroutines. Pre-existing; out of scope.
- components/cpu is intentionally untouched: its existing
soft lockup matcher still only writes events and does not affect
health, since the generic CPU lockup signal is too noisy to act on
unconditionally.
Manual verification on a real node
# 1. Inject "server not responding" (no OK to cancel) -> Unhealthy
within 60s
echo 'nfs: server 192.168.1.100 not responding, timed out' | sudo tee
/dev/kmsg
sleep 65
curl -sk https://localhost:15132/v1/states | jq '.[] |
select(.name=="nfs")'
# 2. Single writeback stack hint -> still Healthy (below threshold)
echo ' nfs_lock_and_join_requests+0x61/0x2b0 [nfs]' | sudo tee /dev/kmsg
sleep 65
# 3. Second writeback hint 5+ minutes later (defeats dedup) -> Unhealthy
sleep 305
echo ' nfs_lock_and_join_requests+0x82/0x2b0 [nfs]' | sudo tee /dev/kmsg
sleep 65
# Expect: .health == "Unhealthy"
# .suggested_actions.repair_actions == ["REBOOT_SYSTEM"]
# .suggested_actions.description == "NFS hang requires immediate reboot;
drain may hang on NFS volumes"
---------
Co-authored-by: Gyuho Lee <gyuhol@nvidia.com>1 parent c570f0a commit 51edfea
6 files changed
Lines changed: 1121 additions & 10 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
9 | 12 | | |
10 | 13 | | |
11 | 14 | | |
| |||
16 | 19 | | |
17 | 20 | | |
18 | 21 | | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
19 | 25 | | |
20 | 26 | | |
21 | 27 | | |
22 | 28 | | |
23 | 29 | | |
24 | 30 | | |
25 | 31 | | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
26 | 37 | | |
27 | 38 | | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
28 | 43 | | |
29 | 44 | | |
30 | 45 | | |
| |||
42 | 57 | | |
43 | 58 | | |
44 | 59 | | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
45 | 74 | | |
46 | 75 | | |
47 | 76 | | |
48 | 77 | | |
49 | 78 | | |
50 | 79 | | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
51 | 96 | | |
52 | 97 | | |
53 | 98 | | |
| |||
73 | 118 | | |
74 | 119 | | |
75 | 120 | | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
76 | 150 | | |
77 | 151 | | |
78 | 152 | | |
| |||
96 | 170 | | |
97 | 171 | | |
98 | 172 | | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
99 | 177 | | |
100 | 178 | | |
101 | 179 | | |
102 | 180 | | |
103 | 181 | | |
104 | | - | |
105 | | - | |
106 | 182 | | |
107 | 183 | | |
108 | 184 | | |
| |||
115 | 191 | | |
116 | 192 | | |
117 | 193 | | |
118 | | - | |
119 | | - | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
120 | 203 | | |
121 | 204 | | |
122 | 205 | | |
123 | 206 | | |
124 | 207 | | |
125 | 208 | | |
126 | 209 | | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
127 | 217 | | |
128 | 218 | | |
129 | 219 | | |
| |||
140 | 230 | | |
141 | 231 | | |
142 | 232 | | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
143 | 285 | | |
144 | 286 | | |
145 | 287 | | |
| |||
275 | 417 | | |
276 | 418 | | |
277 | 419 | | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
278 | 424 | | |
279 | 425 | | |
280 | 426 | | |
| |||
339 | 485 | | |
340 | 486 | | |
341 | 487 | | |
342 | | - | |
343 | | - | |
344 | | - | |
345 | | - | |
346 | | - | |
347 | | - | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
348 | 495 | | |
349 | 496 | | |
350 | 497 | | |
| |||
0 commit comments