From: Johannes Thumshirn <johannes.thumshirn@wdc.com>
To: "Марк Коренберг" <socketpair@gmail.com>,
"David Sterba" <dsterba@suse.com>,
"Chris Mason" <mason@kernel.org>,
linux-btrfs@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [RFC] btrfs: zoned: reset stale zones in unallocated device space
Date: Tue, 27 Oct 2026 11:27:14 +0100 [thread overview]
Message-ID: <52925d79-4dce-4621-b9e1-fd062b47e35d@wdc.com> (raw)
In-Reply-To: <CAEmTpZGu5x9PS7vFwgc_dFH80rL3cFD4-geuuL_w8i=vFDo8AA@mail.gmail.com>
[-- Attachment #1: Type: text/plain, Size: 4849 bytes --]
On 10/27/26 9:36 AM, Марк Коренберг wrote:
> Hi,
>
> I'd like to propose a feature for zoned btrfs. I'm not planning to
> implement it myself, but I'd like to get feedback and, if the idea is
> considered reasonable, have it tracked as a TODO / project idea.
>
> Background
> ----------
> I maintain a host-managed zoned block device implemented with ublk,
> where each zone is backed by S3 objects. Storage cost is proportional
> to the amount of data actually held in zones, so zones that contain
> data but are not referenced by the filesystem are a real cost, not
> just lost capacity.
>
> Problem
> -------
> With a long commit interval (e.g. commit=300) and heavy buffered
> writes, btrfs may allocate new block groups and write a large amount
> of data into their zones before the transaction commits. If the
> filesystem then goes down uncleanly before the commit, this is fine
> from a consistency point of view: nobody called fsync(), and after
> the next mount the filesystem is consistent at the last committed
> transaction.
>
> For a virtual device like mine, "goes down uncleanly" is more likely
> the ublk server crashing or losing connectivity to S3 than an actual
> power loss, but the effect on btrfs is the same: the running
> transaction never commits.
>
> The device extents of those new block groups were never persisted, so
> after the next mount the corresponding zones belong to unallocated
> device space from btrfs' point of view, while on the device they are
> still non-empty (write pointer > zone start) and hold data.
I have a patch lying around doing this in an old stale branch. I've
attached it, but not tried rebasing it onto latest for-next. If you're
brave enough to test it I'd be happy.
> As far as I can tell, btrfs currently handles this lazily: such zones
> are reset only when the allocator picks that range for a new chunk
> (btrfs_ensure_empty_zones()). Until then they hold stale data
> indefinitely. On SMR HDDs this does not matter much, but on ZNS SSDs
> (where a reset helps the device's internal GC) and especially on
> virtual/cloud-backed zoned devices it does.
>
> (Stale data past the committed alloc_offset inside *existing* block
> groups is accounted as zone_unusable and can only be reclaimed via
> relocation; this proposal does not try to address that case.)
>
> Current workaround
> ------------------
> The only safe way I see today is offline: unmount, walk the dev tree
> to collect allocated dev extents, and issue BLKRESETZONE for every
> non-empty sequential zone not covered by a dev extent. Doing this on
> a mounted filesystem from userspace is unsafe, because it races with
> chunk allocation (a dev extent may be allocated in the running
> transaction but not yet visible in the committed dev tree).
>
> Proposal
> --------
> Let the kernel do it, since it can serialize against chunk allocation
> (chunk_mutex / device->alloc_state). The operation itself: reset
> every sequential zone that is fully contained in unallocated device
> space and whose write pointer is not at the zone start.
>
> Possible interfaces, in order of my preference:
>
> 1. Automatic background cleanup, e.g. once after mount (after the
> dev extents are loaded) and/or as part of the existing
> reclaim/discard workers, possibly behind a mount option or sysfs
> knob. This requires no administrator action and covers the
> problem completely.
>
> 2. FITRIM: on zoned filesystems, apply the reset to unallocated
> device space as part of fstrim. Many distributions already run
> fstrim periodically (fstrim.timer), so this would also work
> without extra configuration in most setups, and it fits the
> "release unused space to the device" semantics.
>
> 3. A dedicated trigger: a new ioctl (exposed as a btrfs-progs
> subcommand) or a sysfs knob under /sys/fs/btrfs/<UUID>/ that
> starts the cleanup on demand. Least convenient, but still much
> better than the current offline-only workaround.
>
> Risk
> ----
> With corrupted metadata (a dev extent missing from the dev tree),
> this could reset a zone that actually holds live data. But the
> allocator already makes exactly the same assumption today when it
> resets non-empty zones before reusing them, so this does not add a
> new class of risk; it only performs the reset earlier. If automatic
> cleanup is considered too aggressive, options 2 and 3 are explicit
> and opt-in.
>
> Questions
> ---------
> - Does this sound like something you would accept upstream?
> - Which interface would you prefer?
> - Am I missing a reason why the reset is intentionally deferred to
> allocation time?
>
> If the idea is acceptable, I'd appreciate it being added to the btrfs
> TODO list so that someone interested could pick it up.
>
> Thanks,
> Korenberg Mark
[-- Attachment #2: 0001-btrfs-zoned-reset-leaked-zones-on-read-write-mount.patch --]
[-- Type: text/x-patch, Size: 5523 bytes --]
From a512de1efcc13d997d58dc71509ae3d8171d4d5a Mon Sep 17 00:00:00 2001
From: Johannes Thumshirn <johannes.thumshirn@wdc.com>
Date: Tue, 8 Sep 2026 11:42:05 +0200
Subject: [PATCH] btrfs: zoned: reset leaked zones on read-write mount
On a zoned filesystem a newly created block group has its zones activated
and written during transaction commit writeback. If the commit is aborted
before the superblock is written, the block group's chunk item and device
extent are never persisted, but the underlying zones stay written. After
the next mount those zones are written but not referenced by any block
group and leak, which btrfs check reports as:
zoned: devid X zone Y is open/written but not referenced by any block group
This leaks hardware resources, leaving the zones backing the block-group
open.
On a read-write mount, once the block groups are loaded, walk each device's
zones and reset any non-empty sequential zone that is neither a superblock
log zone nor covered by a block group. At that point the in-memory block
groups reflect the committed on-disk state, so such a zone is known to be
leaked and can be safely reset and reused.
Signed-off-by: Johannes Thumhisn <johannes.thumshirn@wdc.com>
---
fs/btrfs/disk-io.c | 4 +++
fs/btrfs/zoned.c | 77 ++++++++++++++++++++++++++++++++++++++++++++++
fs/btrfs/zoned.h | 6 ++++
3 files changed, 87 insertions(+)
diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c
index a1d83ad9a4c0..7be090289b49 100644
--- a/fs/btrfs/disk-io.c
+++ b/fs/btrfs/disk-io.c
@@ -3075,6 +3075,10 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info)
const bool cache_opt = btrfs_test_opt(fs_info, SPACE_CACHE);
bool rebuild_free_space_tree = false;
+ ret = btrfs_reset_unreferenced_zones(fs_info);
+ if (ret)
+ return ret;
+
if (btrfs_test_opt(fs_info, CLEAR_CACHE) &&
btrfs_fs_compat_ro(fs_info, FREE_SPACE_TREE)) {
if (btrfs_fs_incompat(fs_info, EXTENT_TREE_V2))
diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c
index 08a15465a087..fef872fd4399 100644
--- a/fs/btrfs/zoned.c
+++ b/fs/btrfs/zoned.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/bitops.h>
+#include <linux/bitmap.h>
#include <linux/slab.h>
#include <linux/blkdev.h>
#include <linux/sched/mm.h>
@@ -3233,6 +3234,82 @@ int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info, u64 num
return 0;
}
+int btrfs_reset_unreferenced_zones(struct btrfs_fs_info *fs_info)
+{
+ struct btrfs_fs_devices *fs_devices = fs_info->fs_devices;
+ struct btrfs_device *device;
+ int ret = 0;
+
+ if (!btrfs_is_zoned(fs_info))
+ return 0;
+
+ mutex_lock(&fs_devices->device_list_mutex);
+ list_for_each_entry(device, &fs_devices->devices, dev_list) {
+ struct btrfs_zoned_device_info *zinfo = device->zone_info;
+ struct btrfs_block_group *bg;
+ unsigned long *covered;
+
+ if (!device->bdev || !zinfo)
+ continue;
+
+ covered = bitmap_zalloc(zinfo->nr_zones, GFP_NOFS);
+ if (!covered) {
+ ret = -ENOMEM;
+ break;
+ }
+
+ for (int mirror = 0; mirror < BTRFS_SUPER_MIRROR_MAX; mirror++) {
+ u32 sb_zno = sb_zone_number(zinfo->zone_size_shift, mirror);
+
+ if (sb_zno + BTRFS_NR_SB_LOG_ZONES > zinfo->nr_zones)
+ continue;
+ for (int i = 0; i < BTRFS_NR_SB_LOG_ZONES; i++)
+ set_bit(sb_zno + i, covered);
+ }
+
+ for (bg = btrfs_lookup_first_block_group(fs_info, 0); bg;
+ bg = btrfs_next_block_group(bg)) {
+ struct btrfs_chunk_map *map = bg->physical_map;
+
+ if (!map)
+ continue;
+ for (int i = 0; i < map->num_stripes; i++) {
+ u64 zno;
+
+ if (map->stripes[i].dev != device)
+ continue;
+ zno = map->stripes[i].physical >> zinfo->zone_size_shift;
+ if (zno < zinfo->nr_zones)
+ set_bit(zno, covered);
+ }
+ }
+
+ for (u32 zno = 0; zno < zinfo->nr_zones; zno++) {
+ u64 physical = (u64)zno << zinfo->zone_size_shift;
+ u64 reset_bytes;
+
+ if (!btrfs_dev_is_sequential(device, physical) ||
+ btrfs_dev_is_empty_zone(device, physical) ||
+ test_bit(zno, covered))
+ continue;
+
+ ret = btrfs_reset_device_zone(device, physical,
+ zinfo->zone_size, &reset_bytes);
+ if (ret) {
+ btrfs_warn(fs_info,
+ "zoned: failed to reset leaked zone %u on device %llu: %d",
+ zno, device->devid, ret);
+ ret = 0;
+ }
+ }
+
+ bitmap_free(covered);
+ }
+ mutex_unlock(&fs_devices->device_list_mutex);
+
+ return ret;
+}
+
void btrfs_show_zoned_stats(struct btrfs_fs_info *fs_info, struct seq_file *seq)
{
struct btrfs_block_group *bg;
diff --git a/fs/btrfs/zoned.h b/fs/btrfs/zoned.h
index 8e21a836f858..2dfa6337ae2f 100644
--- a/fs/btrfs/zoned.h
+++ b/fs/btrfs/zoned.h
@@ -97,6 +97,7 @@ int btrfs_zone_finish_one_bg(struct btrfs_fs_info *fs_info);
int btrfs_zoned_activate_one_bg(struct btrfs_space_info *space_info, bool do_finish);
void btrfs_check_active_zone_reservation(struct btrfs_fs_info *fs_info);
int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info, u64 num_bytes);
+int btrfs_reset_unreferenced_zones(struct btrfs_fs_info *fs_info);
void btrfs_show_zoned_stats(struct btrfs_fs_info *fs_info, struct seq_file *seq);
#ifdef CONFIG_BTRFS_FS_RUN_SANITY_TESTS
@@ -287,6 +288,11 @@ static inline int btrfs_reset_unused_block_groups(struct btrfs_space_info *space
return 0;
}
+static inline int btrfs_reset_unreferenced_zones(struct btrfs_fs_info *fs_info)
+{
+ return 0;
+}
+
static inline int btrfs_show_zoned_stats(struct btrfs_fs_info *fs_info, struct seq_file *seq)
{
return 0;
--
2.55.0
next prev parent reply other threads:[~2026-10-27 10:27 UTC|newest]
Thread overview: 4+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 8:36 Марк Коренберг
2026-10-27 10:27 ` Johannes Thumshirn [this message]
2026-10-27 14:48 ` Марк Коренберг
2026-10-27 15:05 ` Johannes Thumshirn
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=52925d79-4dce-4621-b9e1-fd062b47e35d@wdc.com \
--to=johannes.thumshirn@wdc.com \
--cc=dsterba@suse.com \
--cc=linux-btrfs@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=mason@kernel.org \
--cc=socketpair@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®