From 8e4e954bce1290a384ca2ed371d96afe940c5db7 Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Sun, 6 Sep 2026 14:23:37 -0400 Subject: [PATCH] btrfs: stripe_alloc: hand groups full of trapped free space to the reclaim worker 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 | 59 +++++++++++++++++++++++++++++++-- fs/btrfs/block-group.h | 4 ++- fs/btrfs/free-space-cache.c | 65 +++++++++++++++++++++++++++++++++++++ fs/btrfs/free-space-cache.h | 1 + 4 files changed, 125 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index cf8a850dad778..ec66d61c0c4c4 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -4504,21 +4504,74 @@ end: sb_end_write(fs_info->sb); } +/* + * 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, diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 98d1e8c58c661..34355efe3d699 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -292,6 +292,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 @@ -543,7 +545,7 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info); void btrfs_mark_bg_unused(struct btrfs_block_group *bg); 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, diff --git a/fs/btrfs/free-space-cache.c b/fs/btrfs/free-space-cache.c index 775a19a5816e1..0dc195311e3be 100644 --- a/fs/btrfs/free-space-cache.c +++ b/fs/btrfs/free-space-cache.c @@ -3456,6 +3456,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 @@ -3466,6 +3505,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; @@ -3498,9 +3538,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 diff --git a/fs/btrfs/free-space-cache.h b/fs/btrfs/free-space-cache.h index 2e7e9d259b220..dee6cd250226c 100644 --- a/fs/btrfs/free-space-cache.h +++ b/fs/btrfs/free-space-cache.h @@ -153,6 +153,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, -- 2.53.0