Build note: This is one component of the raidkm mdraid stack and is not meant to be built on its own. Please use mdraid-super to build the entire package — it assembles this repo together with the other components in the correct order.
Operators:
docs/raidkm-field-manual.mdis the task-ordered companion to this README — the feature catalogue, the layout maps, and every command for create / scrub / rebuild / grow / shrink in one place. This README stays the canonical record of design decisions, milestones and measurement provenance.
A Linux md personality implementing arbitrary k+m Reed-Solomon
erasure coding, built as a fork of our optimized
mdraid's raid5.c
plus our ISA-L fork's
EC primitives (GFNI when the CPU has it, table-lookup
GF_REGION_MUL fallback otherwise).
Registered as a personality at level 71 under the name
raidkm. All m use the ISA-L gf_gen_rs_matrix Reed-Solomon
code: m≥3 calls ISA-L's ec_encode_data_* directly, while m=2 is
computed with raid6's tuned SIMD (raid6_call) — which produces
byte-identical parity to the ISA-L m=2 encode (see "Architectural
commitments" below). Parity placement is selectable per array via
--layout: rotating (the default — generalized left-symmetric,
spreads parity and read traffic across all members, matching stock
RAID6's own default placement), parity-last (dedicated parity
on the tail m disks, which keeps the cheap offline add-a-parity grow),
or declustered (narrow k+m groups scattered over a wider pool by a
balanced permutation, with a distributed spare — so a single-disk
rebuild is parallelised across the whole pool instead of bottlenecked on
one replacement; see Declustered parity below). The parity-disk count
m is set separately with --parity-count.
The earlier standalone implementation lives on the proto
branch and the v0-proto tag. Benchmarks of that prototype
against the raid5.c-derived line motivated switching to the
fork-and-extend approach this branch takes.
These are load-bearing for the design and not up for negotiation:
-
k+m semantics from the start, including m=2, with an on-disk format that stays reshape-compatible to arbitrary m. Parity is the ISA-L
gf_gen_rs_matrixReed-Solomon code in PARITY_N layout for every m. At m=2 that code's first two rows are exactly P=XOR and Q=Σ2ⁱ·Dᵢ over GF(2⁸)/0x11d — identical to raid6's P+Q — so we compute m=2 with raid6's tuned SIMD (raid6_call) for speed while writing byte-identical parity, keeping the m=2 image a valid prefix of the m≥3 ISA-L encoding (so "add a parity" stays incremental). raid6's hardcoded math alone is a dead end for m>2, which is why m≥3 uses ISA-L directly. Verified byte-for-byte: see the EC-verifier milestone below.Encode and decode are asymmetric. The m=2 encode above runs raid6's XOR + shift P+Q (
raid6_call.gen_syndrome— AVXVPXORfor P, the shift/ mask GF(×2) for Q). Decode (degraded read, rebuild, degraded-write reconstruct) instead routes through one unified path for every m (ops_run_compute_km): build the survivors' decode matrix, invert it withgf_invert_matrix, and apply it with ISA-L'sec_encode_data_*— GFNI (ec_encode_data_avx512_gfni/avx2_gfni) when the CPU has it, else the scalarec_encode_data_basetable lookup. Decode is deliberately PSHUFB-free: it never uses raid6's*_recov(the inherited 2-failure path raidkm doesn't reach) nor ISA-L's PSHUFB kernels, avoiding the StreamScale patent surface. The two SIMD selections are independent: GFNI decode comes straight fromisal_lib(gated onisal_have_*gfni()), so it needs noraid_isal.ko— that optional override only swaps the m=2 encoderaid6_callto a GFNI P+Q and does not touch decode. -
Forked from raid5.c, not patched in place.
raid_km.cis a copy + modify ofraid5.c. Stock raid4/5/6 stay untouched in the kernel; we accept the maintenance cost of porting upstream raid5 fixes manually. -
m=2 AND m=3 validated before declaring any milestone done. m=2 because it's the most-tested case and lets us cross-check against stock raid6 behavior; m=3 because it's the first case that stock raid6 can't do at all.
| Milestone | State |
|---|---|
Prototype preserved at v0-proto tag / proto branch |
✅ done (2026-05-23) |
| Master cleared, scaffolding committed | ✅ done (2026-05-23) |
Fork raid5.c → km/raid_km.c, symbols renamed, builds clean |
✅ done (2026-05-23) |
| Loads as a personality at level 71, coexists with stock raid456 | ✅ done (2026-05-23) |
| m=2 array activates natively at level 71, no shim | ✅ done (2026-05-23) |
| Basic I/O works (writes + reads) | ✅ done (2026-05-23) — 1.9 GB/s write, 7.9 GB/s read on brd |
| Resync / scrub path works | ✅ done (2026-05-23) — completes cleanly in ~2.4 s for 2 GiB |
| Standard benchmark passes vs stock raid6 | ✅ done (2026-05-23, re-measured 2026-05-29) — 1.34-2.35× at default, up to 1.43-2.28× tuned; re-measured on RHEL 10.2 across base / AVX2-GFNI / AVX-512-GFNI (2026-06-15): raidkm wins every workload — 1.35-2.43× scalar, up to 4.20× on AVX-512-GFNI (see table below) |
| Reliability soak: long fio randwrite+verify, disk-fail mid-I/O, scrub/repair | ✅ done (2026-05-25) — ~230 GiB crc32c-verified across healthy/2-fail/3-fail; a member failed mid-fio survives with correct data; scrub mismatch_cnt=0; surfaced + fixed an nsrc<k WARN on mid-I/O failure |
| Reliability: hot-replace / rebuild onto a spare | ✅ done (2026-05-25) — fail a member, mdadm --add a spare, recovery reconstructs (data via decode, parity via re-encode) and writes the rebuilt member; validated for data-disk, parity-disk, and rebuild-while-still-degraded — spare holds correct bytes, scrub mismatch_cnt=0 |
| Write-intent bitmap (unclean-shutdown resync only the dirty bits) | ✅ done (2026-05-25) — internal bitmap created/assembled on level 71; writes set dirty bits and endwrite clears them after settle; a write-while-degraded marks only the affected region dirty (3/128 chunks for an 8M write); --re-add does a bitmap-scoped recovery (17s vs 171s full rebuild, throttled) with correct data; a fresh-spare --add still does a full rebuild (not wrongly scoped). Inherited from raid5.c — no kernel changes needed |
| PPL (partial parity log) — closes the raid5/6 write hole | ✅ done (2026-05-25) — opt-in, off by default (enable with mdadm --create … --consistency-policy=ppl). Extended raid5-only PPL to arbitrary m: logs all m partial parities (raid5 logs only the single XOR P). RMW copies the m parity pages (prexor already leaves them = partial parity); RCW encodes the not-overwritten data (raid6 at m=2, ISA-L at m>2); recovery rebuilds every parity as P_j = PP_j XOR encode_j(modified). Validated: 2 power-loss crash tests (virsh destroy mid-fio) replay the log on reassembly, mismatch_count=0, post-recovery scrub clean. Cost (opt-in): −43% to −72% on brd — the inherent serialized FUA log write, exaggerated by RAM-speed backing; arrays without --consistency-policy=ppl are unaffected. Mutually exclusive with the write-intent bitmap |
k+m via ISA-L (replace raid6_call for m ≥ 3) |
✅ done (2026-05-24) |
| Full-stripe write works for m=3 | ✅ done (2026-05-24) |
| Small RMW writes for m=3 | ✅ done (2026-05-24) — ec_encode_data_update_* RMW |
m=2 byte-identical to ISA-L via raid6_call fast path (reshape-compatible, full perf) |
✅ done (2026-05-24) |
| EC correctness verified (encode == ISA-L + every erasure reconstructs) | ✅ done (2026-05-24) — verifier 18/18 at m=2 (k=2..10, full + RMW), m=3 spot-checks |
| m-way scrub / resync / repair for m ≥ 3 | ✅ done (2026-05-24) — synchronous ISA-L re-encode + compare; detect→repair verified on GFNI + base |
Degraded reads / recovery for m ≥ 2 (m-way decode via gf_invert_matrix) |
✅ done (2026-05-24) — survives up to m failures on read; verified 1/2/3-fail on GFNI + base |
mdadm --create / --assemble for level 71 (persistent v1.2 superblock) |
✅ done (2026-05-25) — patched mdadm 4.4; data round-trips stop→assemble for m=2/3/4; degraded assemble works |
mdadm --grow --add-parity (alias: --add) to add a parity disk, m → m+1 (PARITY_N: offline grow-via-resync; rotating: online COW reshape) |
✅ done — PARITY_N (2026-05-25): offline recreate, data + UUID preserved across m=2→3 and m=3→4; no data movement (parity appended, existing data stays put). Rotating — now an online, journaled COW reshape (2026-06-09; merged to master 2026-06-11): in-kernel, in-place (constant data_offset, no stripe-cache), EC-correct, no backup file, and crash-safe — a power loss mid-reshape is recovered by a plain mdadm --assemble replaying the in-kernel journal. Each band is staged out-of-place before its home is overwritten, so the read/write location-aliasing race that sank the earlier in-place attempt (withdrawn 2026-06-01) is structurally gone. The pre-COW offline windowed relocation is retained as a fallback for kernels without the COW engine (MDADM_RAIDKM_OFFLINE_ADDPARITY). Validated whole-array m=2→3 (base) + m=3→4 Cauchy (GFNI) — data + scrub + new-m-degraded-read oracle — plus a true power-loss crash; reshape crash/fault suite 114/0 on base+GFNI. See the --add-parity section below |
mdadm --grow --add-data to add a data disk (online reshape, capacity grow at fixed m) |
✅ done (2026-05-26) — drives the inherited kernel online reshape (delta_disks, fixed max_degraded); relocation rides the layout-aware sector map, no backup file needed (a grow has writepos < readpos). The data-disk count k changes, so the ISA-L EC matrix/tables are rebuilt for the new k in raid5_start_reshape; the old-k set is kept as prev_ec_* and selected per stripe by k (raidkm_a_matrix/raidkm_g_*) for the duration of the reshape, since I/O to pre- vs post-reshape_position stripes uses both geometries (freed at finish_reshape; rebuilt at mid-reshape assembly). Both layouts. mdadm freezes the array before the spare-add (else the raid_disks write races md_check_recovery → EBUSY). Validated by degraded-read-after-grow (not just scrub, which masks a stale-table bug): PARITY_N+rotating × m=2/3/4 max-degraded, plus m=3 grown to k=6 — all reconstruct correctly; GFNI-checked |
| Rotating parity layout (balance disk usage; parity-last is dedicated-parity) | ✅ done (2026-05-26) — selected via --layout=rotating (generalized left-symmetric: the m-slot parity block rotates one disk per stripe so parity — and normal-read traffic — spread across all members instead of the tail m). One slot mapping serves both layouts (pd_idx is the only layout-specific value; parity-last is the pd_idx==k case), so encode/decode/scrub/PPL are layout-agnostic. Layout packed into the superblock layout field (low byte = m, bit 0x100 = rotating). Rotating later became the create default (2026-06-01); the parity-last placement is bit-for-bit unchanged. Validated: m=2/3/4 create/read/scrub=0, degraded read+write (2- and 3-disk loss), stop→assemble persistence, PPL+rotating partial-write scrub=0, rotation confirmed by raw-disk compare. Add-parity is supported on both layouts but at different cost: PARITY_N appends a parity disk cheaply (offline grow-via-resync, no data movement), while rotating must relocate every block, so its add-parity drives a full online COW reshape (journaled, out-of-place per band, no backup file; an earlier in-place online reshape was withdrawn for a location-aliasing race) — see the --add-parity row above |
| Degraded write + degraded-array scrub (any m ≥ 2, up to m failures) | ✅ done (2026-05-25) — reconstruct failed data from k survivors, re-encode all parity, write the surviving members; data correct + no deadlock across m=2/3/4/5 × 1–m failures (data-only / parity-only / mixed / max-degraded) |
| GFNI cross-validation of degraded write + recovery | ✅ done (2026-05-25) — degraded-write matrix + hot-replace rebuilds repeated on an i5-1340P (GFNI): 10/10 pass, exercising the ec_encode_data_avx2_gfni path. The KVM testbed has no GFNI, so this is the only coverage of the GFNI EC variants under the new write/recovery scheduling |
Device-mapper: drive raidkm via the kernel dm-raid target (dmsetup) |
✅ done (2026-06-05) — level 71 is reachable through the in-tree dm-raid target with no new dm target (dmsetup create … raid raidkm <chunk> parity_count <m> …); m + rotating ride in the dm table, a FEATURE_FLAG_RAIDKM superblock bit keeps stock dm-raid from touching a raidkm SB. Phase 1 (create + I/O + degraded + scrub + reassembly) and Phase 2 (rebuild via reload + rebuild <idx>) validated 21/21 base + 51/51 GFNI, m=2..6. Reshape via dm is gated off (a hand-driven dmsetup grow corrupts — needs LVM's data-offset positioning). The dm-raid.c changes live in the mdraid fork. See notes/dm-raid-design.md |
LVM-managed raidkm (lvcreate --type raidkm) |
✅ done (2026-06-05) — the lvm2 raidkm fork provisions level-71 LVs via two segtypes raidkm (rotating) / raidkm_n (parity-last) carrying parity_count (m). Validated base + GFNI, m=2/3/4: create/activate/I/O/reassembly/degraded; lvconvert --repair (raidkm-aware leg replacement + rebuild); and dmeventd monitoring + auto-repair (level-agnostic plugin, no code change). Reshape via dm/LVM is out of scope (the data-offset out-of-place reshape doesn't fit raidkm — mdadm-only); the kernel reshape gate stays on. See notes/dm-raid-design.md |
| Checksum-driven self-healing — reconstruct silently-corrupt blocks from parity, up to m per stripe | ✅ done (2026-07-02) — driven by a per-block integrity signal (md-kmec's own native checksums, next row, or a stacked dm-integrity; T10-PI passthrough next), md-kmec turns an integrity-flagged read error into an m-erasure reconstruction: the corrupt block is rebuilt from parity and rewritten, on both the read path and the m-way scrub, with mixed data+parity corruption healed in a single repair pass (durable R5_IntegrityHeal marker). A read-only healed_blocks sysfs counter reports repairs. Validated to m=8 — heals 8 simultaneous silent corruptions in one stripe (data-only, parity-only, and mixed 4+4), deterministically, beyond RAID-Z3's 3. The integrity layer supplies the detection; md-kmec supplies the reconstruction. See tools/raidkm-test-selfheal.sh (runs on native checksums with NATIVE=1, or on dm-integrity) |
| Native per-block checksums — built-in per-4K CRC-32C integrity, no stacking required | ✅ done (2026-07-14) — mdadm --create … --checksum=crc32c (bare --checksum also works; --integrity=crc32c is a retained alias) gives every 4 KiB block a CRC-32C that raidkm computes, stores, and verifies itself. CRCs persist in a reserved region at each member's tail (~0.1% of capacity, carved at create; SB layout bit 0x200) in self-checking pages (per-page CRC + generation trailer, so a rotted region page is detected and dropped, never trusted), served through a bounded demand-paged cache, auto-sized at array start to cover every member's whole CRC region (~1 page per 4 MiB of member capacity; clamped to ~1.6% of RAM with a logged hint when a huge array is clamped below full coverage — undersizing costs region-page faults, never correctness; override via raidkm_csum_cache_pages). Reads verify inline in the bio completion (lock-free expected-CRC lookup + crc32c, the dm-integrity inline-mode shape) including a verified chunk-aligned read bypass; stores land at write issue; a fast-path mismatch is never trusted — it is rechecked through the stripe cache before feeding the self-heal above. Real-NVMe validated (8× local SSD, m=2, fio direct): reads and writes at 96–101% of the no-checksum baseline (random read 100.0%), ahead of dm-integrity bitmap on all four workloads and of journal on writes (2.1–2.3×) and random read (1.26×), tying it on sequential read — with 0 false mismatches. Crash semantics: CRCs are recomputed over the resync window after an unclean shutdown (same class as dm-integrity bitmap; journal's crash-atomic tags are the stronger guarantee its ~2× write cost buys). Concurrency-hardened by a three-lens adversarial review (verify-fence ordering, workqueue isolation, rdev pinning). Composes with declustered parity (2026-07-18): the CRC region stacks after the rkdcl block in the member tail, CRCs are keyed by the physical pool disk (so a spare-redirected read still verifies against the disk it hit), and the copy-from-spare rebalance migrates each block's CRC with the bytes — gate tools/raidkm-test-declustered-csum.sh. Design + benchmark: notes/native-checksum-read-redesign-2026-07-14.md; gates: tools/raidkm-test-csum-thrash.sh (cache-eviction round-trip) and the self-heal suite in NATIVE=1 mode. Real-NVMe re-gate (2026-07-15) on 4K-logical local SSD under KASAN + lockdep: functional 12/12, csum-thrash, self-heal 60/60, randrw churn 0 WARNs, 0 splats — the re-gate found and fixed a skip_copy×checksum read/write invariant WARN_ON (need_this_block now defers a read overlapping a draining skip_copy write) |
mdadm --grow --raid-devices=<N-1> to REMOVE a data disk (shrink-data, online, m fixed) |
✅ done (2026-07-30) — the inverse of --add-data, via the same journaled per-band COW engine walked backwards (the narrower geometry packs data deeper per disk, so the walk starts at the end; every home write lands strictly below the still-live old rows — no backup file, no data_offset shift). One disk per invocation (v1). Array-size-first: shrink the filesystem, clamp with --grow --array-size=<kb> (mdadm computes and prints the exact value on refusal), then --grow --raid-devices=<N-1>; the freed member becomes a spare (remove + --zero-superblock before reuse). Crash-safe: a mid-shrink stop/power-loss recovers by plain --assemble (the journal resumes the descending walk). Native-checksum arrays included (band verify-src + CRC re-key). Both layouts, m=2/3 validated: data byte-exact, scrub clean, decode oracle at m, concurrent fio randwrite+verify across the moving frontier, crash/resume ×2. The declustered analogue — pool shrink by one group (--grow --raid-devices=<N-g>) and spare-count increase — shipped 2026-07-31 via the same backward engine (gates: tools/raidkm-test-declustered-shrink*.sh). Gates: tools/raidkm-test-shrink{,-crash}.sh |
Declustered parity — wide-pool layout with distributed spare (--layout=declustered) |
✅ done (2026-07-16) — narrow k+m groups scattered over an N-disk pool by a seeded, rotation-expanded balanced permutation (clean-room, dRAID-lineage combinatorics — Holland & Gibson design theory, no OpenZFS/CDDL code), with distributed spare columns instead of a dedicated hot spare. Capacity balance is exact by construction and the rebuild read/write load is rotation-symmetric, so a failed disk's reconstruction spreads across every survivor. Headline: rebuilding a failed 16 GB member on an 80-disk pool (g=13) took 44.9 s vs 785.1 s for a classic 78+2 array — 17.5× (it also beats a narrow 11+2 classic, because the distributed spare removes the single-writer bottleneck). Geometry via --group-width=g [--spare-columns=s] [--dcl-nbase=n] [--dcl-seed=x]; the permutation seed + geometry live in a per-member on-disk rkdcl metadata block (4 KiB tail reserve, crc + generation), the kernel regenerates the identical permutation from the seed. Constraint C1 (N−s) mod g == 0 (mdadm validates + suggests a legal s). Declustering is m-agnostic — validated m=2..5 on real NVMe (incl. the single-group-row ngroups=1 edge). Native per-block checksums compose (see the next row), and online pool expansion (mdadm --grow --raid-devices=N') widens the pool via a journaled crash-recoverable COW reshape (m=2/3 validated: data intact, scrub clean, degraded-read EC-correct, crash/power-loss/torn-write recovered); the per-group geometry reshapes (--add-parity / --add-data / --spare-columns=s') run online too, serving the un-migrated region with old-geometry stripes (native-checksum arrays included); capacity-shrinking reshapes — pool shrink by one group (--grow --raid-devices=<N-g>) and spare-count increase — run online via the same engine walked backwards, gated array-size-first; v1 still excludes a file-backed bitmap (refused, not run wrong). See Declustered parity below |
| Declustered population + rebalance — rebuild into / migrate out of the distributed spare | ✅ done (2026-07-17) — when a member fails, population reconstructs its content into the distributed spare columns as a raidkm-owned sync action (echo <idx> > /sys/block/mdN/md/rk_dcl_populate, or automatic with rk_dcl_auto=1), advancing a crash-safe journaled prefix mark (rkdcl v2, FUA+PREFLUSH every 16 MiB). Sequential multi-assignment (up to s failed disks, one populating at a time) uses chained redirects — a spare column can rotate onto another failed disk, so the slot→disk map resolves through the active assignments; the on-disk journal is adaptive v2/v3 (a published v2 module fails closed rather than silently dropping an assignment). Adding a replacement rebalances by copy-from-spare (16-worker parallel copy straight from the already-populated spare — no GF decode, array never degraded during it; per-band quiesce + offset-split, strict-journaled per band) instead of a decode rebuild, falling back to the validated decode leg on any persistent copy fault. With native checksums enabled the copy migrates each block's CRC from the spare disk's key to the replacement's alongside the bytes, so integrity is preserved end-to-end (a torn/rotted copy is caught on first read and healed by decode). Power-loss crash matrices (dm-flakey) green for population, second-population, and mid-copy resume. Hardened by two adversarial multi-agent reviews (16 findings fixed: journal quorum, arm-publication barriers, auto-arm rescan, a copy-abort progress-contract data-loss bug, wedge-to-decode fallbacks). Validated under KASAN + on stock-RHEL real NVMe. See Declustered parity below |
raid_km registers at level 71 but reuses raid5.c's existing m=2
code paths via a per-conf effective_level field set to 6 in
setup_conf. Internal "do raid6 math" checks (switch (conf->level) cases, conf->level == 6 branches) consult
conf->effective_level instead, so the verbatim raid5.c logic
fires for raid_km without mutating mddev->level (which md core
treats as invariant after md_run). Two further fixes were
needed to make it land:
effective_levelset order: must be assigned beforeraid5_alloc_percpu, because the CPUHP callback path (raid5_alloc_percpu → cpuhp_state_add_instance → raid456_cpu_up_prepare → alloc_scratch_buffer) reads it to decide whether to allocate the per-CPUspare_pageused for P+Q syndrome validation. If set later,spare_pagestays NULL andasync_pq.chits BUG_ON during resync.- CPU-hotplug slot:
raid5_alloc_percpuandfree_scratch_bufferoriginally referenced the fixedCPUHP_MD_RAID5_PREPAREenum, which stock raid456 already reserves. raid_km asks for a dynamic slot viaCPUHP_BP_PREPARE_DYN(stored inraid_km_cpuhp_state) and both call sites use that instead.
Out of the box — stock raid6 at its default group_thread_cnt=0 vs md-kmec
raidkm at its defaults — tools/raidkm-standard-benchmark.sh --runs=3, 16 × brd
ramdisks, k=14 m=2, 64 KiB chunk, both --assume-clean, on a GCP
c3-standard-22 (22 vCPU, RHEL 10.2 6.12.0-211.16.1.el10_2). raidkm
auto-defaults group_thread_cnt to nproc/2 (= 11 here) and now enables
zero-copy writes (skip_copy) by default; stock ships both off
(IOPS, mean of 3 runs; Test 6 = post-run integrity check, mismatch_cnt=0):
| Test | stock raid6 (gtc=0) |
md-kmec raidkm (default) | speedup |
|---|---|---|---|
| 1 Random 4K Write (RMW worst case) | 54,266 | 375,269 | 6.92× |
| 2 Database Mixed 75/25 8K | 106,072 | 868,263 | 8.19× |
| 3 High Concurrency 70/30 4K (16 j) | 177,730 | 1,210,715 | 6.81× |
| 4 OLTP 70/30 16K | 52,574 | 396,083 | 7.53× |
| 5 Partial Stripe Write 8K | 31,256 | 230,669 | 7.38× |
The out-of-box gap (~7-8× on this wide array) is dominated by md-kmec's
worker-group auto-default — stock raid6's RMW path is serialized at gtc=0,
while raidkm parallelizes stripe handling across nproc/2 worker threads. This
is a default-vs-default comparison; at matched gtc the structural-only
edge is much smaller (the win is the on-by-default tuning, see the SIMD table and
the vCPU-scaling note below).
md-kmec defaults skip_copy on (in raid5_set_limits): for a full-page-aligned
write the stripe-cache page is aliased directly to the incoming bio page instead
of being memcpy'd in through the biodrain step — and that copy, not the
erasure-coding (EC is ~0.6 % of write-path CPU), is the dominant cost of a
full-stripe write. The only requirement is stable pages, which is free for the
O_DIRECT workloads this fork targets; override per-array via the
skip_copy sysfs attribute.
With native checksums on, a chunk-aligned read is re-driven through the stripe
cache for verification (retry_aligned_read). If it lands on a block whose
skip_copy write is still draining — the stripe page is aliased to the write bio
and intentionally left non-UPTODATE until handle_stripe_clean_event() hands it
back to orig_page — the read is deferred in need_this_block() and served
from disk once the write completes (correct read-after-write). Serving it inline
would set R5_UPTODATE on a R5_SkipCopy block and trip that invariant's
WARN_ON; the deferral was added 2026-07-15 after the real-NVMe re-gate (below)
caught the race under randrw churn.
Full-stripe sequential DIO write (16-disk wide array above, bs=896k = one full
stripe, 3 reps):
skip_copy |
throughput |
|---|---|
| 1 (md-kmec default) | 13.33 GiB/s |
| 0 | 9.56 GiB/s |
— +39 %, matching the +36 % measured on stock raid6 with skip_copy forced
on. The win is confined to full-stripe writes; on small/partial RMW writes
the tiny copy is dwarfed by read-modify-write amplification, so skip_copy is
neutral there — 8 KiB partial-stripe random write, 4 reps each:
skip_copy |
IOPS per rep |
|---|---|
| 1 | 210k / 214k / 214k / 215k |
| 0 | 210k / 210k / 212k / 209k |
(no regression — within noise, marginally ahead). Note the standard-benchmark
suite above is entirely small random / RMW writes, so it does not exercise
the regime skip_copy helps; the full-stripe win shows on the large-sequential-write
path.
raidkm inherits raid5.c's plug/unplug path, and raid5_unplug() released every
plugged stripe under the global conf->device_lock — one acquisition per flush,
the dominant CPU cost under many concurrent submitters. md-kmec now routes those
releases through the lockless released_stripes llist (raid5_release_stripe(),
the same path used when unplugged), coalescing all submitters' releases into
batched drains by raid5d + the worker threads. No new lock or invariant —
functional suite 12/12, scrub mismatch_cnt=0, and the change is mirrored
to the perf-tree raid5.c (an upstream-ready md/raid5 fix that lifts stock
raid6 identically).
Throughput proof on raid6 14+2 — the change is in shared raid5.c, so these
numbers measure it directly on the mirrored perf tree; raidkm runs the identical
raid5_unplug path — 64 KiB chunk, group_thread_cnt=16, fio 4 KiB random write
numjobs=24 iodepth=32, base (without the change) vs patched, 3 interleaved
rounds, on null_blk (device-unlimited, so the md layer is the bottleneck), GCP
n2-standard-32 (32 vCPU, 2 NUMA):
| IOPS (4K randwrite) | CPU busy | |
|---|---|---|
| base | 221,067 | 92 % |
| patched | 276,478 | 84 % |
→ +25 % throughput at lower CPU; perf shows raid5_unplug → _raw_spin_unlock_irq 28 % of all CPU → 0. Neutral at the stock default
group_thread_cnt=0 (single-raid5d-bound): 0.98×, no regression.
On real NVMe (16 × GCP local SSD): 4 KiB random write is device-capped at
~199 K (≈ the lock ceiling), so IOPS ties and the win is CPU efficiency — at
matched group_thread_cnt, same ~199 K IOPS at 54 % busy vs stock raid6's
69 % (+28 % IOPS per %CPU). The faster the storage, the more the lock
dominates (28 % of CPU on null_blk vs 14 % on this NVMe), so the throughput
win grows on storage fast enough to clear the single-lock ceiling — the
very-wide-array (k+m ≥ 80) regime this fork targets.
raidkm m=2 vs stock raid6 with the canonical
tools/raidkm-standard-benchmark.sh --runs=3 (6-workload OLTP/IOPS suite; drops
page cache + dentries before every test; both arrays created --assume-clean so
neither resyncs during the run), 6 brd ramdisks, k=4 m=2, 512 KiB chunk, on
RHEL 10.2 (kernel 6.12.0-211.22.1.el10_2). Re-measured 2026-06-15 across
the full SIMD spectrum to separate the structural win from the ISA-L GFNI encode
(IOPS, mean of 3 runs; Test 6 = post-run integrity check, mismatch_cnt=0
everywhere):
| Test | base / no-GFNI (AMD Ryzen 5800X) |
AVX2-GFNI (Intel i5-1340P) |
AVX-512-GFNI (Xeon 8481C / GCP c3-standard-8, 8 vCPU) |
|---|---|---|---|
| 1 Random 4K Write | 239,211 vs 124,327 (1.92×) | 107,728 vs 46,615 (2.31×) | 305,853 vs 72,767 (4.20×) |
| 2 DB Mixed 8K (75/25) | 420,982 vs 275,658 (1.53×) | 182,964 vs 96,838 (1.89×) | 504,563 vs 157,899 (3.20×) |
| 3 High Concurrency 4K rw | 555,725 vs 410,337 (1.35×) | 219,223 vs 135,716 (1.62×) | 818,197 vs 220,291 (3.71×) |
| 4 OLTP 16K rw | 222,370 vs 124,760 (1.78×) | 88,546 vs 42,677 (2.07×) | 266,346 vs 73,455 (3.63×) |
| 5 Partial Stripe Write 8K | 179,735 vs 73,994 (2.43×) | 59,135 vs 24,053 (2.46×) | 159,960 vs 43,837 (3.65×) |
(Each cell is raidkm vs stock raid6 IOPS and the speedup.) raidkm wins every
workload at every tier, at ~2-4× lower latency. The win is structural: the
forked raid5.c carries our post-fork mdraid optimizations — worker-groups
auto-default, STRIPE_ON_INACTIVE_LIST lock-skip, and a faster write/RMW/
partial-stripe path.
⚠️ The ratio scales with vCPU/core count — it is not a fixed per-machine constant. raidkm's worker groups parallelize stripe handling across cores (total worker threads auto-default tonproc/2; see Tuning to change it), so raidkm throughput rises with cores, while stock raid6's RMW path is largely serialized and barely scales. Measured on the samemainbuild, same GCP c3 / Xeon 8481C, varying only the vCPU count (Random-4K-Write, 2026-06-19):
instance raidkm IOPS stock raid6 IOPS ratio c3-standard-4 (4 vCPU) 178,934 72,041 2.48× c3-standard-8 (8 vCPU) 324,958 83,852 3.88× So the three columns above differ as much by core count as by SIMD tier (the
Xeon 8481Ccolumn was a c3-standard-8 / 8 vCPU box). At m=2 parity is theraid6_callP+Q fast path, not the ISA-L GFNI encoder — GFNI does not change the m=2 numbers (verified 2026-06-19: loading the GFNIraid6_calloverride moved m=2 IOPS <1%; the 8-vCPU column reproduces with GFNI off). GFNI's encode advantage shows at m ≥ 3. raidkm keeps scaling with cores; stock raid6 does not.Absolute IOPS are not comparable across the three machines (different CPUs, core counts, RAM/ramdisk sizes). Reproduce the stock raid6 column by
xzcating/lib/modules/$(uname -r)/kernel/drivers/md/raid456.ko.xzinto a writable file andinsmoding it instead ofraid_km.ko.
raidkm also rebuilds a failed disk substantially faster than stock raid6,
because its resync path fans multiple stripes per sync_request instead of
walking them one stripe-window at a time. Single-disk recovery, 6 × brd, k=4
m=2, 3 GiB/disk, on a GCP c3-standard-8 (8 vCPU, Xeon 8481C), with the global
resync governor (/proc/sys/dev/raid/speed_limit_max) raised so neither side is
throttled:
group_thread_cnt |
stock raid6 | raidkm m=2 |
|---|---|---|
| 0 (stock default) | ~200 MB/s | 1178 MB/s (5.9×) |
| 4 (matched) | ~585 MB/s | 1178 MB/s (2.0×) |
Two things to read here. raidkm's rebuild rate is independent of
group_thread_cnt (1178 MB/s at both 0 and 4) — the parallelism is in the
sync path itself, not the worker pool; stock raid6's recovery is serialized at
the default gtc=0 and only speeds up once worker groups are enabled. So the
honest gap is ~2× apples-to-apples (matched gtc=4) and ~6× out of the
box (stock ships worker groups off, raidkm's parallel sync is always on).
Rebuild is a streaming, fully-parallelizable scan, so the gap is wider than the
random-I/O table above. (brd is RAM-backed and compute-bound; on real disks
the rebuild is capped by disk write bandwidth, so the gap narrows — the full win
shows on fast NVMe or when the rebuild is CPU/EC-bound.)
On a device-bound array the gap closes. An independent evaluation over
NVMe-oF with QLC namespaces (k=8 m=2, 128 KiB chunk, 1 TiB of each member)
measured raidkm and stock raid6 at parity at every matched worker count:
535 vs 545 MiB/s at group_thread_cnt 32 with default block-layer settings,
615 vs 611 MiB/s tuned. There the rebuild is bound by its I/O pattern, not by
CPU: both engines read survivors and write the spare in ~4 KiB stripe units
(see Flash with a large indirection unit). Treat the 2× / 6× figures above as
the CPU-bound ceiling, not as a promise for disk-bound arrays.
A three-way rebuild comparison on real NVMe (16 × local SSD, RHEL 10.2,
raidkm-standard-benchmark.sh --rebuild-victim, 16 GiB member) — declustered
vs classic on the current build, vs classic on the pre-declustered build
(the parent of the first declustered commit, to isolate any personality
overhead the declustered code adds to the classic path):
| pool | declustered populate | classic recover (current) | classic recover (pre-dcl) | speedup |
|---|---|---|---|---|
| N=14 g=6 (4+2) | 27.9 s | 57.1 s | 56.8 s | 2.05× |
| N=80 g=13 (11+2) | 48.3 s | 761.5 s | 777.5 s | 15.8× |
Two readings. Declustering wins the rebuild — 15.8× at N=80 (the classic 78+2 recover is single-writer-bound at ~21 MB/s; the distributed spare fans reconstruction across the whole pool). And the declustered code adds no overhead to the classic path — current-classic and pre-dcl-classic recover in the same time at both scales. Steady-state fio (6-workload suite, same run) shows no classic-path regression either.
Earlier single-box measurement on the AMD Ryzen 5800X VM (md-kmec-rhel10,
kernel 6.12.0-124.8.1.el10_1, --runs=3 --runtime=20) isolating the
worker_thread_cnt knob: the auto-default wtc=2 (4-core single-NUMA box) vs
the hand-tuned wtc=4 (see the Tuning section). Stock raid6 measured
against the same VM/ramdisks/kernel.
| Test | raidkm | Stock raid6 | |
|---|---|---|---|
| wtc=2 default (vs Stock) | wtc=4 (vs Stock) | ||
| 1 Random 4K Write | 259,156 ± 5.2% (2.00×) | 274,650 ± 0.9% (2.12×) | 129,367 ± 4.3% |
| 2 DB Mixed 8K (75/25) | 427,510 ± 0.6% (1.50×) | 467,748 ± 0.5% (1.64×) | 285,811 ± 2.3% |
| 3 High Concurrency 4K rw | 568,588 ± 1.0% (1.34×) | 606,627 ± 0.8% (1.43×) | 423,035 ± 1.4% |
| 4 OLTP 16K rw | 239,478 ± 2.5% (1.85×) | 242,968 ± 0.7% (1.88×) | 129,588 ± 0.9% |
| 5 Partial Stripe Write 8K | 180,554 ± 1.1% (2.35×) | 175,580 ± 1.0% (2.28×) | 76,990 ± 0.4% |
raidkm is 1.34-2.35× faster than stock raid6 at m=2 on every workload at the
auto-default, and 1.43-2.28× with worker_thread_cnt raised to 4 (which
lifts the floor on concurrent/mixed-write tests but slightly trades off Test 5).
The win comes
because the raid5.c we forked carries our post-fork mdraid optimizations
(worker-groups auto-default, STRIPE_ON_INACTIVE_LIST lock-skip, etc.). The m=2
parity here is computed by the raid6_call SIMD fast path; after briefly unifying
m=2 onto ISA-L (which regressed 30-50% on no-GFNI hardware) the fast path was
restored. Reproduce stock raid6 numbers by xzcating
/lib/modules/$(uname -r)/kernel/drivers/md/raid456.ko.xz into a writable file
and insmoding it instead of raid_km.
Cost of the built-in per-4K CRC-32C integrity (--checksum=crc32c), measured
2026-07-14 on real hardware: GCP n2-standard-32 (32 vCPU, AVX-512),
8 × 375 G local NVMe SSD, RHEL 10.2 (6.12.0-211.16.1.el10). raidkm m=2
rotating (6 data + 2 parity), 64 KiB chunk, 8 GiB/member, --assume-clean +
full-stripe prewrite; fio --direct=1 --iodepth=32, 30 s per point (seq =
bs=1M, rand = bs=4k; seq read 4 jobs, rand read 16 jobs, rand write 4
jobs); CRC region cache sized to cover the working set (20480 pages ≈ 80 MiB).
The dm-integrity columns run the same raidkm array (checksums off) over 8 ×
integritysetup crc32c members — journal is its crash-atomic default, bitmap
its fast mode. Percentages are relative to the no-checksum baseline:
| Workload | no checksum | native checksum | dm-integrity journal | dm-integrity bitmap |
|---|---|---|---|---|
| Sequential write (MB/s) | 2245 | 2264 (101%) | 1088 (48%) | 2230 (99%) |
| Random write (K IOPS) | 97.2 | 93.2 (96%) | 40.8 (42%) | 78.5 (81%) |
| Sequential read (MB/s) | 5626 | 5599 (99.5%) | 5624 (100%) | 5014 (89%) |
| Random read (K IOPS) | 1236.2 | 1235.9 (100.0%) | 978.0 (79%) | 934.4 (76%) |
Verified integrity at effectively no cost: reads and writes run at
96–101% of the raw array — ahead of dm-integrity bitmap on all four
workloads and of journal on writes (2.1–2.3×) and random read (1.26×),
tying it on sequential read (0.4% apart, inside run-to-run variance). Zero
false checksum mismatches across every run. The read side is the 2026-07-14
read-first redesign: verification runs inline in the bio completion
(lock-free tag lookup + crc32c, no thread handoff) with a verified
chunk-aligned read bypass — before it, random read sat at 37% of baseline.
Fair-comparison note: dm-integrity journal is crash-atomic (tags+data
survive power loss torn-write-free), a stronger guarantee than native or
bitmap, both of which recompute checksums over the resync window after an
unclean shutdown — that guarantee is what journal's ~2× write cost buys.
Design + full write-up: notes/native-checksum-read-redesign-2026-07-14.md.
Same VM/ramdisks, m=2, 64 KiB chunk. A grow has two costs: a one-time restripe, and (the important question) any lasting penalty afterwards.
One-time reshape. Adding a data disk relocates every block, so the cost is
a full online read+rewrite of the array — inherently O(data). Growing a
filled k=4→5 array (≈2.0 GiB of data) took 2.5 s ≈ 825 MiB/s on brd. That
is a RAM-speed upper bound: on real disks the reshape is bounded by disk
bandwidth and throttled by sync_speed_{min,max}, exactly like any md
reshape. The array stays readable/writable throughout, and no backup file is
needed (a grow has writepos < readpos).
No lasting cost. A grown array performs the same as one created at the
final geometry — the reshape leaves no scar. Steady-state IOPS for k=5 m=2
(N=7), natively created vs grown from k=4 (--runs=2 --runtime=15):
| Test | native k=5 | grown k=4→5 | grown / native |
|---|---|---|---|
| 1 Random 4K Write | 212,077 | 229,838 | 1.08* |
| 2 DB Mixed 8K (75/25) | 377,679 | 379,835 | 1.01 |
| 3 High Concurrency 4K rw | 535,525 | 542,583 | 1.01 |
| 4 OLTP 16K rw | 196,346 | 191,542 | 0.98 |
| 5 Partial Stripe Write 8K | 138,016 | 143,182 | 1.04 |
The two are statistically indistinguishable (*Test 1's native run had cv≈8%,
so 1.08 is run-to-run noise; the rest are within ±4%). So the only cost of
--add-data is the one-time reshape; steady-state is identical to native.
Ordered by impact. Most of the md side is already the default — the wins are geometry and layout decisions made before the array carries data. Each item is expanded in the sections that follow.
Decide once, at array-create time (a reshape is the only later fix):
- Choose
ksok × chunkis a power of two — 256K, 512K, 1M. Large writes are almost always a power of two, so a row is only ever written whole if the row is one too. A width likek=5@ 64K (320K) ork=14@ 64K (896K) leaves a partial row at the tail of every write, at every chunk size, with no tuning available. - Prefer declustered parity on wide pools — it decouples pool width from row width, so an 80-disk pool keeps a small, easily-filled row instead of a ~5 MiB one, and rebuilds far faster.
- Keep the 64K chunk default unless deliberately trading for a specific row width — or the members are flash with a large indirection unit: then make the chunk a power-of-two multiple of the largest unit you expect to deploy (128K for 16–64K units; see Flash with a large indirection unit).
Storage layout:
- Put the filesystem journal on a separate device — the single largest win
here (see Keep the filesystem journal off the array). This is the
filesystem's own journal (ext4/jbd2), not md's
--write-journal. - Start the partition or LV on a row boundary — nothing detects a violation at runtime; it silently phase-shifts every allocation.
- Keep other small, barriered write streams off the array — same mechanism as the journal.
Filesystem geometry:
- Set
stride/stripe_widthto match the array, or letmkfs.ext4derive them fromoptimal_io_size. Verify withdumpe2fs -h <dev> | grep RAID. - After a grow that changes
k, refresh them withtune2fs—mdadm --growprints the exact command.
md tunables — verify, don't tune:
skip_copyand worker groups are already on by default. On many-core hosts, raisingworker_thread_cnttowardnprocmay help concurrent writes — measure rather than assume.stripe_cache_size(default fine) andpreread_bypass_threshold(irrelevant to full-row writes) are not worth sweeping.
One-line version: get k, the journal, and the partition offset right at
build time; everything else is already the default or automatic.
High-capacity QLC drives map logical blocks in units of 16 KiB, 32 KiB or 64 KiB instead of 4 KiB — the indirection unit (IU). A write smaller than the unit, or one that straddles a unit boundary, makes the drive read the rest of the unit and rewrite all of it: extra flash wear, extra latency. Reads below the unit cost only performance. So on these drives the number that matters is the request size at the member devices, after md has split and the block layer has merged the I/O.
Measured on a calibrated rig (tools/raidkm-bench-iosize.sh, k=8 m=2, 128 KiB
chunk, 1 MiB sequential I/O; the rig reproduces an NVMe-oF QLC array's table):
| state | request size at the members |
|---|---|
| healthy and degraded full-row writes, m=2 (classic or declustered) | ~123–128 KiB |
| full-row writes, m ≥ 3 | ~5 KiB — full-row batching is off above m=2 |
| degraded reads (classic / declustered) | ~5 KiB / ~6 KiB |
| rebuild onto a spare: survivor reads / spare writes | ~5 KiB / ~7 KiB |
| declustered population: survivor reads / spare-column writes | ~5.5 KiB / ~7 KiB |
| declustered copy back to a replacement | ~128 KiB |
Degraded reads and rebuild go through 4 KiB stripe units on both raidkm and
stock raid6, and with worker groups enabled those units reach the members out
of order, so the block layer cannot merge them. With group_thread_cnt=0 the
same rebuild merges to ~120–125 KiB on both engines — but runs on one thread,
about 2.5–3× slower on a CPU-bound rig; degraded reads improve only to
~7–10 KiB. Chunk-sized rebuild and degraded read are being worked on. Until
then, on large-IU flash:
- chunk = a power-of-two multiple of the IU, with room to grow — 128 KiB
covers 16, 32 and 64 KiB units; with
k=8that is a 1 MiB row; - keep
k × chunkequal to the application's large I/O size (item 1 above); - use m=2 for now;
- no
--write-journaland no PPL — an attached md log or PPL turns off full-row batching, which brings back ~5 KiB member writes even at m=2; - external filesystem journal on a device that is not QLC — a mirror or an NVMe with power-loss protection;
- start namespaces, partitions and LVs on an IU boundary as well as a row boundary (mdadm already rounds its data offset to 1 MiB);
- check, don't assume:
raidkm-ab-benchmark.shandraidkm-standard-benchmark.shreport the member request size per workload.
raidkm already auto-enables raid5 worker groups: total worker threads default
to max(num_online_cpus()/2, 2), distributed across num_possible_nodes() groups
(the kernel creates one worker group per NUMA node). The single raid5d kthread
would otherwise cap stripe handling at ~one core; worker groups are on out of the
box — the benchmark above was measured with them — so there is no large "free"
win sitting unused. The one knob worth revisiting per host:
-
worker_thread_cnt(/sys/block/mdX/md/worker_thread_cnt) — total worker threads for the array. Recommended user-facing knob because it expresses intent in the natural "I want N parallel workers" mental model. The auto-default isnproc/2(with a floor of 2 to preserve the win on small hosts) — conservative because workers are CPU-bound and share the box withraid5d, your application, and IRQ handling. Raising further towardnprocmay help concurrent/mixed writes: the benchmark table above includes both the auto-default (2 on a 4-core single-NUMA box) andworker_thread_cnt=4, showing +6 / +9 / +7 % on the concurrent/mixed-write tests (1, 2, 3), noise on Test 4, and a small regression on Test 5. On dual-socket 2×16 the default lands at 16 total workers (8 per group × 2 groups); raising to 32 puts it at one worker per core. To override:echo 4 | sudo tee /sys/block/md70/md/worker_thread_cnt.Rounding: the written value is divided across
num_possible_nodes()groups with ceiling division, so the actual worker thread count may be higher than what you wrote when the total isn't evenly divisible by the node count. E.g., writing 5 on a 2-NUMA box yields 3 per group × 2 groups = 6 workers; the read-back reflects the realized total (6), not what you wrote (5). This never happens on single-NUMA hosts. The choice to round up rather than down ensures you never silently get less parallelism than requested. -
group_thread_cnt(/sys/block/mdX/md/group_thread_cnt, or thedefault_group_thread_cntmodule param at load). Stock-mdraid-compatible view: threads per worker group (total =group_thread_cnt × num_groups). Same underlying state asworker_thread_cnt; either knob updates the other. Useful when migrating tuning scripts from stock RAID5/6 or when you want explicit per-group control on multi-NUMA hosts. -
stripe_cache_size— leave at the default 256. Raising it reduced throughput on ramdisk (no device latency to hide, just more cache churn): 256 → 8192 lost ~15-25% on both boxes. It may help on real spinning disks, so measure before changing rather than bumping it blindly.
(Measured on brd ramdisks, which are CPU/memcpy-bound; on real disks the worker- group win should be larger — threads overlap device latency — and the stripe-cache result may invert.)
A write that covers whole rows is a reconstruct-write with no pre-read; one
that straddles a row boundary forces a read-modify-write. So a filesystem on a
raidkm array should align and size its allocations to the data row,
k × chunk.
The array advertises exactly that as optimal_io_size, so mkfs.ext4 picks it
up with no options:
cat /sys/block/md70/queue/optimal_io_size # = k * chunkThis is the data row width, not the pool width — on a declustered array the
row is k = g − m cells wide regardless of how many disks are in the pool, so a
14-disk pool with g=6, m=2 advertises 4 × chunk, not 12 × chunk. To set
the geometry by hand (4 KiB filesystem blocks, 64 KiB chunk → stride 16):
mkfs.ext4 -b 4096 -E stride=$((chunk/4096)),stripe_width=$((k*chunk/4096)) /dev/md70After a grow that changes k, the filesystem's stored geometry is stale and every subsequent write lands off-row.
mdadm --growprints the exact refresh command; it is a metadata-only update, no re-write of data:tune2fs -E stride=16,stripe_width=80 /dev/md70 # then remountGrows that leave k unchanged (adding parity, expanding a declustered pool) don't affect it and print nothing.
Alignment fixes the large writes. What is left is usually a small, frequent write stream sharing the array with them — and on a parity array that is expensive out of all proportion to its size.
A journalled filesystem is the common case. By default the ext4/jbd2 journal is a hidden inode inside the filesystem, so its commits land on the array, interleaved with the data. Those commits are small and sub-row, and every sub-row write is a read-modify-write: read the old data and parity, recompute, write back. The journal is only a few percent of the bytes written and can still dominate the array's read traffic.
Measured on a k=8 m=2 array at 64 KiB chunk under streaming 1 MiB aligned
O_DIRECT writes — same filesystem, same workload, only the journal moved:
| journal location | array member reads | journal device |
|---|---|---|
| internal (on the array) | 43–50 MB per GiB written | — |
| external (own device) | 0.2 MB per GiB written | 39 MB/GiB written, 0 read |
The work does not disappear, it just stops being charged parity RMW. Note the symptom this produces if you do not know to look for it: the cost is nearly constant per transaction, so it scales with commit rate rather than with I/O size, and it is easy to mistake for a fixed per-write overhead somewhere in the block layer.
To move it:
mke2fs -O journal_dev /dev/sdX # format a device as an external journal
tune2fs -O ^has_journal /dev/md70 # drop the internal journal
tune2fs -J device=/dev/sdX /dev/md70 # point the filesystem at the new one(or mke2fs -J device=/dev/sdX at filesystem-creation time.)
The journal device becomes a correctness dependency. The filesystem will not mount without it, and losing it loses the journal. Give it durability at least equal to the array it serves — a mirror, or an SSD with power-loss protection. An internal journal is parity-protected; an external one is only as safe as the device you put it on.
The general rule generalizes past journals: any small, frequent write stream co-located with large aligned writes will tax them. Metadata-heavy sidecars and write-intent logs behave the same way.
tools/raidkm-test.sh runs the regression suite — functional
(create/write/read/scrub), max-degraded reconstruction, grow (--add-data
online reshape incl. degraded-read-after-grow, --add-parity — PARITY_N offline
recreate / rotating online COW reshape),
the traditional stock --grow --raid-devices syntax (one-line and two-step,
verified to grow capacity), and I/O concurrent with a throttled reshape
— across PARITY_N and rotating at m=2/3/4, on brd ramdisks:
sudo MDADM=../mdadm/mdadm bash tools/raidkm-test.shIt loads the module + deps and creates ramdisks itself; point MDADM at the
raidkm-aware fork (it refuses a stock mdadm). Exit status is non-zero if any
check fails. Individual stages can be run directly (raidkm-test-{functional, degraded,grow,grow-traditional,reshape-concurrent}.sh); see
raidkm-test-lib.sh for the env knobs.
Reshape crash/fault suite (tools/raidkm-test-reshape-crash.sh, needs a
CONFIG_RAIDKM_FAULT_INJECT kernel build): power-loss and torn-write recovery of
the COW-staged online reshape, driven by the raidkm_reshape_inject debug knob.
Tier 0 clean reshape · Tier 1 crash+resume at each phase × band · Tier 2 torn
STAGE/COMMIT (redo-from-old / replay-from-scratch) · Tier 3 hybrid fault tolerance
(frozen mid-reshape: the migrated region survives m_new failures, the pending
region m_old, each probed on its own array) · Tier 4 torn COMMIT + concurrent
member failure. 114 passed / 0 failed on both base and GFNI (2026-06-11).
Scope: this validates reading through faults during a reshape; completing a
reshape after losing members mid-flight is not yet supported (migrate_band is
non-degraded-read only) — see notes/reshape-cow-design.md §6/§9. Without the
fault-inject build the script auto-runs Tier 0 + a best-effort timed crash.
Self-healing suite (tools/raidkm-test-selfheal.sh): writes data, injects silent
corruption directly on the raw backing store, and verifies md-kmec reconstructs the corrupt
block from parity — on the read path and the m-way scrub, for data-only, parity-only, and
mixed data+parity corruption up to m per stripe, confirming the healed_blocks counter
advances. Runs in two modes: NATIVE=1 uses raidkm's built-in checksums (also covers
detection-after-remount from the persisted CRC region, and rotted-region-page handling — no
false heal); the default stacks raidkm on dm-integrity members (needs integritysetup from
the cryptsetup package). Validated to m=8 (8 silent corruptions healed in one stripe,
beyond RAID-Z3's 3).
Native-checksum cache-thrash suite (tools/raidkm-test-csum-thrash.sh, NATIVE=1):
drives an array whose CRC-region footprint is several times the cache ceiling, so region pages
are continuously faulted, evicted, written back, and re-faulted, and asserts both directions of
the round-trip: no false mismatch or spurious heal across evict+reload, fio data-verify clean
under the churn, a REAL corruption of a block whose CRC page was evicted is still detected and
healed after re-fault, and a final scrub reports zero mismatches. This is the standing gate for
the demand-paged cache and the inline-verify engine.
Real-NVMe re-gate (2026-07-15). The native-checksum stack was re-gated on real
4K-logical-block NVMe (8× GCP local SSD) under a KASAN + lockdep kernel: the
functional (12/12), csum-thrash, and self-heal (60/60) suites all run clean with
zero KASAN/lockdep splats. The re-gate also exposed — and fixed — a
skip_copy×native-checksum read/write WARN_ON (see Zero-copy full-stripe
writes above): a 12-round randrw churn that warned in 8/12 rounds is 0/12 after
the need_this_block deferral. Two harness bugs that only surface on 4K-logical
devices were fixed alongside it (the raw-member probes now use the member's
logical block size via blockdev --getss, not a hardcoded 512-byte O_DIRECT).
md-kmec/
├── Kbuild # top-level kbuild glue (obj-m += km/)
├── Makefile # build infra; symlinks ../mdraid/md and ../mdraid/isa-l
├── compat/
│ ├── compat-rhel10.h # RHEL 10.x personality-API shim (force-included by the build)
│ ├── compat-rhel9.h # RHEL 9.x shim (md_submodule head-style personality)
│ └── compat-vanilla.h # mainline-kernel shim
├── md-rhel9/ # vendored RHEL 9.x md headers (ABI matches in-kernel md-mod)
├── md-vanilla/ # vendored mainline md headers
├── tools/
│ ├── raidkm-test.sh # run the full test suite (functional/degraded/grow)
│ ├── raidkm-test-lib.sh # shared helpers sourced by the test scripts
│ ├── raidkm-test-functional.sh # create/write/read/scrub, PARITY_N+rotating × m=2/3/4
│ ├── raidkm-test-degraded.sh # max-degraded reconstruction (read + write)
│ ├── raidkm-test-grow.sh # --add-data (incl. degraded-read-after-grow) + --add-parity
│ ├── raidkm-test-grow-traditional.sh # stock --grow --raid-devices syntax (one-line + two-step)
│ ├── raidkm-test-reshape-concurrent.sh # I/O concurrent with a throttled reshape (dual EC tables)
│ ├── raidkm-test-reshape-crash.sh # power-loss/torn-write recovery of the COW reshape (fault-inject build)
│ ├── raidkm-test-selfheal.sh # checksum-driven self-heal (NATIVE=1 or dm-integrity)
│ ├── raidkm-test-csum-thrash.sh # native-checksum region-cache eviction round-trip
│ ├── raidkm-standard-benchmark.sh # fio harness (7 workloads incl. 1 MiB
│ │ # sequential) + member request size,
│ │ # Test-7 rebuild/populate wall-clock
│ │ # (--rebuild-victim=DEV)
│ ├── raidkm-ab-benchmark.sh # A/B vs stock md on the same disks:
│ │ # raw / raid6 / raid6-intree / raidkm<M>,
│ │ # ABBA order, ratio tables
│ ├── raidkm-bench-iosize.sh # request size + merge share at the members
│ │ # per I/O state (healthy, degraded, rebuild,
│ │ # declustered populate/copyback); null_blk rig
│ ├── raidkm-member-stats.sh # sourced helper: member request counters
│ └── raidkm-create.sh # sysfs array creation; needs adapting
│ # to "raidkm" name / level 71
└── km/
├── Kbuild # builds raidkm.ko = raid_km.o + raid_km-cache.o + raid_km-ppl.o
├── raid_km.c # fork of mdraid/md/raid5.c with effective_level dispatch
├── raid_km.h # fork of raid5.h (RAID_KM_LEVEL, is_raid6_math, effective_level)
├── raid_km-cache.c # fork of raid5-cache.c (journal, dormant unless attached)
├── raid_km-ppl.c # fork of raid5-ppl.c (partial parity log, ditto)
├── raid_km-log.h # fork of raid5-log.h
└── raid0.h # fork (for the takeover stubs to link)
Requires built mdraid
in a sibling directory for isal_lib.ko's exports.
cd ../mdraid && make # produces isal_lib.ko + raid456.ko etc.
cd ../md-kmec && make # produces km/raidkm.koisal_lib.ko's exports are prefixed isal_lib_ so they cannot collide with
another out-of-tree module that vendors the same ISA-L code under the upstream
API names — the kernel matches exported symbols by bare name and rejects the
second module to load, which would leave raidkm.ko unloadable depending on
boot order. The rename is a macro in ../mdraid/isa-l/isal_lib_syms.h applied
to definitions and call sites alike, so raid_km.c still reads in the plain
ISA-L names (ec_encode_data_base, gf_invert_matrix, …) and nothing about
building or loading raidkm changes. The prefix shows up only in
nm km/raidkm.ko and in unresolved-symbol messages.
One source tree builds against three kernel flavours. The build picks a
TARGET from the running kernel release and force-includes the matching
compat/compat-*.h shim, so the verbatim raid5.c fork compiles against each
kernel's personality API (register_md_submodule etc.) unchanged:
TARGET |
Selected when | md headers | Notes |
|---|---|---|---|
rhel10 |
.el10 release |
built mdraid tree | flat md_personality with .level |
rhel9 |
.el9 release |
md-rhel9/ |
md_submodule_head-style personality (level comes from head.id) |
vanilla |
anything else | md-vanilla/ |
mainline |
Override the auto-detection with make TARGET=rhel9 (useful when building
against a KDIR whose release string doesn't carry the distro suffix, e.g. a
locally-built debug kernel).
RHEL 9 support is production-grade: the full 12-suite matrix (functional,
declustered create/io/degraded/csum/populate/rebalance/autoarm/multi/crash,
checksum thrash, self-heal) passes 211 distinct checks under a
KASAN + lockdep kernel with zero splats. The vendored md-rhel9/ headers come
from the distro kernel source, so the ABI matches the in-kernel md-mod by
construction rather than by inspection.
Stock mdadm rejects level 71. A patched mdadm 4.4 that
understands raidkm/level 71 lives in the sibling
mdadm checkout (branch raidkm-level71). On the CLI,
placement and count are separate: --layout=rotating|parity-last
and --parity-count=N. Internally raidkm packs both into the v1.x
superblock layout field — the low byte carries m (2–8) and
bit 0x100 selects rotating (clear = parity-last) — but that
packing is an implementation detail you don't type. Under
parity-last, data lives on disks [0, raid_devices − m) and never
moves; under rotating the m-slot parity block rotates one disk per
stripe. No raidkm-specific superblock code is needed — md core
round-trips level 71 and the packed layout through the standard
v1.2 superblock.
Version pairing. raidkm.ko and the patched mdadm are
co-dependent — features land across both repos together (level-71
create / assemble / grow, and the PPL consistency policy). Build and
run them as a matched pair: the mdadm fork that goes with this
tree is branch raidkm-level71, currently at commit
24e99c1b ("raidkm: --grow --remove-parity + fix add-parity
dropping the csum layout bit"). When you advance one repo, rebuild the other from
its matching commit. The fork will be wired in as a git submodule once it has a
published remote (scopedog/mdadm); until then it lives in
the sibling mdadm checkout and the pairing is tracked
here by hand.
Build it (userspace; NO_LIBUDEV avoids the libudev build dep):
cd ../mdadm && make CXFLAGS=-DNO_LIBUDEV mdadmLoad the personality and its dependencies (the async_tx family
is not pulled in by raid6_pq alone), then isal_lib.ko,
then raidkm.ko:
for m in async_tx async_memcpy async_xor async_pq async_raid6_recov raid6_pq; do
modprobe $m
done
insmod ../mdraid/isa-l/isal_lib.ko
insmod km/raidkm.koTwo independent knobs: --parity-count=N sets the parity-disk count
m (2–8, default 2; alias --parities), and --layout= sets the
placement — rotating (default) or parity-last (aliases dedicated/
fixed). Data disks = raid-devices − m. An optional third knob,
--checksum[=crc32c], enables native per-block CRC-32C integrity with
checksum-driven self-healing (bare --checksum defaults to crc32c;
--integrity=crc32c is the retained alias).
# 3 data + 2 parity (m=2, raid6-equivalent fast path), rotating (default)
mdadm --create /dev/md70 --level=raidkm --parity-count=2 \
--raid-devices=5 --chunk=64 /dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4
# 3 data + 4 parity (m=4, Cauchy matrix), parity-last (dedicated tail parity)
mdadm --create /dev/md70 --level=raidkm --parity-count=4 --layout=parity-last \
--raid-devices=7 --chunk=64 \
/dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4 /dev/ram5 /dev/ram6
# 4 data + 3 parity (m=3), rotating — parity spread across all 7 disks
mdadm --create /dev/md70 --level=raidkm --parity-count=3 --layout=rotating \
--raid-devices=7 --chunk=64 \
/dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4 /dev/ram5 /dev/ram6
# m=2 with native per-block checksums (integrity + self-healing) enabled
mdadm --create /dev/md70 --level=raidkm --parity-count=2 --checksum=crc32c \
--raid-devices=5 --chunk=64 /dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4Deprecated: the older packed form
--layout=N(parity-last) /--layout=Nr(rotating), which crammed m into--layout, is still accepted for back-compat but prints a warning; prefer--parity-count
--layout=rotating|parity-last. Note the default flipped with the new syntax: omitting--layoutnow means rotating (it used to mean PARITY_N), so a bare--layout=2still means parity-last as before.
--detail / --examine report Raid Level : raidkm, Layout : rotating (or parity-last), and Parity Count : <N>.
mdadm --stop /dev/md70
mdadm --assemble /dev/md70 /dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4A degraded array (up to m missing members) assembles with
--run; missing data is reconstructed on read:
mdadm --assemble --run /dev/md70 /dev/ram0 /dev/ram1 /dev/ram2 /dev/ram3 /dev/ram4raidkm --grow has two role-tagged forms; each disk you list is added in
the named role, so you never compute the new device count yourself:
| command | what it does | layouts | mechanism |
|---|---|---|---|
--grow --add-data <disks> |
add data disk(s) — grow capacity, m fixed | PARITY_N and rotating | online kernel reshape |
--grow --raid-devices=<N-1> |
REMOVE one data disk — shrink capacity, m fixed | PARITY_N and rotating | online backward COW reshape; --array-size clamp first (mdadm prints the value) |
--grow --add-parity <disks> |
add parity disk(s) — raise m, k fixed | PARITY_N and rotating | PARITY_N: offline recreate. rotating: online COW reshape (no backup-file, crash-safe via the kernel journal); offline windowed relocation retained as a fallback (MDADM_RAIDKM_OFFLINE_ADDPARITY) — see below |
--grow --remove-parity |
drop one parity disk — lower m (≥2 remain), k and capacity fixed | PARITY_N and rotating | online COW reshape (k fixed ⇒ every row re-encodes in place; no --array-size dance); the freed member becomes a spare (remove + --zero-superblock before reuse). The m=4→3 Cauchy→Vandermonde boundary is handled by the re-encode. Classic only (declustered refused) |
The traditional (stock) --grow syntax also works. Because parity in a
stock RAID6 is fixed, growing --raid-devices there means add capacity, so
on raidkm an untagged grow that gives an explicit --raid-devices=N is
treated as --add-data (grow data at fixed m). Both stock entry forms work —
the one-line --add and the classic two-step where you add hot spares first:
mdadm --grow /dev/md70 --raid-devices=5 --add /dev/ram5 # one-line: == --add-data
# …or…
mdadm /dev/md70 --add /dev/ram5 /dev/ram6 # add spares (MANAGE)
mdadm --grow /dev/md70 --raid-devices=6 # grow into them == --add-dataA bare --grow --add <disks> with no --raid-devices keeps the raidkm
shorthand of adding parity (alias for --add-parity). An explicit
--add-data/--add-parity always wins, so use those when you want to be
unambiguous.
mdadm --grow /dev/md70 --add-data /dev/ram5 # k=3 → k=4 (m unchanged)
mdadm --grow /dev/md70 --add-data /dev/ram5 /dev/ram6 # k=3 → k=5Adding a data disk changes the stripe width, so every block relocates — a
true restripe. raidkm drives the inherited kernel online reshape
(delta_disks at a fixed parity count): the new disk(s) are added as spares,
raid_disks is bumped, and the kernel relocates the array stripe-by-stripe
with a crash-safe reshape_position checkpoint. Works for both layouts
(the relocation rides the layout-aware sector mapping). The array stays
readable/writable throughout, and a grow needs no backup file (the wider
new layout writes behind the old layout's read frontier, so nothing
unread is overwritten — and a crash resumes from the checkpoint). The layout
is immutable across a grow. Monitor with /proc/mdstat or --detail.
Adding parity raises m (more fault tolerance) at a fixed data-disk count. How it's done depends on the layout, because the two layouts place parity differently:
PARITY_N — offline recreate (cheap, no data movement). PARITY_N keeps
data on a fixed prefix of disks and never relocates it, so adding parity only
appends a parity disk and recomputes parity for the new m. There is no
in-kernel online reshape for a parity-count change (it would alter
max_degraded), so --grow does it offline but data-preserving: it stops
the array and recreates it at the new m (same device order, data_offset, UUID
and name), then md's normal resync recomputes parity while the array is online.
You can add several parity disks in one command.
mdadm --grow /dev/md70 --add-parity /dev/ram5 # m=2 → m=3
mdadm --grow /dev/md70 --add-parity /dev/ram5 /dev/ram6 # m=2 → m=4
mdadm --grow /dev/md70 --add /dev/ram5 # legacy alias (= --add-parity)The array is usable throughout the background resync, but is not fully
fault-tolerant until the resync completes (the same window as any md
rebuild), and the brief stop+recreate is not crash-safe. On real
(non-identical) disks the recreate must reuse the original data_offset; on
uniform disks mdadm picks it deterministically.
Rotating — online COW reshape (default).
Under rotating parity every block moves when m changes, so there is no cheap
append. raidkm drives an online, journaled, copy-on-write reshape entirely
in the kernel: it adds the new disk, then migrates the array one band at a
time, staging each band's new-geometry stripe out-of-place (in the
metadata gap below a constant data_offset) and journaling STAGE→COMMIT→DONE
before overwriting the band's home location. Because no live block is
overwritten until its new-geometry copy is durably staged, correctness is
placement-agnostic — the read/write location-aliasing race that sank the earlier
in-place attempt (withdrawn 2026-06-01) is structurally impossible. The
data-disk count k is unchanged, so array_size and data_offset stay
constant; only m (hence max_degraded and the parity placement) changes.
The array stays readable/writable throughout and needs no backup file: a
power loss mid-reshape is recovered by a plain mdadm --assemble, which replays
the in-kernel journal (raidkm sets RESHAPE_NO_BACKUP, so mdadm neither demands
a backup-file nor runs its critical-section restore). Add one parity disk
per run; raise m further with repeated runs. The same reshape is also drivable
through device-mapper / LVM.
mdadm --grow /dev/md70 --add-parity /dev/ram5 # m=2 → m=3 (rotating), onlineValidated. Whole-array m=2→3 (base) and m=3→4 Cauchy (GFNI) — data byte-identical + scrub=0 + new-m-degraded-read EC oracle — plus a true power-loss crash (dm-flakey
drop_writes) recovered by a plain--assemble. The reshape crash/fault suite (tools/raidkm-test-reshape-crash.sh) is 114 passed / 0 failed on base and GFNI (2026-06-11).Scope. Reading through faults during a reshape is supported; completing a reshape after losing members mid-flight is not yet (
migrate_bandis non-degraded-read only) — seenotes/reshape-cow-design.md§6/§9.Fallback — offline windowed relocation. For kernels without the COW engine, the pre-COW offline path is retained behind
MDADM_RAIDKM_OFFLINE_ADDPARITY=1: it stops the array, relocates data on the raw members in ≤64 MiB batches (each backed up to--backup-filefirst for crash rollback — bounded scratch, a 64 MiB window, not array-sized), then recreates at m+1 and lets resync rebuild parity. Crash-safe/resumable via a<backup-file>.raidkm-statesidecar (re-run the same command to roll back the in-flight window and continue), but the array is offline for the duration.MDADM_RAIDKM_OFFLINE_ADDPARITY=1 \ mdadm --grow /dev/md70 --add-parity --backup-file=/var/tmp/rk.bak /dev/ram5
To add a hot spare (not part of the array yet), use MANAGE-mode --add
without --grow: mdadm /dev/md70 --add /dev/ram5.
Wide erasure-coded pools have a rebuild problem: a classic k+m array with one
dedicated spare funnels the entire reconstruction of a failed member through
that one replacement disk, so rebuild time grows with member size and the array
stays degraded (and exposed to a second failure) the whole time. Declustered
parity fixes this by making the stripe narrower than the pool: k+m groups
are scattered over an N-disk pool by a balanced permutation, and the spare is
distributed as a rotating set of spare columns rather than one disk. When a
member dies, its lost chunks live on — and are rebuilt across — every survivor
in parallel, so single-disk rebuild parallelises ~(N−1)/g instead of
bottlenecking on one writer.
The construction is clean-room: the rotation/difference permutation is classic design-theory combinatorics (Holland & Gibson lineage); it is the same idea as OpenZFS dRAID but shares no CDDL code — this is a GPL implementation. Capacity balance is exact by construction, and the per-survivor rebuild load is rotation-symmetric.
The headline (wide-pool rebuild): rebuilding a failed 16 GB member on an N=80, g=13 (11+2) pool took 44.9 s vs 785.1 s for a classic 78+2 array — 17.5×. It even beats a narrow 11+2 classic (51.9 s), because the distributed spare removes the single-writer bottleneck.
The wall-clock headline above follows from where the rebuild I/O lands. A
classic rebuild funnels every reconstructed byte onto the one hot spare — so
rebuild throughput is capped by a single disk's write speed no matter how wide
the array — while a declustered rebuild spreads those writes across the whole
pool. Measured with exact per-disk I/O counters
(tools/raidkm-bench-declustered-rebuild-load.sh), which makes the ratios
device-count-independent (so they hold on any box, not just a wide physical rig):
| Pool | Group | Rebuild-write funnelling¹ | Survivor read at rebalance² |
|---|---|---|---|
| N=14 | g=6 (4+2) | 14.2× | 5.0× |
| N=42 | g=10 (8+2) | 42.5× | 9.3× |
| N=80 | g=13 (11+2) | 85.0× | 12.6× |
¹ Write funnelling = busiest single disk's write during the rebuild, classic ÷ declustered. In the N=80 classic rebuild all 255 MiB of write hit the one spare (write spread 13.4× across the 13 members); declustered's busiest disk wrote just 3 MiB (spread 1.5×). The ratio ≈ N — the wider the pool, the more the single-writer bottleneck is removed.
² Survivor read = total bytes read off survivors to bring a fresh disk back, copy-from-spare rebalance ÷ decode rebuild. Decode reads g−1 survivors per lost chunk; copy-from-spare reads the already-populated spare (~1 disk's worth). The ratio ≈ g−1. (Copy front-loads a one-time population decode paid when the disk first failed; the harness reports it separately.)
Counters are exact regardless of device count; the wall-clock headline needs many physical spindles, but these distribution ratios are the mechanism it follows from.
mdadm --create /dev/md0 --level=raidkm --parity-count=2 \
--layout=declustered --group-width=6 --spare-columns=2 \
--raid-devices=14 /dev/sd[b-o]--group-width=g— the stripe widthk+m(here 6 = 4+2). Each row of the pool holdsngroups = (N−s)/gindependent groups plussspare columns.--spare-columns=s(optional) — distributed-spare capacity, in columns per row (≈sdisks' worth of spare, spread across all members). Defaults to the smallest legal value;s ≥ mlets the pool absorbmconcurrent failures.--dcl-nbase=n/--dcl-seed=x(optional, advanced) — the number of base permutations and the acceptance-search seed.mdadmruns the acceptance search at create time (float scoring is userspace-only) and records the winning seed + geometry in a per-member on-disk rkdcl metadata block; the kernel regenerates the identical permutation from that seed alone.
Constraint C1: (N − s) mod g == 0 (groups tile a row without wrapping).
mdadm validates this and suggests a legal --spare-columns if you miss it.
For example N=80 wants g=13/s=2 (good) but g=16 would force s=16 (12.5%
overhead) — so mdadm steers you.
A declustered array does not rebuild onto a single replacement. Instead it populates the failed member's content into the distributed spare columns:
# a member failed; reconstruct it into the distributed spare
echo <failed-index> > /sys/block/md0/md/rk_dcl_populate
cat /sys/block/md0/md/rk_dcl_populate # populating <X> -> spare <j> mark A/BOr set echo 1 > /sys/block/md0/md/rk_dcl_auto to arm population automatically
the moment a member fails (off by default; not persisted). Population runs as a
raidkm-owned sync action with a crash-safe journaled prefix mark (FUA +
PREFLUSH every 16 MiB) — a power loss mid-rebuild resumes from the mark on the
next assembly. Up to s failures can be populated sequentially (one at a
time); because a spare column can rotate onto another failed disk, the
slot→disk map resolves through the active assignments (chained redirects),
and the on-disk journal is adaptive v2/v3 so an older module fails closed
rather than silently dropping an assignment.
When you add a replacement, the array rebalances — it copies the failed
member's already-reconstructed content straight from the distributed spare onto
the new disk (--layout-transparent; just --add the replacement):
mdadm /dev/md0 --add /dev/sdp # copy-from-spare onto the replacementThis copy-from-spare path (16 parallel workers, no GF decode, the array never degraded during it — reads are served live from the replacement below the copy mark and from the spare above it, split at a strict-journaled per-band mark) is faster and safer than a decode rebuild, and falls back to the validated decode leg on any persistent copy fault. After it completes the spare columns are freed and the pool is whole again.
--checksum composes with --layout=declustered:
mdadm --create /dev/md0 --level=raidkm --parity-count=2 \
--layout=declustered --group-width=6 --spare-columns=2 --checksum \
--raid-devices=14 /dev/sd[b-o]Each member's tail holds both reserves, stacked — the rkdcl metadata block
first, then the CRC region (the kernel derives the CRC region one chunk past the
data area). CRCs are keyed by the physical pool disk a block lives on, not its
logical slot, so a block served through the distributed-spare redirect still
verifies against the disk it was read from, and the checksum-driven self-heal
reconstructs a silently-corrupt block from its group's parity exactly as on a
non-declustered array. The copy-from-spare rebalance migrates each copied
block's CRC from the spare disk's key to the replacement's alongside the bytes
(never recomputing — a torn or rotted copy is copied verbatim but its original CRC
travels with it, so the first read of the new disk detects the mismatch and heals
by decode). Gate: tools/raidkm-test-declustered-csum.sh.
Chunk-aligned reads take the same direct read bypass as classic raidkm
(2026-07-19): a healthy declustered array serves an in-chunk read straight from
the mapped pool disk (forward permutation + redirect chain), never touching the
stripe cache; with native checksums the bypass read is still verified at
completion against the physical disk's CRCs, and a mismatch is rechecked and
healed through the stripe cache. Gate: tools/raidkm-test-declustered-aligned.sh.
A declustered array can be widened — add disks to the pool and it re-tiles
each row into more k+m groups, growing capacity and rebuild parallelism while
keeping groups narrow. Group width g, parity m and spare-column count s
stay fixed (so (N'−s) mod g == 0 must hold, which mdadm checks); only the pool
size and the permutation change. Usage:
mdadm --add /dev/mdN /dev/newdisk... # add the new pool disks as spares
mdadm --grow /dev/mdN --raid-devices=N' # widen the pool
mdadm runs the acceptance search for the new pool's permutation seed, then the
kernel migrates the array one band (device row) at a time with a
COW-staged, journaled reshape: each new row is read from the old permutation,
re-encoded per group, staged to a scratch region, then committed to its home —
so a power loss mid-reshape is recovered by a plain mdadm --assemble replaying
the journal, no backup file. The array stays readable/writable throughout.
Validated on real kernels (m=2 and m=3): data intact across the reshape, scrub
clean, degraded-read EC-correct, and crash-recovered from clean-stop, atomic
power-loss (dm-flakey) and deterministic torn STAGE/COMMIT injection. The
COW-staged reshape engine is described in notes/reshape-cow-design.md; gates
tools/raidkm-test-declustered-reshape*.sh.
The within-group geometry can also be changed online, through the same journaled COW engine:
mdadm --grow /dev/mdN --add-parity <newdisks> # m -> m+1 (g -> g+1; capacity fixed)
mdadm --grow /dev/mdN --add-data <newdisks> # k -> k+1 (g -> g+1; capacity grows)
mdadm --grow /dev/mdN --spare-columns=s' # s -> s' (either direction)
mdadm --grow /dev/mdN --raid-devices=<N-g> # pool shrink: remove one group's disks
add-parity/add-data take one new disk per group (ngroups disks); spare-count
runs on the fixed pool (delta_disks == 0). The capacity-shrinking
kinds — pool shrink and spare-count increase — run the same engine
backwards (the sparser new geometry packs data deeper, so the walk starts
at the top row and descends) and are gated array-size-first: shrink the
filesystem, clamp with --grow --array-size=<kb> (printed exactly on
refusal), then reshape. After a pool shrink the departing members become
spares — --remove and --zero-superblock them before reuse (they hold a
readable pre-shrink copy and are never auto-zeroed; a member failure before
their removal will legitimately pull them back in as hot-spare rebuild
targets). While the migration walks the
array, the not-yet-migrated region is served by old-geometry stripes — the
stripe carries its geometry (old group width, old parity count, old EC tables)
so concurrent reads and writes on either side of the frontier are correct, the
same mechanism at a different axis as pool expansion's dual permutation maps.
Crash recovery is identical (journal v2 records both geometries).
Native-checksum arrays run these online too: CRC store/verify on the
stripe path is keyed by the stripe's own geometry, and the band's CRC re-key
runs inside the migrating row's claim/quiesce bracket, so concurrent I/O
never sees a stale key.
A declustered array currently refuses (rather than running wrong): a
file-backed write-intent bitmap (the internal bitmap and online member
resize are supported), a pool shrink of more than one group per reshape, and
online member-size shrink. The group-geometry reshapes — per-group
add-parity (--grow --add-parity), add-data (--grow --add-data)
and spare-count change in either direction
(--grow --spare-columns=s') — run online like pool expansion: the
un-migrated region is served with old-geometry stripes while the journaled
COW migration walks the array — native-checksum arrays included.
Capacity-shrinking reshapes run online too (backward COW walk): a pool
shrink removes one group's worth of disks
(--grow --raid-devices=<N-g>; the departing members become spares at
completion — --remove + --zero-superblock them before reuse), and a
spare-count increase trades capacity back into spare columns. Both are
gated array-size-first: shrink the filesystem, clamp with
--grow --array-size=<kb> (mdadm computes and prints the exact value on
refusal), then reshape. Crash-safe like every other kind: a mid-walk stop or
power loss recovers by plain --assemble and resumes the descending walk.
Validation gates: tools/raidkm-test-declustered-*.sh.
Besides mdadm, raidkm arrays can be driven through device-mapper — either
directly with dmsetup (the kernel dm-raid target), or, the managed way, as
LVM logical volumes. This needs no new dm target: dm-raid already stands up
an mddev and runs whatever personality md_run() selects by level, so level 71
rides the existing target. (The enabling dm-raid.c changes live in the
mdraid fork; full design and
validation are in notes/dm-raid-design.md.)
# 3 data + 2 parity (m=2), rotating, 512 KiB chunk, over 5 devices:
dmsetup create kmtest --table \
"0 <sectors> raid raidkm 3 1024 parity_count 2 5 - /dev/ram0 - /dev/ram1 - /dev/ram2 - /dev/ram3 - /dev/ram4"
dmsetup status kmtest # health string + sync_action + mismatch_cnt
dmsetup message kmtest 0 check # scrubDegraded = suspend / reload-with-- - in the victim slot / resume. Rebuild =
reload with a fresh device + a rebuild <idx> parameter. raidkm is the
rotating layout; raidkm_n is parity-last.
The lvm2 raidkm fork registers two segtypes — raidkm (rotating) and raidkm_n
(parity-last) — each carrying the parity count m:
lvcreate --type raidkm --paritycount 2 -i 3 -L 1G -n data vg # 3 data + 2 parity
lvconvert --repair vg/data # raidkm-aware leg replace + rebuild
lvchange --monitor y vg/data # dmeventd auto-repairCreate / activate / I/O / reassembly / degraded read, lvconvert --repair, and
dmeventd auto-repair are validated for m=2/3/4 on both the base and GFNI EC paths.
Because the dm/LVM reshape path (out-of-place via data-offset) does not fit
raidkm's layout, growing/shrinking a raidkm LV is not supported via LVM — use
mdadm --grow for capacity/parity changes (the kernel gate rejects dm reshape).
An LVM raidkm LV is an ordinary cache origin, so it can be fronted by lvmcache
(lvconvert --type cache) — e.g. a fast tier over the EC capacity tier under a
filesystem. See notes/rhel9-lvmcache-ost.md (still only on the legacy
rhel9-port branch) for an end-to-end
dm-cache → dm-raid(raidkm) validation on RHEL 9.
GPL-2.0-only. See LICENSE.