ZFS: re-read a transitional pool state before failing the monitor - #2213
ZFS: re-read a transitional pool state before failing the monitor#2213lhjnano wants to merge 4 commits into
Conversation
The kstat path was written out at four call sites and the health read at two, with only one of them falling back to zpool list. Put both behind zpool_kstat_state() and zpool_read_health(), and lift the operation timeout clamp out of zpool_settle() so a second caller can use it. The kstat root becomes a variable so the agent can be driven against a fake tree by a test. It is not an OCF parameter and is not advertised in the metadata; the default is the real path and nothing sets it. No behaviour change: meta-data output is byte-identical.
ClusterLabs#2206 returned from the settle on the first ONLINE reading, but the post-import state does not transition once to ONLINE: it oscillates while the vdevs are reopened. Sampling on our hardware measured 1210 transitions over 65 minutes, OFFLINE bursts of 4ms median and 78ms maximum, concentrated in the first ~0.5s. A single sample therefore reads ONLINE about 93% of the time while the pool is still unsettled, the wait returned immediately, and the monitor pacemaker starts the moment start returns could still land on a burst. Wait for three consecutive healthy reads 100ms apart instead. A pool that is already settled still costs nothing, because the state is read before any waiting, and the wait still never fails the start.
The pool state kstat is a lock-free snapshot. A monitor that samples it while the pool is being imported, or while a vdev is being replaced, can read a transient OFFLINE, or an empty string if the read raced an export. Pacemaker sees OCF_ERR_GENERIC, and with a low migration-threshold that single sample is enough to move the resource off a healthy node. Re-read the state before deciding. A pool that is really broken keeps reporting the same value, so the retry costs nothing in that case; one that was caught mid-transition settles within a few samples. The retry budget is bounded by monitor_settle_ms and clamped to the operation timeout, so the monitor cannot overrun its own deadline. Set monitor_settle_ms=0 to keep the previous behaviour of failing on the first reading.
d2dd4eb to
8c4adae
Compare
|
Can one of the project admins check and authorise this run please: https://haci.fast.eng.rdu2.dc.redhat.com/job/resource-agents/job/resource-agents-pipeline/job/PR-2213/1/input |
oalbrigt
left a comment
There was a problem hiding this comment.
The agent changes looks good.
I've added some suggestions for the test script.
| check: $(ocf_SCRIPTS:=.check) | ||
| EXTRA_DIST += tests/ZFS.test.sh | ||
|
|
||
| check: $(ocf_SCRIPTS:=.check) check-ZFS |
There was a problem hiding this comment.
We do not have ZFS in our CI, and most of the users dont use it in their build envs, so we only want it as a separate check, like you've done below.
There was a problem hiding this comment.
Done — I dropped the heartbeat/Makefile.am change entirely rather than just
taking it out of check, since the test now lives under tools/ocft and
there is nothing left for that file to wire up.
Drives the agent against a fake kstat tree and a stub zpool, so it needs no ZFS, no pool and no storage of any kind - only the temporary directory it creates and removes itself. That makes it cheaper to run than most of the existing cases, so it is listed in runocft.prereq without a prerequisite. Covers a settled pool, a degraded one, a transient OFFLINE, an empty reading, a pool that really is offline, that monitor_settle_ms=0 restores the previous behaviour, and that both new parameters are rejected when they are not numbers.
8c4adae to
bd3fd29
Compare
|
Can one of the project admins check and authorise this run please: https://haci.fast.eng.rdu2.dc.redhat.com/job/resource-agents/job/resource-agents-pipeline/job/PR-2213/2/input |
Follow-up to #2206.
#2206 made
startwait for the pool state to settle, and that part works.But it only covered the start path, and the failure I was actually chasing
happens on the monitor path — so #2206 on its own did not fix it. Sorry for
coming back so soon after it was merged; I should have looked at the monitor
in the same pass instead of splitting it across two rounds.
The problem
/proc/spl/kstat/zfs/<pool>/stateis a lock-free snapshot. A monitor thatsamples it while the pool is being imported, or while a vdev is being
replaced, can read a transient
OFFLINE, or an empty string if the readraced an export.
zpool_monitormaps anything that is notONLINE/DEGRADED/FAULTEDtoOCF_ERR_GENERIC, so that single sample isreported to pacemaker as a resource failure. With a low
migration-thresholdit is enough to move a perfectly healthy pool off itsnode.
We hit this in production. On one node we logged 1210 ONLINE/OFFLINE
transitions in 65 minutes (median 4 ms, longest burst 78 ms). About 93% of
the samples read
ONLINEfirst, which is why this looks like a rare flakerather than a broken pool.
The window is small, but it is only reachable while the pool is in
transition and a monitor lands in the same instant. A plain
zpool export/zpool importloop does not reproduce it — I ran 100import cycles and 3273 samples without a single bad reading. It needs the
transition and a concurrent monitor together.
The fix
Re-read the state before deciding. A pool that is really broken keeps
reporting the same value, so the retry costs nothing in that case; one that
was caught mid-transition settles within a few samples.
The retry budget is bounded by a new
monitor_settle_ms(default 500 ms)and additionally clamped to
OCF_RESKEY_CRM_meta_timeoutminus a smallmargin, so the monitor can never overrun its own deadline. Setting
monitor_settle_ms=0restores the previous behaviour of failing on thefirst reading.
Measured effect
Same node, same script, 60 import/export transitions with a concurrent
monitor loop:
Caught in the act. On an unrelated test cluster, running the patched agent,
the monitor sampled a transitional state twice and recovered both times:
Pacemaker recorded
Result of monitor operation ... okfor both. Withoutthe re-read those two samples would have been OCF_ERR_GENERIC on a healthy
pool.
Note the first pool:
start settled on 'ONLINE'was logged, and the poolstill read OFFLINE from a later monitor. Waiting on the start path is not
enough on its own, which is what this series is for.
Across the same run: 22 starts, all settled, no warnings, 2 re-reads, 0
monitor failures.
The oscillation itself is not specific to one machine. Sampling the kstat
every 10ms from the moment each pool appeared, on a different cluster
again:
Three bursts, all 15-16ms wide and all within 300ms of the import. Against
a 5s monitor interval that is a small chance of collision per transition,
which is why this reads as a rare flake in the field rather than as a
reproducible bug.
Commits
ZFS: read the pool health in one place— pull the kstat/zpool listreading intozpool_read_health, and make the kstat root a parameter (kstat_root) so it can be pointed at a fixture. Pure refactor:meta-dataoutput is byte-identical to the base.ZFS: require a stable state before start returns— rework #2206's settle loop on top of the above, so start and monitor share one implementation, and log the state it settled on. A successful settle used to leave no trace at all, so the only way to tell it had run was to time the agent by hand; this runs once per start, not per monitor.ZFS: re-read a transitional state before failing the monitor— the actual fix, plusmonitor_settle_msand its validation.ZFS: add a test for the transitional-state handlingCommit 1 is deliberately separate so the behaviour change in 3 is small and
readable on its own.
Tests
heartbeat/tests/ZFS.test.shdrives the real agent with azpoolstub onPATHand a fake kstat tree, so it runs anywhere — no ZFS, no pool, noroot, no
tools/ocftsetup. It is wired intomake check, so it runs in CIwith the rest of the suite rather than being something a reviewer has to
take my word for.
The
heartbeat/Makefile.amhunk is only what wires that in:EXTRA_DISTfor the new file and a
check-ZFStarget added to the existingcheckdependency. If you would rather this lived under
tools/ocft, or not run incheckat all, say so and I will move it.On the size
This is larger than I would like for a follow-up — about 310 added lines,
roughly half of it the test, and most of the rest commit 1, which moves
existing code around without changing behaviour.
I split it this way so the behaviour change would stay small on its own,
and so the fix would be verifiable in CI rather than only described in a
commit message.