]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: keep an open run per size band to trap less
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Tue, 28 Jul 2026 00:55:59 +0000 (20:55 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:40:00 +0000 (17:40 -0400)
A single open stripe run per class forces every allocation that does not
fit the current run's remainder to close that run -- trapping its
unallocated tail in a now partially filled stripe -- and claim a fresh
fully-free stripe.  A workload that interleaves small and large extents
(the common case) therefore strands a tail on every size change, even
though a later small allocation could have filled it.

Keep one open run per power-of-two band of remaining free space instead
(band k holds a run with remaining in [2^k, 2^(k+1)) sectors), per class.
An allocation takes the run in the smallest band that still fits --
segregated best fit -- so a small write lands in an already-open, nearly
full stripe rather than opening a new one, while a large write that no
open run can hold opens a fresh stripe and leaves the smaller runs open
for the small writes that do fit them.  After each allocation the run is
re-placed into the band its new remainder falls in; when two runs collide
in a band the fuller one is kept -- it can serve larger future
allocations, and closing it would trap more -- and the other is closed,
its tail returned to the free space cache.

16 bands cover a full stripe of up to 2^16 sectors, far more than any
raid56 geometry (nr_data_stripes * stripe_len / sectorsize is 158 sectors
at 10 data stripes and a 64K stripe).  Commit-time retirement and the
read-only / removal paths now walk every band of every class; only the
bg->open_stripe[][] slots can ever hold an open run, so one pass still
suffices, and the obsolete single-slot assertions are dropped.

This does not touch the write-hole guarantee: every run is still a
contiguous fill of fully-free stripes, closed at commit and never
reopened.  It only changes which open run an allocation joins, reducing
the free space trapped in partial stripes -- and thus stripe_unusable --
for mixed-size workloads.

The open-stripe selftest is rewritten to the multi-cursor contract: a
misfit allocation now keeps the old run open (its tail is not returned to
the cache) and a later small allocation backfills it, so fewer stripes are
claimed before the free space is exhausted.

Assisted-by: Claude:claude-opus-4-8
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/tests/free-space-tests.c

index bac15db5a1f29e47b7535f4f8f2e06a3a8e9e29b..7a53d2624a6b44a3b55fde8302c194a649cff143 100644 (file)
@@ -476,12 +476,30 @@ static bool open_stripe_run_drained(const struct btrfs_open_stripe_run *run)
        return !run->open && run->inflight_bytes == 0;
 }
 
+/*
+ * Band for a run with @remaining bytes of unallocated space: the power-of-two
+ * bucket of its remaining sectors (band k == remaining in [2^k, 2^(k+1))
+ * sectors).  Runs are kept at most one per band so an allocation can take the
+ * smallest band that fits.
+ */
+static int stripe_run_band(const struct btrfs_block_group *bg, u64 remaining)
+{
+       u32 sectors = remaining >> bg->fs_info->sectorsize_bits;
+       int band;
+
+       if (!sectors)
+               return 0;
+       band = fls(sectors) - 1;
+       if (band >= BTRFS_STRIPE_RUN_NR_BANDS)
+               band = BTRFS_STRIPE_RUN_NR_BANDS - 1;
+       return band;
+}
+
 /* Free a drained run.  Caller holds bg->stripe_run_lock. */
 static void free_open_stripe_run(struct btrfs_block_group *bg,
                                 struct btrfs_open_stripe_run *run)
 {
        ASSERT(open_stripe_run_drained(run));
-       ASSERT(bg->open_stripe[run->class] != run);
        list_del(&run->list);
        kfree(run);
        wake_up_var(&bg->open_stripe_runs);
@@ -489,9 +507,9 @@ static void free_open_stripe_run(struct btrfs_block_group *bg,
 
 /*
  * Close a run: no further allocation may join it, and its unallocated tail
- * must be returned to the free space cache by the caller (after dropping
- * the lock).  Caller holds bg->stripe_run_lock.  Returns the tail length
- * and sets *tail_start.
+ * must be returned to the free space cache by the caller (after dropping the
+ * lock).  The caller has already removed @run from bg->open_stripe[][].
+ * Caller holds bg->stripe_run_lock.  Returns the tail length, sets *tail_start.
  */
 static u64 close_open_stripe_run(struct btrfs_block_group *bg,
                                 struct btrfs_open_stripe_run *run,
@@ -500,9 +518,7 @@ static u64 close_open_stripe_run(struct btrfs_block_group *bg,
        u64 tail_len;
 
        ASSERT(run->open);
-       ASSERT(bg->open_stripe[run->class] == run);
        run->open = false;
-       bg->open_stripe[run->class] = NULL;
        *tail_start = run->offset;
        tail_len = run->end - run->offset;
        run->end = run->offset;
@@ -511,6 +527,37 @@ static u64 close_open_stripe_run(struct btrfs_block_group *bg,
        return tail_len;
 }
 
+/*
+ * Install @run (not currently in any band slot) into its remaining-space band.
+ * If a run already occupies that band, keep whichever has more remaining -- it
+ * can serve larger future allocations, and closing the other traps less -- and
+ * close the loser, returning its tail via *tail_start.  Caller holds
+ * bg->stripe_run_lock.  Returns the closed tail length, or 0 if no collision.
+ */
+static u64 stripe_run_place(struct btrfs_block_group *bg,
+                           struct btrfs_open_stripe_run *run, u64 *tail_start)
+{
+       int class = run->class;
+       int band = stripe_run_band(bg, run->end - run->offset);
+       struct btrfs_open_stripe_run *occ = bg->open_stripe[class][band];
+
+       if (occ) {
+               struct btrfs_open_stripe_run *keep, *shut;
+
+               if (run->end - run->offset >= occ->end - occ->offset) {
+                       keep = run;
+                       shut = occ;
+               } else {
+                       keep = occ;
+                       shut = run;
+               }
+               bg->open_stripe[class][band] = keep;
+               return close_open_stripe_run(bg, shut, tail_start);
+       }
+       bg->open_stripe[class][band] = run;
+       return 0;
+}
+
 /*
  * Allocate num_bytes from the block group's open stripe run, opening a new
  * run via btrfs_claim_free_stripe_run() when there is none or the current
@@ -533,26 +580,46 @@ int btrfs_alloc_from_open_stripe(struct btrfs_block_group *bg, u64 num_bytes,
        u64 tail_len = 0;
        u64 start;
        u64 len;
+       int band;
        int ret;
 
        ASSERT(num_bytes);
        *available = 0;
 
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
-       run = bg->open_stripe[class];
-       if (run && num_bytes <= run->end - run->offset) {
+       /*
+        * Best fit: take the run in the smallest band that can hold this
+        * allocation, and track the largest remaining across all bands as the
+        * retry hint should nothing fit.
+        */
+       run = NULL;
+       for (band = 0; band < BTRFS_STRIPE_RUN_NR_BANDS; band++) {
+               struct btrfs_open_stripe_run *r = bg->open_stripe[class][band];
+               u64 rem;
+
+               if (!r)
+                       continue;
+               rem = r->end - r->offset;
+               if (rem > *available)
+                       *available = rem;
+               if (!run && num_bytes <= rem) {
+                       run = r;
+                       bg->open_stripe[class][band] = NULL;
+               }
+       }
+       if (run) {
                *ret_offset = run->offset;
                run->offset += num_bytes;
                run->inflight_bytes += num_bytes;
-               if (run->offset == run->end) {
-                       run->open = false;
-                       bg->open_stripe[class] = NULL;
-               }
+               if (run->offset == run->end)
+                       run->open = false;      /* full; drains then is freed */
+               else
+                       tail_len = stripe_run_place(bg, run, &tail_start);
                spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+               if (tail_len)
+                       btrfs_add_free_space(bg, tail_start, tail_len);
                return 0;
        }
-       if (run)
-               *available = run->end - run->offset;
        spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
 
        new_run = kmalloc(sizeof(*new_run), GFP_NOFS);
@@ -605,14 +672,6 @@ int btrfs_alloc_from_open_stripe(struct btrfs_block_group *bg, u64 num_bytes,
                ret = -ENOSPC;
                goto out;
        }
-       /*
-        * Close this class's open run if one (re)appeared: it was too small
-        * for us, or a racing opener's.  Closing is always safe; the last
-        * opener wins.
-        */
-       run = bg->open_stripe[class];
-       if (run)
-               tail_len = close_open_stripe_run(bg, run, &tail_start);
        new_run->bg = bg;
        new_run->class = class;
        new_run->start = start;
@@ -622,7 +681,13 @@ int btrfs_alloc_from_open_stripe(struct btrfs_block_group *bg, u64 num_bytes,
        new_run->open_seq = fs_info->stripe_retire_seq;
        new_run->open = (new_run->offset != new_run->end);
        list_add_tail(&new_run->list, &bg->open_stripe_runs);
-       bg->open_stripe[class] = new_run->open ? new_run : NULL;
+       /*
+        * Place the new run in its remaining-space band, closing whichever run
+        * loses a band collision (the last, fuller opener wins).  Other bands'
+        * open runs are untouched.
+        */
+       if (new_run->open)
+               tail_len = stripe_run_place(bg, new_run, &tail_start);
        spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
        spin_unlock(&fs_info->open_stripe_lock);
 
@@ -736,33 +801,37 @@ void btrfs_open_stripe_write_done_run(struct btrfs_open_stripe_run *run,
 
 /*
  * Close the block group's open runs that predate @seq and return their
- * unallocated tails to the free space cache.  Does not wait for inflight
- * IO.  Only the bg->open_stripe[] slots can ever be open, so one pass over
- * the classes suffices.
+ * unallocated tails to the free space cache.  Does not wait for inflight IO.
+ * Only the bg->open_stripe[][] slots can ever be open, so one pass over the
+ * classes and their remaining-space bands suffices.
  */
 static void close_block_group_stripe_runs(struct btrfs_block_group *bg,
                                          u64 seq)
 {
-       u64 tail_start[BTRFS_STRIPE_RUN_NR_CLASSES];
-       u64 tail_len[BTRFS_STRIPE_RUN_NR_CLASSES];
+       u64 tail_start[BTRFS_STRIPE_RUN_NR_CLASSES][BTRFS_STRIPE_RUN_NR_BANDS];
+       u64 tail_len[BTRFS_STRIPE_RUN_NR_CLASSES][BTRFS_STRIPE_RUN_NR_BANDS];
        struct btrfs_open_stripe_run *run;
        unsigned long flags;
-       int class;
+       int class, band;
 
        spin_lock_irqsave(&bg->stripe_run_lock, flags);
        for (class = 0; class < BTRFS_STRIPE_RUN_NR_CLASSES; class++) {
-               tail_len[class] = 0;
-               run = bg->open_stripe[class];
-               if (run && run->open_seq < seq)
-                       tail_len[class] = close_open_stripe_run(bg, run,
-                                                       &tail_start[class]);
+               for (band = 0; band < BTRFS_STRIPE_RUN_NR_BANDS; band++) {
+                       tail_len[class][band] = 0;
+                       run = bg->open_stripe[class][band];
+                       if (run && run->open_seq < seq) {
+                               bg->open_stripe[class][band] = NULL;
+                               tail_len[class][band] = close_open_stripe_run(
+                                       bg, run, &tail_start[class][band]);
+                       }
+               }
        }
        spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
-       for (class = 0; class < BTRFS_STRIPE_RUN_NR_CLASSES; class++) {
-               if (tail_len[class])
-                       btrfs_add_free_space(bg, tail_start[class],
-                                            tail_len[class]);
-       }
+       for (class = 0; class < BTRFS_STRIPE_RUN_NR_CLASSES; class++)
+               for (band = 0; band < BTRFS_STRIPE_RUN_NR_BANDS; band++)
+                       if (tail_len[class][band])
+                               btrfs_add_free_space(bg, tail_start[class][band],
+                                                    tail_len[class][band]);
 }
 
 /*
index d8dcdfa1cb6cae3f608dc3c4abfcd4f64b502dde..69f4546316be002029079989f20aa34d9edb36f1 100644 (file)
@@ -33,6 +33,16 @@ enum btrfs_stripe_run_class {
        BTRFS_STRIPE_RUN_NR_CLASSES,
 };
 
+/*
+ * Open stripe runs are kept in power-of-two bands of remaining free space:
+ * band k holds a run whose remaining is in [2^k, 2^(k+1)) sectors.  An
+ * allocation takes the smallest band that fits (segregated best fit), so it
+ * lands in an already-open stripe sized to it instead of forcing a fresh
+ * claim and trapping the old run's tail.  16 bands cover a full stripe of up
+ * to 2^16 sectors, far more than any raid56 geometry.
+ */
+#define BTRFS_STRIPE_RUN_NR_BANDS 16
+
 enum btrfs_disk_cache_state {
        BTRFS_DC_WRITTEN,
        BTRFS_DC_ERROR,
@@ -221,7 +231,8 @@ struct btrfs_block_group {
         */
        spinlock_t stripe_run_lock;
        struct list_head open_stripe_runs;
-       struct btrfs_open_stripe_run *open_stripe[BTRFS_STRIPE_RUN_NR_CLASSES];
+       struct btrfs_open_stripe_run
+               *open_stripe[BTRFS_STRIPE_RUN_NR_CLASSES][BTRFS_STRIPE_RUN_NR_BANDS];
        /*
         * Membership in fs_info->open_stripe_bgs, protected by
         * fs_info->open_stripe_lock; holds a block group reference.
index cd5c7f7bf8c71773a30fa61e13f0bfc928309bb8..ce2ee313c13c40739c4604c612341901ff39a703 100644 (file)
@@ -1261,7 +1261,12 @@ static int test_open_stripe(struct btrfs_block_group *cache, u32 sectorsize)
                goto out;
        }
 
-       /* Doesn't fit the remainder: closes the run, opens the next stripe. */
+       /*
+        * A full-stripe allocation cannot fit the open run's remainder, so it
+        * opens a fresh stripe.  With multiple cursors the old run stays open in
+        * its remaining-space band (its tail is not trapped); a single cursor
+        * would have closed it here.
+        */
        ret = btrfs_alloc_from_open_stripe(cache, fsl, BTRFS_STRIPE_RUN_COW,
                                           &offset, &avail);
        if (ret || offset != 2 * fsl) {
@@ -1269,29 +1274,34 @@ static int test_open_stripe(struct btrfs_block_group *cache, u32 sectorsize)
                ret = -EINVAL;
                goto out;
        }
-       /* The closed run's tail went back to the free space cache... */
-       if (!test_check_exists(cache, fsl + 4 * sectorsize,
-                              fsl - 4 * sectorsize)) {
-               test_err("closed run tail missing from free space");
+       /* The old run is still open, so its tail is NOT back in the cache. */
+       if (test_check_exists(cache, fsl + 4 * sectorsize,
+                             fsl - 4 * sectorsize)) {
+               test_err("kept-open run tail wrongly freed");
                ret = -EINVAL;
                goto out;
        }
-       /* ...but is part of a partial stripe, so the next alloc skips it. */
+       /* A small allocation backtracks to fill the still-open run. */
        ret = btrfs_alloc_from_open_stripe(cache, sectorsize,
                                           BTRFS_STRIPE_RUN_COW, &offset,
                                           &avail);
-       if (ret || offset != 3 * fsl) {
-               test_err("post-close alloc wrong: ret %d offset %llu",
+       if (ret || offset != fsl + 4 * sectorsize) {
+               test_err("backfill alloc wrong: ret %d offset %llu",
                         ret, offset);
                ret = -EINVAL;
                goto out;
        }
 
-       /* A different class never joins the cow run: fresh stripe. */
+       /*
+        * A different class never joins the cow run: it opens a fresh stripe.
+        * Only stripes #1 and #2 have been claimed so far (multi-cursor kept
+        * #1 open and backfilled it rather than claiming a new stripe for the
+        * small allocations), so the reloc class claims #3.
+        */
        ret = btrfs_alloc_from_open_stripe(cache, sectorsize,
                                           BTRFS_STRIPE_RUN_RELOC, &offset,
                                           &avail);
-       if (ret || offset != 4 * fsl) {
+       if (ret || offset != 3 * fsl) {
                test_err("reloc class alloc wrong: ret %d offset %llu",
                         ret, offset);
                ret = -EINVAL;
@@ -1301,51 +1311,66 @@ static int test_open_stripe(struct btrfs_block_group *cache, u32 sectorsize)
        ret = btrfs_alloc_from_open_stripe(cache, sectorsize,
                                           BTRFS_STRIPE_RUN_COW, &offset,
                                           &avail);
-       if (ret || offset != 3 * fsl + sectorsize) {
+       if (ret || offset != fsl + 5 * sectorsize) {
                test_err("cow run lost by class open: ret %d offset %llu",
                         ret, offset);
                ret = -EINVAL;
                goto out;
        }
 
-       /* Report all data IO complete, in pieces. */
+       /* Report all data IO complete, in pieces, at the offsets allocated. */
        btrfs_open_stripe_write_done(cache, fsl, 2 * sectorsize);
        btrfs_open_stripe_write_done(cache, fsl + 2 * sectorsize,
                                     2 * sectorsize);
+       btrfs_open_stripe_write_done(cache, fsl + 4 * sectorsize, sectorsize);
+       btrfs_open_stripe_write_done(cache, fsl + 5 * sectorsize, sectorsize);
        btrfs_open_stripe_write_done(cache, 2 * fsl, fsl);
        btrfs_open_stripe_write_done(cache, 3 * fsl, sectorsize);
-       btrfs_open_stripe_write_done(cache, 3 * fsl + sectorsize, sectorsize);
-       btrfs_open_stripe_write_done(cache, 4 * fsl, sectorsize);
 
        /*
-        * Commit-style retirement: closes both classes' open runs, returns
-        * their tails, drains (instantly, all IO reported done) and drops
-        * the block group's membership in fs_info->open_stripe_bgs.
+        * Commit-style retirement: closes every open run in every band of both
+        * classes, returns their tails, drains (instantly, all IO reported
+        * done) and drops the block group's membership in
+        * fs_info->open_stripe_bgs.  The cow run filled [fsl, fsl+6*sectorsize)
+        * and the reloc run filled [3*fsl, 3*fsl+sectorsize); each tail returns.
         */
        btrfs_retire_open_stripes(fs_info, NULL);
-       if (!test_check_exists(cache, 3 * fsl + 2 * sectorsize,
-                              fsl - 2 * sectorsize)) {
+       if (!test_check_exists(cache, fsl + 6 * sectorsize,
+                              fsl - 6 * sectorsize)) {
                test_err("retired cow run tail missing from free space");
                ret = -EINVAL;
                goto out;
        }
-       if (!test_check_exists(cache, 4 * fsl + sectorsize,
+       if (!test_check_exists(cache, 3 * fsl + sectorsize,
                               fsl - sectorsize)) {
                test_err("retired reloc run tail missing from free space");
                ret = -EINVAL;
                goto out;
        }
 
-       /* Stripe #5 was never claimed and is still fully free. */
+       /* Stripe #4 was never claimed and is still fully free. */
        ret = btrfs_alloc_from_open_stripe(cache, sectorsize,
                                           BTRFS_STRIPE_RUN_COW, &offset,
                                           &avail);
-       if (ret || offset != 5 * fsl) {
+       if (ret || offset != 4 * fsl) {
                test_err("post-retire alloc wrong: ret %d offset %llu",
                         ret, offset);
                ret = -EINVAL;
                goto out;
        }
+       btrfs_open_stripe_write_done(cache, 4 * fsl, sectorsize);
+       btrfs_retire_open_stripes(fs_info, NULL);
+
+       /* Stripe #5 is the last fully-free stripe. */
+       ret = btrfs_alloc_from_open_stripe(cache, sectorsize,
+                                          BTRFS_STRIPE_RUN_COW, &offset,
+                                          &avail);
+       if (ret || offset != 5 * fsl) {
+               test_err("last-stripe alloc wrong: ret %d offset %llu",
+                        ret, offset);
+               ret = -EINVAL;
+               goto out;
+       }
        btrfs_open_stripe_write_done(cache, 5 * fsl, sectorsize);
        btrfs_retire_open_stripes(fs_info, NULL);
 
@@ -1359,20 +1384,24 @@ static int test_open_stripe(struct btrfs_block_group *cache, u32 sectorsize)
                goto out;
        }
 
-       /* Clean up the partial-stripe remainders. */
+       /*
+        * Clean up the partial-stripe remainders: stripe #0's head, stripe #1's
+        * cow tail (10 sectors), and the 15-sector tails of stripes #3 (reloc),
+        * #4 and #5.  Stripe #2 was filled completely and has no remainder.
+        */
        ret = btrfs_remove_free_space(cache, sectorsize, fsl - sectorsize);
        if (ret) {
                test_err("error cleaning up head %d", ret);
                goto out;
        }
-       ret = btrfs_remove_free_space(cache, fsl + 4 * sectorsize,
-                                     fsl - 4 * sectorsize);
+       ret = btrfs_remove_free_space(cache, fsl + 6 * sectorsize,
+                                     fsl - 6 * sectorsize);
        if (ret) {
                test_err("error cleaning up tail 1 %d", ret);
                goto out;
        }
-       ret = btrfs_remove_free_space(cache, 3 * fsl + 2 * sectorsize,
-                                     fsl - 2 * sectorsize);
+       ret = btrfs_remove_free_space(cache, 3 * fsl + sectorsize,
+                                     fsl - sectorsize);
        if (ret) {
                test_err("error cleaning up tail 2 %d", ret);
                goto out;