]> 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>
Wed, 16 Sep 2026 21:40:05 +0000 (17:40 -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 676fdd238759fdfef7dbdb13ffebae45fa41432c..a018e2eae154b3fe45618986e259f4a9b106ddef 100644 (file)
@@ -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;
        }
 
index b10c0321d63d73ec437a315f730bbb4ab498919d..10b6624895bf8634d894d8dc3b5309f817e880ae 100644 (file)
@@ -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;
index 403b6632002c534b97b27f558da2dd2871f38214..5de5103f98b51eb21ce967cbc910f1d51226c11f 100644 (file)
@@ -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);
 }
 
index 97f0e0c4372b076e2990e5f5bf93722ab7161e83..f7fa2b6dd80e06b90d6c2bf7438046a2667c530f 100644 (file)
@@ -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();
 
index 989b1f6ad5b0f0edf79db4b596e09eca03938bfb..1bdab3a6dfd619054deb0795c458caa6ada9aff3 100644 (file)
@@ -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,
index bd96e81fa5eddcbc8a3e0de8419f5c6c3b22f073..a769aeb26c8ee59b6c631246c9df244c8de2f6e4 100644 (file)
@@ -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),