Skip to content

ec: scrub a stripe when a read finds a bad block in it - #1100

Open
aderk wants to merge 11 commits into
koverstreet:ec_read_aroundfrom
aderk:ec-read-around-scrub
Open

aderk wants to merge 11 commits into
koverstreet:ec_read_aroundfrom
aderk:ec-read-around-scrub

Conversation

@aderk

@aderk aderk commented Oct 5, 2026 •

Copy link
Copy Markdown

Rebased onto #1116; one commit on top, +175/-2.

A read-around whose result fails the extent checksum read a bad block
but can't say which, so every later read-around of that stripe fails and
falls back to the slow device until scrub gets there. This queues a scrub
of the stripe from bch2_rbio_error(), where the failure is counted, and
from bch2_ec_read_extent() for blocks a reconstruct names. The worker
enters scrub_stripe_block() through the block's backpointers, so the
check and repair are scrub's own.

This is a bit janky: the queue is in memory, 16 slots, and a stripe
scrubbed in the last 60 s isn't queued again, since a repair that
narrows the stripe leaves the bad block until reconcile evacuates it. A
full or lost queue costs only the slow fallback until a scheduled scrub.
The worker runs at once on write_ref_wq, outside reconcile; its moves are
idle IO, but the stripe reads and parity writes go through
bch2_ec_block_io() at the default priority. Each entry holds a write
ref, and each scrubbed block does a write buffer flush.

Possible follow-ups: a persistent "needs scrub" marker on the stripe,
run by reconcile, which survives a remount and drops the queue, the
timer and the write refs; or, smaller, dedupe on the stripe until its
repair finishes, in place of the 60 s timer.

Tests: ktest#137. Three stripes with bad parity and data on
a slow member: after one read no bad block is left, and the second
read's 78 read-arounds all succeed (39 of 39 with data_checksum=none).
The repair reads the bad block, so the scrub tests and
ec_read_around_fallback now expect stripe_read_csum_err.

