diff options
| author | Boris Burkov <boris@bur.io> | 2026-09-28 17:20:33 -0700 |
|---|---|---|
| committer | David Sterba <dsterba@suse.com> | 2026-10-12 19:16:23 +0200 |
| commit | cd5e2d2f2ea2b2c644ac8a01b50a8f22bd23610f (patch) | |
| tree | 1ce4d835f58c3ed7814e092bc606200271bd9587 | |
| parent | d658eb2bda4c67836cf49a88c082b3719258f814 (diff) | |
btrfs: allocate additional SYSTEM space earlier
Currently, reserve_chunk_space() allocates a new system chunk when the
space left is smaller than the reservation for a single chunk tree
update.
Therefore, a filesystem in single metadata mode on a very large block
device can accumulate tens of thousands (TB) of chunks while never
allocating more than the original 4MiB SYSTEM block group created by
mkfs. If it then fills up (with large fallocates for example) and then
that data is freed, en-masse, we end up trying to delete thousands of
block groups in a single transaction in btrfs_delete_unused_bgs().
This process touches most of the nodes and leaves of the chunk tree,
essentially trying to allocate roughly double the current usage of the
chunk tree for cow. When this runs into needing a fresh system chunk,
the fs is full of the empty bgs and we abort with ENOSPC in
btrfs_remove_chunk() like:
BTRFS error (device loop0 state A): Transaction 30733 aborted (-ENOSPC)
space_info SYSTEM (sub-group id 0) has 0 free, is not full
space_info total=4194304, used=3276800, pinned=0, reserved=917504, may_use=0, readonly=0 zone_unusable=0
BTRFS: error (device loop0 state A) in btrfs_remove_chunk:3643: errno=-28 No space left
This was seen on a 30TiB production filesystem with 10TiB of unused
block groups and reproduces on a 30TiB sparse loop device: mkfs with
-m single, fallocate 1GiB files until ENOSPC, delete two of every three
adjacent files, sync.
To fix this, we should allocate the (relatively tiny) system chunks a
little bit more eagerly. If we do it when it is half full, we ensure we
can delete all of the block groups in one go. To fill up half of a 4MiB
system bg requires thousands of block_groups so this should only affect
very large fileystems.
Assisted-by: LLM
Reviewed-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: Boris Burkov <boris@bur.io>
Signed-off-by: David Sterba <dsterba@suse.com>
| -rw-r--r-- | fs/btrfs/block-group.c | 42 |
1 files changed, 33 insertions, 9 deletions
diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 2eb09c9901c9..0a41b16ed270 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -1575,6 +1575,19 @@ static bool btrfs_link_bg_list(struct btrfs_block_group *bg, struct list_head *l } /* + * Compute a rough bound on the bytes needed to modify every leaf in the chunk + * tree. Normally, we could just allocate a new system chunk then, but that can + * fail if there is no free space for a new dev extent. This can happen when + * we fill up a large filesystem then rapidly delete a large portion of it. + */ +static u64 system_space_target(struct btrfs_space_info *sinfo) +{ + lockdep_assert_held(&sinfo->lock); + + return btrfs_space_info_used(sinfo, true) + sinfo->bytes_used; +} + +/* * Process the unused_bgs list and remove any that don't have any allocated * space inside of them. */ @@ -1661,6 +1674,9 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) if (btrfs_is_block_group_used(block_group) || (block_group->ro && !(block_group->flags & BTRFS_BLOCK_GROUP_REMAPPED)) || list_is_singular(&block_group->list) || + ((block_group->flags & BTRFS_BLOCK_GROUP_SYSTEM) && + space_info->total_bytes - block_group->length < + system_space_target(space_info)) || test_bit(BLOCK_GROUP_FLAG_FULLY_REMAPPED, &block_group->runtime_flags)) { /* * We want to bail if we made new allocations or have @@ -1674,6 +1690,10 @@ void btrfs_delete_unused_bgs(struct btrfs_fs_info *fs_info) * next block group of this type would be created with a * "single" profile (even if we're in a raid fs) because * fs_info->avail_*_alloc_bits would be 0. + * + * Also bail out if this is a system block group that + * system_space_target() relies on to ensure head room for + * mass deletion. */ trace_btrfs_skip_unused_block_group(block_group); spin_unlock(&block_group->lock); @@ -4509,6 +4529,7 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, struct btrfs_fs_info *fs_info = trans->fs_info; struct btrfs_space_info *info; u64 left; + bool want_system_chunk, need_system_chunk; int ret = 0; /* @@ -4520,15 +4541,17 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, info = btrfs_find_space_info(fs_info, BTRFS_BLOCK_GROUP_SYSTEM); spin_lock(&info->lock); left = info->total_bytes - btrfs_space_info_used(info, true); + want_system_chunk = info->total_bytes < system_space_target(info); + need_system_chunk = left < bytes; spin_unlock(&info->lock); - if (left < bytes && btrfs_test_opt(fs_info, ENOSPC_DEBUG)) { + if (need_system_chunk && btrfs_test_opt(fs_info, ENOSPC_DEBUG)) { btrfs_info(fs_info, "left=%llu, need=%llu, flags=%llu", left, bytes, type); btrfs_dump_space_info(info, 0, false); } - if (left < bytes) { + if (want_system_chunk || need_system_chunk) { u64 flags = btrfs_system_alloc_profile(fs_info); struct btrfs_block_group *bg; struct btrfs_space_info *space_info; @@ -4572,13 +4595,14 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, } } - if (!ret) { - ret = btrfs_block_rsv_add(fs_info, - &fs_info->chunk_block_rsv, - bytes, BTRFS_RESERVE_NO_FLUSH); - if (!ret) - trans->chunk_bytes_reserved += bytes; - } + if (ret && need_system_chunk) + return; + + ret = btrfs_block_rsv_add(fs_info, + &fs_info->chunk_block_rsv, + bytes, BTRFS_RESERVE_NO_FLUSH); + if (!ret) + trans->chunk_bytes_reserved += bytes; } /* |
