]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: hand groups full of trapped free space to the reclaim worker
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 6 Sep 2026 18:23:37 +0000 (14:23 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:26 +0000 (17:36 -0400)
Zoned filesystems mark a block group for reclaim as soon as its
unusable bytes reach bg_reclaim_threshold percent of its capacity, so
space that can no longer be written is recycled without an operator
running balance.  stripe_alloc had no equivalent.  The non-zoned
trigger consults the threshold only where a free drops a group's used
bytes across it, and the test hosts were already running every mount
with bg_reclaim_threshold=75 when this was measured: trapped tails
still accumulated until balance (1.86 GiB of 6.5 GiB after a few hours
of churn), while every byte-based check counted them as free, because
that predicate keys on used bytes only and fires only at the crossing
-- a group hollowed out after it was already below the threshold, or
one whose used bytes never moved while its stripes trapped, is never
queued, and a group at the same used fraction with nothing trapped is.

Mark a group for the existing reclaim worker when its trapped bytes
reach the threshold; the worker re-checks with
should_reclaim_block_group().  The predicate, factored out as
btrfs_stripe_bg_wants_reclaim(), compares the trapped bytes with the
group's free space (length minus used) rather than its length -- a
stripe_alloc group also holds claimable whole stripes, and in practice
fragmentation settles around two thirds of the group trapped with a
fifth used, nothing left worth writing into yet below any sensible
fraction of the length -- and requires room for the group's live data
in the rest of the claimable supply after the bytes already promised to
admitted writers, so a group that cannot be relocated is left alone
until space is freed instead of being retried every commit while the
read-only transitions starve writers.

The trigger runs from the cleaner, which the transaction thread wakes
every cycle whether or not a transaction exists, as well as at the
commit-time rescan: on an idle filesystem nothing rescans, and a group
already on another list (a chunk created in the running transaction is
still on trans->new_bgs) cannot be linked to the reclaim list, so the
five-minute hysteresis stamp is taken only when the mark actually
lands.  The worker re-checks whatever it takes, so a stale read costs at
most one wasted attempt.  bg_reclaim_threshold defaults to 0 on
non-zoned filesystems, so nothing changes until the sysfs knob is set;
the default is left for review.  Relocation needs inc_block_group_ro()
to succeed, hence the earlier fix.

Assisted-by: Claude:claude-fable-5
Assisted-by: Claude:claude-fable-5-1
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/free-space-cache.c
fs/btrfs/free-space-cache.h

index 335291cc3c9614dfc285bddadc98002f8d027f75..312dca95ae1bdec53dd3514eb25012d2b2172335 100644 (file)
@@ -4604,21 +4604,74 @@ void btrfs_reclaim_bgs_work(struct work_struct *work)
        btrfs_reclaim_block_groups(fs_info, -1);
 }
 
+/*
+ * Periodic re-evaluation of stripe_alloc / stripe_meta groups from the
+ * cleaner.  The commit-time rescan only sees groups whose free space changed
+ * in that transaction, and only when there is a transaction: a mark that
+ * did not land (the group was on another list) or a group the worker dropped
+ * would otherwise wait for the next commit, which an idle filesystem at the
+ * fill edge never produces.  Same predicate and five-minute hysteresis as
+ * the rescan; the worker re-checks everything it takes.
+ */
+static void btrfs_stripe_reclaim_tick(struct btrfs_fs_info *fs_info)
+{
+       struct btrfs_space_info *sinfo;
+
+       if (!btrfs_test_opt(fs_info, STRIPE_ALLOC))
+               return;
+       list_for_each_entry(sinfo, &fs_info->space_info, list) {
+               int raid;
+
+               if (!(sinfo->flags & (BTRFS_BLOCK_GROUP_DATA |
+                                     BTRFS_BLOCK_GROUP_METADATA)))
+                       continue;
+               if (READ_ONCE(sinfo->bg_reclaim_threshold) <= 0)
+                       continue;
+               down_read(&sinfo->groups_sem);
+               for (raid = 0; raid < BTRFS_NR_RAID_TYPES; raid++) {
+                       struct btrfs_block_group *bg;
+
+                       if (!(btrfs_raid_array[raid].bg_flag &
+                             BTRFS_BLOCK_GROUP_RAID56_MASK))
+                               continue;
+                       list_for_each_entry(bg, &sinfo->block_groups[raid],
+                                           list) {
+                               if (bg->stripe_reclaim_jiffies &&
+                                   !time_after(jiffies,
+                                               bg->stripe_reclaim_jiffies +
+                                               5 * 60 * HZ))
+                                       continue;
+                               if (!btrfs_stripe_bg_wants_reclaim(bg))
+                                       continue;
+                               spin_lock(&bg->lock);
+                               if (btrfs_mark_bg_to_reclaim(bg))
+                                       bg->stripe_reclaim_jiffies = jiffies ?: 1;
+                               spin_unlock(&bg->lock);
+                       }
+               }
+               up_read(&sinfo->groups_sem);
+       }
+}
+
 void btrfs_reclaim_bgs(struct btrfs_fs_info *fs_info)
 {
        btrfs_reclaim_sweep(fs_info);
+       btrfs_stripe_reclaim_tick(fs_info);
        spin_lock(&fs_info->unused_bgs_lock);
        if (!list_empty(&fs_info->reclaim_bgs))
                queue_work(system_dfl_wq, &fs_info->reclaim_bgs_work);
        spin_unlock(&fs_info->unused_bgs_lock);
 }
 
-void btrfs_mark_bg_to_reclaim(struct btrfs_block_group *bg)
+/* Returns true when the group was linked to the reclaim list. */
+bool btrfs_mark_bg_to_reclaim(struct btrfs_block_group *bg)
 {
        struct btrfs_fs_info *fs_info = bg->fs_info;
 
-       if (btrfs_link_bg_list(bg, &fs_info->reclaim_bgs))
-               trace_btrfs_add_reclaim_block_group(bg);
+       if (!btrfs_link_bg_list(bg, &fs_info->reclaim_bgs))
+               return false;
+       trace_btrfs_add_reclaim_block_group(bg);
+       return true;
 }
 
 static int read_bg_from_eb(struct btrfs_fs_info *fs_info, const struct btrfs_key *key,
index f8cb733ad377286e09f1eb0c911206d539e62e5e..0f8e4c165d54ce6fc432bd83f31cd6814a413c55 100644 (file)
@@ -300,6 +300,8 @@ struct btrfs_block_group {
         * (honest f_bavail); the reservation layer is deliberately unchanged.
         */
        u64 stripe_unusable;
+       /* jiffies when the reclaim trigger last queued this group (stripe_alloc) */
+       unsigned long stripe_reclaim_jiffies;
        /*
         * Directly measured claimable supply: bytes of fully free, aligned
         * whole stripes in the free space cache -- exactly what
@@ -555,7 +557,7 @@ void btrfs_mark_bg_unused(struct btrfs_block_group *bg);
 void btrfs_reclaim_block_groups(struct btrfs_fs_info *fs_info, unsigned int limit);
 void btrfs_reclaim_bgs_work(struct work_struct *work);
 void btrfs_reclaim_bgs(struct btrfs_fs_info *fs_info);
-void btrfs_mark_bg_to_reclaim(struct btrfs_block_group *bg);
+bool btrfs_mark_bg_to_reclaim(struct btrfs_block_group *bg);
 int btrfs_read_block_groups(struct btrfs_fs_info *info);
 struct btrfs_block_group *btrfs_make_block_group(struct btrfs_trans_handle *trans,
                                                 struct btrfs_space_info *space_info,
index b4526c199ffb53553ed2deb4d9ba7478afadf1c2..58f02314aef856fec8dd50d5fb89fe213bcf5f42 100644 (file)
@@ -2185,6 +2185,45 @@ void btrfs_block_group_disarm_stripe_unusable(struct btrfs_block_group *bg)
 }
 
 
+/*
+ * Does this stripe_alloc / stripe_meta group want the reclaim worker?  Like
+ * zoned's zone_unusable reclaim: once enough of the group's FREE space is
+ * trapped in partially used stripes (zoned compares with the capacity, but a
+ * stripe group also holds claimable whole stripes), relocation packs the live
+ * extents into whole stripes elsewhere and frees the group.  Only groups
+ * whose live data is below the same threshold are cheap enough to move, and
+ * only when that data fits in the claimable supply of the OTHER groups:
+ * relocation needs whole stripes too, and a group queued while there is
+ * nowhere to move it starves writers with read-only transitions and fails
+ * each time at the fill edge.  The worker re-checks with
+ * should_reclaim_block_group().  Reads the counters without locks; the
+ * commit-time rescan calls it with fresh values, the cleaner's periodic
+ * re-evaluation with slightly stale ones, and either error is caught by
+ * the worker.
+ */
+bool btrfs_stripe_bg_wants_reclaim(struct btrfs_block_group *bg)
+{
+       struct btrfs_space_info *sinfo = bg->space_info;
+       const int thresh = READ_ONCE(sinfo->bg_reclaim_threshold);
+       const u64 used = READ_ONCE(bg->used);
+       const u64 trapped = READ_ONCE(bg->stripe_unusable);
+       u64 elsewhere;
+       u64 promised;
+
+       if (thresh <= 0 || bg->ro || !READ_ONCE(bg->stripe_unusable_ready))
+               return false;
+       if (trapped < mult_perc(bg->length - min(used, bg->length), thresh))
+               return false;
+       if (used >= mult_perc(bg->length, thresh))
+               return false;
+       elsewhere = READ_ONCE(sinfo->bytes_stripe_claimable);
+       promised = READ_ONCE(sinfo->bytes_may_use) +
+                  READ_ONCE(sinfo->bytes_stripe_margin);
+       elsewhere -= min(elsewhere, READ_ONCE(bg->stripe_claimable));
+       elsewhere -= min(elsewhere, promised);
+       return elsewhere >= used;
+}
+
 /*
  * Recompute a stripe_alloc block group's trapped free space from a fresh scan
  * and fold the change into the space_info total.  Called at commit for groups
@@ -2195,6 +2234,7 @@ void btrfs_block_group_disarm_stripe_unusable(struct btrfs_block_group *bg)
 void btrfs_block_group_rescan_stripe_unusable(struct btrfs_block_group *bg)
 {
        struct btrfs_free_space_ctl *ctl = bg->free_space_ctl;
+       bool reclaim = false;
        u64 nstripes;
        u32 *freep;
 
@@ -2227,9 +2267,34 @@ void btrfs_block_group_rescan_stripe_unusable(struct btrfs_block_group *bg)
                          "bg %llu incremental stripe_claimable %llu above scanned %llu",
                          bg->start, bg->stripe_claimable, scanned);
                bg->stripe_claimable = scanned;
+               /*
+                * Hand a group full of trapped free space to the reclaim
+                * worker (btrfs_stripe_bg_wants_reclaim()), but not again
+                * within five minutes of the last mark: a relocation that
+                * failed at the fill edge is not going to succeed a commit
+                * later, and re-queueing it every commit starves writers
+                * with read-only transitions.
+                */
+               if ((!bg->stripe_reclaim_jiffies ||
+                    time_after(jiffies, bg->stripe_reclaim_jiffies + 5 * 60 * HZ)) &&
+                   btrfs_stripe_bg_wants_reclaim(bg))
+                       reclaim = true;
        }
        spin_unlock(&ctl->tree_lock);
        kvfree(freep);
+       if (reclaim) {
+               /*
+                * Stamp the hysteresis only when the mark lands.  A group
+                * that is already on another list (a chunk created this
+                * transaction is still on trans->new_bgs) cannot be linked,
+                * and stamping it anyway lost the mark for five minutes and,
+                * on an idle filesystem with no commit to rescan it, for good.
+                */
+               spin_lock(&bg->lock);
+               if (btrfs_mark_bg_to_reclaim(bg))
+                       bg->stripe_reclaim_jiffies = jiffies ?: 1;
+               spin_unlock(&bg->lock);
+       }
        /*
         * The space_info total is recomputed authoritatively from the sum of
         * armed groups by btrfs_scan_stripe_unusable() after all rescans, so it
index 1cff596a4900dc52f929fbdccc84c969624066d6..330bb988f959286d95df9f366a64cf6ad4e64a83 100644 (file)
@@ -118,6 +118,7 @@ int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group,
 void btrfs_block_group_init_stripe_unusable(struct btrfs_block_group *block_group);
 void btrfs_block_group_disarm_stripe_unusable(struct btrfs_block_group *block_group);
 void btrfs_block_group_rescan_stripe_unusable(struct btrfs_block_group *block_group);
+bool btrfs_stripe_bg_wants_reclaim(struct btrfs_block_group *bg);
 void btrfs_dump_free_space(struct btrfs_block_group *block_group,
                           u64 bytes);
 int btrfs_find_space_cluster(struct btrfs_block_group *block_group,