aderk added 11 commits October 7, 2026 00:52
817bade ("read: track which ways a read has been attempted, per
device") made a read-around the ec read mode, so a failed one marks ec
as tried and bch2_bkey_pick_read_device() clears has_ec for the pointer.
If the direct read after it fails too, no reconstruct follows and the
read fails. ktest ec_read_around_fallback_reconstruct, with the slow
member's block and one more data block of a 4+2 stripe bad, gets EIO
where it recovered before.

The two ec reads aren't the same attempt. A read-around reads k blocks
over the extent's range, checked only by the extent's checksum, so it
can't tell which block is bad; the full reconstruct reads every block
in whole checksum granules, finds the bad one by the stripe's checksums
and rebuilds with the parity. So give the read-around its own mode,
BCH_READ_MODE_ec_read_around: a failed one marks that tried, the direct
read follows, and a failed direct read gets the full reconstruct. Each
mode is still tried at most once per device.

A failure in either ec mode goes to the ec error, so self healing and
damage records don't see a read-around that found a bad block elsewhere
in the stripe. bch2_ec_read_optional() was that distinction; the mode
says it now.
ec_read_around_blocks(), for the read path, and bch2_ec_read_around_skip(),
for stripe repair and reuse, each collected the stripe's device latencies
and left out the slowest blocks, by rules that could drift apart: one
dropped the slowest down to k with the highest index winning a tie, the
other skipped blocks of the required set while they beat the penalty,
with the lowest.

One function, ec_read_around_slow(), does both. The blocks to read are
the k fastest that can be read now, and a block is slow when its device
is slower than the slowest of those by the penalty plus the least gain
worth having. A read-around of a block is picked when it's slow, and
reads those k; repair and reuse rebuild a required block that's slow
from the same k, where before they read every other block. The read
after all of what was skipped, when the rebuild doesn't check out, is
unchanged.

The penalty comes from the option inside the function. Reconcile's 16x
applies in bch2_ec_read_around_pick()'s own comparison, which is what
decides.
bch2_rbio_retry() had its own message path for a read-around that
failed and whose direct read then succeeded, with the failure counter in
it: a copy of the retry message, under a different condition, and not
reached when the direct read failed too and the full reconstruct
recovered the data, although a read-around failed there as well.

Count data_read_ec_read_around_fail in bch2_rbio_error(), where every
failed read-around arrives: from bch2_ec_read_extent() when the stripe
read fails, and from the extent's checksum check when a block it read
was bad. That's the one place that already knows a failed read-around
isn't the device's error. The retry's single message path says what was
found, as it does for any recovered retry; since a read-around failure
isn't a device error, the accounting after it is untouched.
bch2_rbio_retry() special-cased the read-around in two places: the
trace at the top, where it counted data_read_ec_read_around instead of
data_read_retry, and the error accounting, where it took whether the
failure was a checksum error from the failures instead of from the
original error, which is 0 for a read-around.

Count the read-around where it's decided, in __bch2_read_extent(),
and let the retry trace every retry as such: a forced reconstruct is
already a retry by that count. The failures say what any retry
recovered from, since the retry marks its original error into them
first, so take the checksum verdict from them for every retry. Then
the original error and the read-around bool have no users left.
A read-around of a checksummed extent reads only the extent's range of
each block, so a61951b ("ec: read around a device much slower than
the rest of its stripe") gave bch2_ec_stripe_buf_init() a flags
argument, and since eb862ca ("ec: a read-around checks for stripe
buffer room when it's picked") the enum has one flag, with a wrapper for
the other callers, all to set the buffer's unaligned field.

The caller sets up the buffer's key before init; have it set unaligned
the same way, and init test the field. The init signature is back to
what it was.
bch2_ec_read_extent() left the block out of the read only when a direct
read of it had failed, and read it with the rest of the stripe otherwise:
for a device that went offline before the read was picked, and for
force_reconstruct_read. The former fails the block at once either way;
the latter read every block, found nothing bad, and returned the block's
own data without going through the parity.

Rebuild the block from the others in every mode but a read-around,
which chooses its own blocks, and keep the read after all for when they
can't. That drops the check of what was tried, and force_reconstruct_read
reconstructs.
… once

ec_read_around_slow() left out devices with a failure recorded against
them, but none of its callers can have one: bch2_ec_read_around_pick()
only reads around in the first pass of a read, where there are no
failures yet, and in the retry that carries its decision, whose failures
are empty until that read-around runs. Repair and reuse pass NULL. Drop
the argument, and with it bch2_ec_read_extent()'s need for the failures:
it takes the message buffer again, as before the series.

The retry carrying a decision ran the latency checks again before
honouring it. Handle the retry first: it reads around if it was decided
and hasn't been tried, and otherwise doesn't, so the rest of the
function is the first pass's decision, unnested. The selection takes
the penalty and returns the cost, which the pick compared against after
computing it from the latency the selection returned; reconcile's 16x
now selects the slow blocks too, as it decides.

has_ec is cleared with the other per-failure fields, where the line it
replaced was.
The read path, stripe reuse and stripe repair each had a loop that read
the blocks it wanted and marked the rest skipped, and each had a copy of
the read after all: clear the post-recovery errors, clear the block's
error, issue the read.

bch2_stripe_buf_read() reads the blocks in a mask and marks the rest
skipped; the blocks a first round doesn't need are erasures too, which
only matters if a validate runs before they're read, and none does.
bch2_ec_block_io() clears the block's error at issue, as reading it
again replaces the result, and validate clears the post-recovery errors
it's about to recompute. The read after all is then just the reads.

stripe_repair_read() reads the skipped blocks on the path that reads the
rest when a required block is bad, and the caller validates the whole
stripe as it does then, instead of the read checking the required
blocks itself.
A read-around is decided in the first pass and done by the retry it's
punted to, and 8ef50cee8d6c ("ec: promote read-around results") had the
first pass clear BCH_READ_may_promote so as not to allocate a promote
the punt frees, then put it back on the rbio for the retry to find.

Have promote_alloc() return nothing for a read that will be punted,
which a forced reconstruct is too: the flags carry across the punt
untouched. The check for a retry with nothing to repair is gone: a
retry only gets here with a failure to heal or with may_promote, which
the read-around retry keeps. __promote_alloc() gates on a failure the
same way, so promote_alloc() passes the failures through.
Comments that restated the code or argued for it at commit-message
length are cut to the reason, where the code doesn't give it. The option
help says the same in fewer words.

The latency helper is an atomic64_read; have_io_error() is a
darray_find_p(); the pick checks that the pointer is read directly, and
sets the flag that carries its decision to the retry, so the call site
is the call, and the trace names the stripe.
A read-around of a checksummed extent checks the data it rebuilt
against the extent's checksum. When that fails, one of the blocks it
read is bad, and nothing says which. Until scrub gets to the stripe,
every read-around of it fails the same way and falls back to the slow
device. A reconstruct that finds a block failing the stripe's
checksums names it, and leaves it bad too.

Queue those blocks for a scrub: after a read-around's checksum error,
where it's counted as failed, every block but the one read around;
after a reconstruct, the blocks it named. A worker on write_ref_wq
walks each block's backpointer with verify_stripes, so
scrub_stripe_block() checks it and repairs the stripe as scrub does.
The queue holds 16 stripes. A stripe scrubbed in the last 60 s isn't
queued again: a repair that narrows the stripe leaves the bad block in
place until reconcile has evacuated it, and reads keep failing until
then. Each entry holds a write ref, and the worker drops what's left
when the filesystem goes read-only.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant