Re: [PATCH 1/4] btrfs: zoned: flush active metadata block group at btree_writepages() start
Boris Burkov <[email protected]> Wed, 22 Jul 2026 10:08:35 -0700
| Newsgroups | org.kernel.vger.linux-btrfs |
|---|---|
| Message-ID | <[email protected]> |
On Wed, Jul 22, 2026 at 01:30:13PM +0200, Johannes Thumshirn wrote:
> On a zoned filesystem btree_writepages() writes metadata in ascending
wording nit: isn't this true on non-zoned too? Maybe something like:
btree_writepages() writes metadata in ascending logical order. On a
zoned filsystem, which keeps a single .... <something terrible happens>
> logical address order and keeps a single active metadata/system block group,
> pivoting it via btrfs_check_meta_write_pointer() -> check_bg_is_active() as
> writeback moves between block groups.
>
> If the active block group is at a higher address than another block group
> that also has dirty metadata, the walk reaches the lower one first and the
> pivot must finish the active block group. It cannot finish one that still has
> unsent IO, and refuses to wait for it during WB_SYNC_ALL commit writeback, so
Can you add some more detail on why it refuses? Or why the pivot can't
be the one to do these submissions? I think that would help fully
motivate this change.
> btrfs_check_meta_write_pointer() returns -EAGAIN and the transaction is
> aborted, forcing the filesystem read-only. This happens intermittently under
> metadata-heavy relocation (e.g. fstests btrfs/187).
>
> Flush the active metadata and system block groups at the start of
> btree_writepages(), under the fs_info->zoned_meta_io_lock it already holds, so
> they have no unsent IO and the later pivot can finish them.
>
> Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time")
> Assisted-by: LLM (debugging, commit message)
> Signed-off-by: Johannes Thumshirn <[email protected]>
> ---
> fs/btrfs/extent_io.c | 62 ++++++++++++++++++++++++++++++++++++++++++++
> 1 file changed, 62 insertions(+)
>
> diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c
> index 97bd18d515af..bac3edabe7c9 100644
> --- a/fs/btrfs/extent_io.c
> +++ b/fs/btrfs/extent_io.c
> @@ -2383,6 +2383,52 @@ void btrfs_btree_wait_writeback_range(struct btrfs_fs_info *fs_info, u64 start,
> }
> }
>
> +/* Write out the dirty metadata extent buffers of a single block group. */
This comment is quite generic, and not zoned specific, but the function
name is much more zoned focused (and we assert zoned)
I am also not a huge fan of this code mostly duplicating the loop in
btree_writepages while just leaving that spot open coded. Is that an
intentional decision because of important distinctions between the two?
Is there a variant we could lift to "write a range" and make
flush_active_meta_bg() and btree_writepages share it? (like the while
loop basically? or maybe the per-eb bit?)
note the missing call to btrfs_schedule_zone_finish_bg() I noted inline
as a concrete reason it's helpful to not duplicate.
> +static void flush_active_meta_bg(struct address_space *mapping,
> + struct writeback_control *wbc,
> + struct btrfs_eb_write_context *ctx,
> + struct btrfs_block_group *bg)
> +{
> + struct btrfs_fs_info *fs_info = inode_to_fs_info(mapping->host);
> + unsigned long index = bg->start >> fs_info->nodesize_bits;
> + unsigned long end = (btrfs_block_group_end(bg) - 1) >> fs_info->nodesize_bits;
> + struct eb_batch batch;
> + unsigned int nr_ebs;
> +
> + ASSERT(btrfs_is_zoned(fs_info));
> + lockdep_assert_held(&fs_info->zoned_meta_io_lock);
> +
> + eb_batch_init(&batch);
> + while (index <= end &&
> + (nr_ebs = buffer_tree_get_ebs_tag(fs_info, &index, end,
> + PAGECACHE_TAG_DIRTY, &batch))) {
> + struct extent_buffer *eb;
> +
> + while ((eb = eb_batch_next(&batch)) != NULL) {
> + ctx->eb = eb;
> +
> + /*
> + * Best effort: if the eb is not writable at the write
> + * pointer (e.g. a hole), stop flushing this bg and let
> + * the main walk deal with it.
> + */
> + if (btrfs_check_meta_write_pointer(eb->fs_info, ctx)) {
> + eb_batch_release(&batch);
> + return;
> + }
> +
> + if (!lock_extent_buffer_for_io(eb, wbc))
> + continue;
> +
> + if (ctx->zoned_bg)
the btree_writepages code also calls btrfs_schedule_zone_finish_bg()
here, is that intentionally omitted?
> + ctx->zoned_bg->meta_write_pointer += eb->len;
> + write_one_eb(eb, wbc);
> + }
> + eb_batch_release(&batch);
> + cond_resched();
> + }
> +}
> +
> int btree_writepages(struct address_space *mapping, struct writeback_control *wbc)
> {
> struct btrfs_eb_write_context ctx = { .wbc = wbc };
> @@ -2418,6 +2464,22 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb
> else
> tag = PAGECACHE_TAG_DIRTY;
> btrfs_zoned_meta_io_lock(fs_info);
> +
> + /*
> + * On a zoned filesystem, flush the currently active metadata/system
> + * block group(s) first, under this same lock, so the ascending-address
> + * walk below can pivot the active block group instead of aborting the
> + * commit with -EAGAIN.
> + */
> + if (btrfs_is_zoned(fs_info) && wbc->sync_mode == WB_SYNC_ALL &&
> + !wbc->for_sync) {
> + if (fs_info->active_meta_bg)
> + flush_active_meta_bg(mapping, wbc, &ctx,
> + fs_info->active_meta_bg);
> + if (fs_info->active_system_bg)
> + flush_active_meta_bg(mapping, wbc, &ctx,
> + fs_info->active_system_bg);
> + }
I think this placement makes sense, but I also wouldn't mind just having
a more generic "eb ordering" concept that zoned could use to plug the
active ebs to the front of the list then fallback to the normal order,
or something. Not a deal breaker for me, just food for thought.
I think fewer "tricks" for zoned writeback and more "it falls out
correctly from a generic form" would make it easier to maintain in the
long run. Obviously there are real, important differences so this kind
of thing is never free...
Thanks,
Boris
> retry:
> if (wbc->sync_mode == WB_SYNC_ALL)
> buffer_tree_tag_for_writeback(fs_info, index, end);
> --
> 2.54.0
>