]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_meta: hold back whole stripes so tree blocks always land and trapped...
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 6 Sep 2026 02:58:50 +0000 (22:58 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:27 +0000 (17:36 -0400)
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
fs/btrfs/block-group.c
fs/btrfs/fs.h
fs/btrfs/relocation.c
fs/btrfs/space-info.c
fs/btrfs/space-info.h
fs/btrfs/sysfs.c

index e91fb98c324421d8cfcbd178b901bd9697002abf..10997f2dc0a1507818824de464ff1be31b3cba70 100644 (file)
@@ -2009,8 +2009,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;
@@ -2637,6 +2650,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;
 
                /*
@@ -2681,7 +2697,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);
@@ -2690,6 +2709,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
@@ -3899,7 +3952,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(sinfo, num_bytes - trapped,
-                                        BTRFS_RESERVE_NO_FLUSH))
+                                        BTRFS_RESERVE_NO_FLUSH, false))
                        ret = 0;
        }
 
index 04a36349e6cd513640537a3f7a6c3d68e6584575..f4c3226e10bc5607ab47614b1a3088c8cd615e86 100644 (file)
@@ -786,6 +786,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;
index a442d5858961007b1d441991d97f35d79db4880a..a829987dbb415815b244758d3287ed08806b5cd0 100644 (file)
@@ -3551,6 +3551,7 @@ static void set_reloc_control(struct reloc_control *rc)
        spin_lock(&fs_info->reloc_ctl_lock);
        fs_info->reloc_ctl = rc;
        spin_unlock(&fs_info->reloc_ctl_lock);
+       WRITE_ONCE(fs_info->reloc_task, current);
        mutex_unlock(&fs_info->reloc_mutex);
 }
 
@@ -3562,6 +3563,7 @@ static void unset_reloc_control(struct reloc_control *rc)
        spin_lock(&fs_info->reloc_ctl_lock);
        fs_info->reloc_ctl = NULL;
        spin_unlock(&fs_info->reloc_ctl_lock);
+       WRITE_ONCE(fs_info->reloc_task, NULL);
        mutex_unlock(&fs_info->reloc_mutex);
 }
 
index bb2b2934670bf893da9099d7c57f72a5fb65009b..54e433ee651f9d92723cb8ab16f08c629d7f93b5 100644 (file)
@@ -187,6 +187,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;
        spinlock_t lock;
@@ -614,7 +616,7 @@ static inline bool can_overcommit(const struct btrfs_space_info *space_info,
 }
 
 bool btrfs_can_overcommit(const struct btrfs_space_info *space_info, u64 bytes,
-                         enum btrfs_reserve_flush_enum flush)
+                         enum btrfs_reserve_flush_enum flush, bool reloc)
 {
        u64 used;
 
@@ -623,6 +625,13 @@ bool btrfs_can_overcommit(const struct btrfs_space_info *space_info, u64 bytes,
                return false;
 
        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);
 
        return check_can_overcommit(space_info, used, bytes, flush);
 }
@@ -730,12 +739,15 @@ again:
                u64 used_after;
 
                ticket = list_first_entry(head, struct reserve_ticket, list);
+               /* Relocation's tickets may draw on the stripe_meta reserve. */
+               if (ticket->reloc)
+                       used -= min(used, space_info->bytes_stripe_reserve);
                used_after = used + ticket->bytes;
 
                /* Check and see if our ticket can be satisfied now. */
                if ((used_after <= space_info->total_bytes ||
                     can_overcommit(space_info, used, ticket->bytes, flush)) &&
-                   stripe_claimable_admit(space_info, ticket->bytes, false)) {
+                   stripe_claimable_admit(space_info, ticket->bytes, ticket->reloc)) {
                        btrfs_space_info_update_bytes_may_use(space_info, ticket->bytes);
                        remove_ticket(space_info, ticket, 0);
                        space_info->tickets_id++;
@@ -784,13 +796,13 @@ static void __btrfs_dump_space_info(const struct btrfs_space_info *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_space_info *info, u64 bytes,
@@ -1933,6 +1945,7 @@ static int reserve_bytes(struct btrfs_space_info *space_info, u64 orig_bytes,
        u64 used;
        int ret = -ENOSPC;
        bool pending_tickets;
+       const bool reloc = (READ_ONCE(fs_info->reloc_task) == current);
 
        ASSERT(orig_bytes, "orig_bytes=%llu", orig_bytes);
        /*
@@ -1972,6 +1985,8 @@ static int reserve_bytes(struct btrfs_space_info *space_info, u64 orig_bytes,
         * 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) ||
             can_overcommit(space_info, used, orig_bytes, flush)) &&
@@ -2008,6 +2023,7 @@ static int reserve_bytes(struct btrfs_space_info *space_info, u64 orig_bytes,
                init_waitqueue_head(&ticket.wait);
                spin_lock_init(&ticket.lock);
                ticket.steal = can_steal(flush);
+               ticket.reloc = reloc;
                if (trace_btrfs_reserve_ticket_enabled())
                        start_ns = ktime_get_ns();
 
index 702222bb54ae4e5faca7734599db4c4b741c04d1..e9a984a7af34e4e1e6abbcc113b0e2dfe85529fc 100644 (file)
@@ -192,7 +192,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
@@ -338,6 +356,7 @@ static inline u64 btrfs_space_info_used(const struct btrfs_space_info *s_info,
                s_info->bytes_zone_unusable +
                s_info->bytes_stripe_unusable + s_info->bytes_stripe_open +
                s_info->bytes_stripe_margin +
+               s_info->bytes_stripe_reserve +
                (may_use_included ? s_info->bytes_may_use : 0);
 }
 
@@ -359,7 +378,7 @@ int btrfs_reserve_metadata_bytes(struct btrfs_space_info *space_info,
                                 enum btrfs_reserve_flush_enum flush);
 void btrfs_try_granting_tickets(struct btrfs_space_info *space_info);
 bool btrfs_can_overcommit(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,
index 458700c9c855064544b97e3488d7c38ebd58d1c2..cf3d0049e5a3eda4c5d5c8a88d14eaaca018ac4c 100644 (file)
@@ -821,6 +821,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);
@@ -953,6 +954,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),