From: Zygo Blaxell Date: Sun, 6 Sep 2026 18:23:37 +0000 (-0400) Subject: btrfs: stripe_alloc: re-arm reclaim from the cleaner, and only stamp a mark that... X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=db144e2f990cc91af89cd86ec696f253dc266b5d;p=linux btrfs: stripe_alloc: re-arm reclaim from the cleaner, and only stamp a mark that landed The trapped-space trigger lives in the commit-time rescan of groups whose free space changed, and stamps the five-minute hysteresis whenever it decides to mark a group. Two gaps followed. A group already on another list cannot be linked to the reclaim list -- a chunk created in the running transaction is still on trans->new_bgs -- so the mark was lost while the stamp held it back for five minutes. And on an idle filesystem there is no commit, so nothing rescans: the metadata fill test watched an eligible 63 MiB group sit unrelocated at the edge for fifteen minutes with the worker running for others. Stamp the hysteresis only when the mark actually lands, and re-evaluate every stripe group from the cleaner, which the transaction thread wakes every cycle whether or not a transaction exists, with the same predicate (factored out as btrfs_stripe_bg_wants_reclaim()) and hysteresis. The worker re-checks whatever it takes, so a stale read here costs at most one wasted attempt. Assisted-by: Claude:claude-fable-5 --- diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 712907923346d..2de9d007456cb 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -4439,21 +4439,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 44eee7e5e740d..0356b148fc7d8 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -544,7 +544,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 6c0c797f7a8cf..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,7 +3505,6 @@ 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; - int thresh = READ_ONCE(bg->space_info->bg_reclaim_threshold); bool reclaim = false; u64 nstripes; u32 *freep; @@ -3501,44 +3539,31 @@ void btrfs_block_group_rescan_stripe_unusable(struct btrfs_block_group *bg) bg->start, bg->stripe_claimable, scanned); bg->stripe_claimable = scanned; /* - * 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), hand it to the reclaim worker - * so 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; the - * worker re-checks with should_reclaim_block_group(). + * 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 (thresh > 0 && !bg->ro && - (!bg->stripe_reclaim_jiffies || + if ((!bg->stripe_reclaim_jiffies || time_after(jiffies, bg->stripe_reclaim_jiffies + 5 * 60 * HZ)) && - bg->stripe_unusable >= mult_perc(bg->length - min(bg->used, bg->length), thresh) && - bg->used < mult_perc(bg->length, thresh)) { - /* - * Only when the live data fits in the claimable supply - * of the OTHER groups: relocation needs whole stripes - * too, and a group queued every commit while there is - * nowhere to move it starves writers with read-only - * transitions and fails each time at the fill edge. - */ - struct btrfs_space_info *sinfo = bg->space_info; - u64 elsewhere = READ_ONCE(sinfo->bytes_stripe_claimable); - u64 promised = READ_ONCE(sinfo->bytes_may_use) + - READ_ONCE(sinfo->bytes_stripe_margin); - - elsewhere -= min(elsewhere, bg->stripe_claimable); - elsewhere -= min(elsewhere, promised); - if (elsewhere >= bg->used) - reclaim = true; - } + btrfs_stripe_bg_wants_reclaim(bg)) + reclaim = true; } spin_unlock(&ctl->tree_lock); kvfree(freep); if (reclaim) { - bg->stripe_reclaim_jiffies = jiffies ?: 1; + /* + * 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); - btrfs_mark_bg_to_reclaim(bg); + if (btrfs_mark_bg_to_reclaim(bg)) + bg->stripe_reclaim_jiffies = jiffies ?: 1; spin_unlock(&bg->lock); } /* 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,