From: Zygo Blaxell Date: Sun, 6 Sep 2026 02:58:50 +0000 (-0400) Subject: btrfs: stripe_meta: hold back whole stripes so tree blocks always land and trapped... X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9fd28cd89bec8ce17cd36ce35aff8e04b2d502f7;p=linux btrfs: stripe_meta: hold back whole stripes so tree blocks always land and trapped groups can be reclaimed Two failures at the metadata fill edge under stripe_meta, both seen with the tiny-file fill test on raid5 metadata. The first is a transaction abort. A tree block is reserved in bytes and admitted while claimable whole stripes cover the outstanding reservations, but it is placed in whole stripes: the open run it would have joined is closed at the transaction boundary, so every group claims at least one fresh full stripe per transaction. The last few megabytes of claimable supply go to those claims, made for reservations admitted earlier, and the next admitted tree block finds no stripe at all ("stripe_meta: tree block allocation of 16384 bytes returned ENOSPC: claimable 0 open 1622016 trapped 1614266368"), which aborts the transaction. Pre-pay two transactions' worth of claims -- twice the number of writable groups times the widest full stripe -- in the space_info's stripe margin, recomputed at each commit scan. Best-fit run selection opens a fresh run only when a group's runs are full, so this is a few megabytes, not a fraction of the space_info. The second is a filesystem that quietly runs out at half capacity. COW frees trap stripes rather than free them: a stripe is claimable again only once every tree block in it is dead, which random deaths in a sixteen-block stripe essentially never achieve, so the METADATA space_info settles around half used, half trapped and no claimable stripe left (3.04 GiB metadata, 49% used, fs not writable, deletes included). Only relocation packs live tree blocks back into whole stripes, and it needs the moved group's live bytes claimable elsewhere before it starts -- by the time the trigger fires there is nothing left. Hold back a reserve of whole stripes -- the largest writable group's length, capped at a quarter of the space_info, nothing with a single group -- from every metadata reservation except those made by the relocation task, identified by a task pointer that reloc_ctl already implies. Reservations admitted against the reserve reach the same tickets, so the flag travels with the ticket. The reserve is an accounting hold-back, not a partition: a committing transaction still allocates from those stripes when it must, and the next scan restates the reserve from what remains. Both counters are cleared when stripe_alloc is turned off, since the scan that maintains them stops with it. The reserve is reported through sysfs as bytes_stripe_reserve and in the space_info dump, whose stripe_claimable and stripe_margin values were printed under each other's label. Assisted-by: Claude:claude-fable-5 --- diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 676fdd238759f..a018e2eae154b 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -1976,8 +1976,21 @@ static void stripe_alloc_sweep_groups(struct btrfs_fs_info *fs_info, bool arm) list_for_each_entry(sinfo, &fs_info->space_info, list) { int raid; - if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) + if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) { + /* + * Metadata: the scan that maintains the stripe_meta + * margin and reserve stops with the option, so clear + * them here or they hold metadata back forever. + */ + if (!arm && (sinfo->flags & BTRFS_BLOCK_GROUP_METADATA)) { + spin_lock(&sinfo->lock); + sinfo->bytes_stripe_margin = 0; + sinfo->bytes_stripe_reserve = 0; + btrfs_try_granting_tickets(sinfo); + spin_unlock(&sinfo->lock); + } continue; + } down_read(&sinfo->groups_sem); for (raid = 0; raid < BTRFS_NR_RAID_TYPES; raid++) { struct btrfs_block_group *bg; @@ -2604,6 +2617,9 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force) u64 total = 0; u64 total_claimable = 0; u64 total_claimable_reloc = 0; + u64 largest = 0; + u64 unit = 0; + u32 nr_rw = 0; int raid; /* @@ -2648,7 +2664,10 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force) if (bg->start == READ_ONCE(fs_info->data_reloc_bg)) total_claimable_reloc += READ_ONCE(bg->stripe_claimable); + nr_rw++; + largest = max(largest, bg->length); } + unit = max(unit, bg->full_stripe_len); } } up_read(&sinfo->groups_sem); @@ -2657,6 +2676,40 @@ void btrfs_scan_stripe_unusable(struct btrfs_fs_info *fs_info, bool force) sinfo->bytes_stripe_unusable = total; sinfo->bytes_stripe_claimable = total_claimable; sinfo->bytes_stripe_claimable_reloc = total_claimable_reloc; + if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) { + u64 reserve = 0; + + /* + * stripe_meta. A tree block is reserved in bytes but + * placed in whole stripes: the run it would join is + * closed at the transaction boundary, so each group + * claims at least one fresh stripe per transaction. + * Pre-pay two transactions' worth of such claims, or + * the last claimable stripes go to claims made for + * reservations admitted earlier and an admitted tree + * block finds none, which aborts the transaction. + */ + sinfo->bytes_stripe_margin = 2ULL * nr_rw * unit; + /* + * COW frees trap stripes rather than free them (a + * stripe is claimable again only once every block in + * it is dead), so the space_info settles around half + * used, half trapped. Only relocation packs live tree + * blocks back into whole stripes, and it needs the + * moved group's live bytes claimable elsewhere first. + * Hold back enough for any one group, capped at a + * quarter of the space_info, from everyone but the + * relocation task. With a single group there is no + * elsewhere to hold it for. This is an accounting + * hold-back only: a committing transaction still + * allocates from these stripes if it must. + */ + if (nr_rw > 1 && unit) + reserve = round_up(min(largest, + sinfo->total_bytes / 4), + unit); + sinfo->bytes_stripe_reserve = reserve; + } /* * The rescan usually lowers the counter (mid-transaction * incremental adds overcount conservatively); admission @@ -3849,7 +3902,7 @@ static int inc_block_group_ro(struct btrfs_block_group *cache, bool force) if (READ_ONCE(cache->stripe_unusable_ready)) trapped = min(READ_ONCE(cache->stripe_unusable), num_bytes); if (btrfs_can_overcommit(cache->fs_info, sinfo, num_bytes - trapped, - BTRFS_RESERVE_NO_FLUSH)) + BTRFS_RESERVE_NO_FLUSH, false)) ret = 0; } diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index b10c0321d63d7..10b6624895bf8 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -739,6 +739,13 @@ struct btrfs_fs_info { struct btrfs_space_info *data_sinfo; struct reloc_control *reloc_ctl; + /* + * The task running relocation while reloc_ctl is set. Under + * stripe_meta it alone may reserve metadata out of the whole stripes + * held back in bytes_stripe_reserve, so that a trapped metadata + * group can always be relocated (see btrfs_can_overcommit()). + */ + struct task_struct *reloc_task; /* data_alloc_cluster is only used in ssd_spread mode */ struct btrfs_free_cluster data_alloc_cluster; diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 403b6632002c5..5de5103f98b51 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -3436,6 +3436,7 @@ static void set_reloc_control(struct reloc_control *rc) mutex_lock(&fs_info->reloc_mutex); fs_info->reloc_ctl = rc; + WRITE_ONCE(fs_info->reloc_task, current); mutex_unlock(&fs_info->reloc_mutex); } @@ -3445,6 +3446,7 @@ static void unset_reloc_control(struct reloc_control *rc) mutex_lock(&fs_info->reloc_mutex); fs_info->reloc_ctl = NULL; + WRITE_ONCE(fs_info->reloc_task, NULL); mutex_unlock(&fs_info->reloc_mutex); } diff --git a/fs/btrfs/space-info.c b/fs/btrfs/space-info.c index 97f0e0c4372b0..f7fa2b6dd80e0 100644 --- a/fs/btrfs/space-info.c +++ b/fs/btrfs/space-info.c @@ -182,7 +182,7 @@ u64 __pure btrfs_space_info_used(const struct btrfs_space_info *s_info, s_info->bytes_pinned + s_info->bytes_readonly + s_info->bytes_zone_unusable + s_info->bytes_stripe_unusable + s_info->bytes_stripe_open + - s_info->bytes_stripe_margin + + s_info->bytes_stripe_margin + s_info->bytes_stripe_reserve + (may_use_included ? s_info->bytes_may_use : 0); } @@ -569,7 +569,7 @@ static u64 calc_available_free_space(struct btrfs_fs_info *fs_info, int btrfs_can_overcommit(struct btrfs_fs_info *fs_info, const struct btrfs_space_info *space_info, u64 bytes, - enum btrfs_reserve_flush_enum flush) + enum btrfs_reserve_flush_enum flush, bool reloc) { u64 avail; u64 used; @@ -579,6 +579,13 @@ int btrfs_can_overcommit(struct btrfs_fs_info *fs_info, return 0; used = btrfs_space_info_used(space_info, true); + /* + * The stripe_meta reserve exists for relocation: it is the room in + * which the live tree blocks of a trapped group are packed into whole + * stripes. Everyone else sees it as used. + */ + if (reloc) + used -= min(used, space_info->bytes_stripe_reserve); avail = calc_available_free_space(fs_info, space_info, flush); if (used + bytes < space_info->total_bytes + avail) @@ -673,9 +680,11 @@ again: ticket = list_first_entry(head, struct reserve_ticket, list); /* Check and see if our ticket can be satisfied now. */ + if (ticket->reloc) + used -= min(used, space_info->bytes_stripe_reserve); if (((used + ticket->bytes <= space_info->total_bytes) || btrfs_can_overcommit(fs_info, space_info, ticket->bytes, - flush)) && + flush, ticket->reloc)) && stripe_claimable_admit(space_info, ticket->bytes, false)) { btrfs_space_info_update_bytes_may_use(space_info, ticket->bytes); remove_ticket(space_info, ticket); @@ -741,13 +750,13 @@ static void __btrfs_dump_space_info(const struct btrfs_fs_info *fs_info, (s64)(info->total_bytes - btrfs_space_info_used(info, true)), info->full ? "" : "not "); btrfs_info(fs_info, -"space_info total=%llu, used=%llu, pinned=%llu, reserved=%llu, may_use=%llu, readonly=%llu zone_unusable=%llu stripe_unusable=%llu stripe_open=%llu stripe_margin=%llu stripe_claimable=%llu", +"space_info total=%llu, used=%llu, pinned=%llu, reserved=%llu, may_use=%llu, readonly=%llu zone_unusable=%llu stripe_unusable=%llu stripe_open=%llu stripe_claimable=%llu stripe_margin=%llu stripe_reserve=%llu", info->total_bytes, info->bytes_used, info->bytes_pinned, info->bytes_reserved, info->bytes_may_use, info->bytes_readonly, info->bytes_zone_unusable, info->bytes_stripe_unusable, info->bytes_stripe_open, info->bytes_stripe_claimable, - info->bytes_stripe_margin); + info->bytes_stripe_margin, info->bytes_stripe_reserve); } void btrfs_dump_space_info(struct btrfs_fs_info *fs_info, @@ -1880,6 +1889,7 @@ static int __reserve_bytes(struct btrfs_fs_info *fs_info, u64 used; int ret = -ENOSPC; bool pending_tickets; + const bool reloc = (READ_ONCE(fs_info->reloc_task) == current); ASSERT(orig_bytes); /* @@ -1919,9 +1929,12 @@ static int __reserve_bytes(struct btrfs_fs_info *fs_info, * Carry on if we have enough space (short-circuit) OR call * can_overcommit() to ensure we can overcommit to continue. */ + if (reloc) + used -= min(used, space_info->bytes_stripe_reserve); if (!pending_tickets && ((used + orig_bytes <= space_info->total_bytes) || - btrfs_can_overcommit(fs_info, space_info, orig_bytes, flush)) && + btrfs_can_overcommit(fs_info, space_info, orig_bytes, flush, + reloc)) && stripe_claimable_admit(space_info, orig_bytes, flush == BTRFS_RESERVE_FLUSH_DATA_RELOC)) { btrfs_space_info_update_bytes_may_use(space_info, orig_bytes); @@ -1954,6 +1967,7 @@ static int __reserve_bytes(struct btrfs_fs_info *fs_info, space_info->reclaim_size += ticket.bytes; init_waitqueue_head(&ticket.wait); ticket.steal = can_steal(flush); + ticket.reloc = reloc; if (trace_btrfs_reserve_ticket_enabled()) start_ns = ktime_get_ns(); diff --git a/fs/btrfs/space-info.h b/fs/btrfs/space-info.h index 989b1f6ad5b0f..1bdab3a6dfd61 100644 --- a/fs/btrfs/space-info.h +++ b/fs/btrfs/space-info.h @@ -167,7 +167,25 @@ struct btrfs_space_info { writeback can never trap space out from under an admitted reservation. Charged and released in lockstep with - the inodes' outstanding_extents. */ + the inodes' outstanding_extents. + On a stripe_meta METADATA space_info + it is instead set at each commit scan + to two transactions' worth of whole + stripe claims: every group can claim + a fresh stripe per transaction for a + tree block admitted in bytes. */ + u64 bytes_stripe_reserve; /* stripe_meta: whole stripes held back + from every metadata reservation except + the relocation task's, so that any one + metadata group's live tree blocks can + be moved into whole stripes elsewhere. + COW frees trap rather than free + stripes, so without it the space_info + settles half used, half trapped, with + nothing left for reclaim to move + into. Counted in + btrfs_space_info_used(); recomputed + at each commit scan. */ u64 max_extent_size; /* This will hold the maximum extent size of the space info if we had an ENOSPC in the @@ -271,6 +289,8 @@ struct reserve_ticket { u64 bytes; int error; bool steal; + /* Made by the relocation task: may draw on bytes_stripe_reserve. */ + bool reloc; struct list_head list; wait_queue_head_t wait; }; @@ -335,7 +355,7 @@ int btrfs_reserve_metadata_bytes(struct btrfs_fs_info *fs_info, void btrfs_try_granting_tickets(struct btrfs_space_info *space_info); int btrfs_can_overcommit(struct btrfs_fs_info *fs_info, const struct btrfs_space_info *space_info, u64 bytes, - enum btrfs_reserve_flush_enum flush); + enum btrfs_reserve_flush_enum flush, bool reloc); static inline void btrfs_space_info_free_bytes_may_use( struct btrfs_space_info *space_info, diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index bd96e81fa5edd..a769aeb26c8ee 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -931,6 +931,7 @@ SPACE_INFO_ATTR(bytes_stripe_unusable); SPACE_INFO_ATTR(bytes_stripe_claimable); SPACE_INFO_ATTR(bytes_stripe_open); SPACE_INFO_ATTR(bytes_stripe_margin); +SPACE_INFO_ATTR(bytes_stripe_reserve); SPACE_INFO_ATTR(disk_used); SPACE_INFO_ATTR(disk_total); SPACE_INFO_ATTR(reclaim_count); @@ -1063,6 +1064,7 @@ static struct attribute *space_info_attrs[] = { BTRFS_ATTR_PTR(space_info, bytes_stripe_claimable), BTRFS_ATTR_PTR(space_info, bytes_stripe_open), BTRFS_ATTR_PTR(space_info, bytes_stripe_margin), + BTRFS_ATTR_PTR(space_info, bytes_stripe_reserve), BTRFS_ATTR_PTR(space_info, disk_used), BTRFS_ATTR_PTR(space_info, disk_total), BTRFS_ATTR_PTR(space_info, bg_reclaim_threshold),