From 3e726abca5e4b27b016e367c22b0ed36e533d13d Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Tue, 11 Aug 2026 02:53:38 -0400 Subject: [PATCH] btrfs: raid56: adaptive park deadline from the parked-rbio backlog Full-stripe batching parks a sub-stripe write's rbio for a deadline so the writes that fill the rest of its stripe can merge into it, turning a read-modify-write into one full-stripe write. The deadline is a fixed per-filesystem value. It is long enough to catch those merges, but at a small-file fill to ENOSPC -- where a run's stripes never fill because each file's neighbours land in the next file's stripe rather than this one -- every partial write waits the deadline out, and then its 10x stuck cap, on an arrival that never comes before padding to a full stripe and going down. Lowering the deadline globally fixes that fill but throws away the merges the deadline exists to catch on ordinary moderate writes. Feed the deadline back from the parking machinery's own backlog instead of guessing at the workload. Track stripe_parked_now, the number of currently-parked rbios, incremented as a partial write parks and decremented as it unparks. When it runs ahead of the stripe_park_congestion knob (0 disables it, the default), a new async partial write takes the short sync deadline through the existing sync-park path rather than the full one. This is negative feedback: the shorter deadline drains the backlog, so once it falls back below the knob later writes regain the full deadline and full- stripe batching. No space-state estimate is needed -- a small-file flood builds the backlog directly, while a large sequential fill (already full stripes, never parked) and a moderate steady stream do not. Measured on an eight-device raid5. A small-file balance-reclaim soak that times out at the default deadline completes at a congestion of 4; on a throttled moderate stream, where the backlog stays low, a congestion of 32 never trips, keeps the full deadline, and merges three times as much with 38% fewer RMW reads as the same filesystem under a flat 3ms deadline. The gauge and a congestion_short counter join the stripe_park_stats sysfs file so the knob can be set from measurement. Off by default, and no data-path change: a shortened park still pads or reads exactly as it would have, only sooner. Assisted-by: Claude:claude-opus-4-8 --- fs/btrfs/fs.h | 5 +++++ fs/btrfs/raid56.c | 25 +++++++++++++++++++++++++ fs/btrfs/sysfs.c | 10 ++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 9216300e2f4fc..985dc2c6b3e2a 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -827,6 +827,7 @@ struct btrfs_fs_info { atomic64_t meta_rmw_cur; atomic64_t meta_rmw_old; atomic64_t meta_rmw_free; + atomic64_t congestion_short; } stripe_park_stats; /* BTRFS_STRIPE_RMW_* masks; see btrfs_stripe_allow_rmw(). */ u32 stripe_rmw_opt; @@ -839,6 +840,10 @@ struct btrfs_fs_info { */ u32 stripe_park_timeout_ms; u32 stripe_park_sync_timeout_ms; + /* Parked-rbio backlog above which parks go short; 0 = off. */ + u32 stripe_park_congestion; + /* Live count of currently-parked rbios (the backlog gauge). */ + atomic_t stripe_parked_now; /* Private scrub information */ struct mutex scrub_lock; diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 60d741bdf01af..59d5d04c02637 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -765,6 +765,7 @@ static void rbio_unpark_locked(struct btrfs_raid_bio *rbio) { list_del_init(&rbio->parked_node); clear_bit(RBIO_PARKED_BIT, &rbio->flags); + atomic_dec(&rbio->bioc->fs_info->stripe_parked_now); } /* Does any bio gathered in @rbio carry a sync hint (a blocked waiter)? */ @@ -853,6 +854,29 @@ static bool rbio_try_park(struct btrfs_raid_bio *rbio) timeout_ms == sync_timeout_ms) return false; + /* + * Park-congestion feedback. stripe_parked_now is the live count of + * parked rbios. When it runs ahead of the stripe_park_congestion knob + * (0 = off) the park machinery is not clearing its backlog -- merges + * are not arriving fast enough, the regime a small-file / partial-write + * fill produces -- so collapse this write's deadline to the short sync + * value (the sync-park path below flushes at it, skipping the stuck + * cap). Negative feedback: the shorter deadline drains the backlog, so + * once it falls back under the knob new writes regain the full deadline + * and full-stripe batching. No space-state assumption. + */ + { + unsigned int congestion = + READ_ONCE(fs_info->stripe_park_congestion); + + if (timeout_ms != sync_timeout_ms && sync_timeout_ms != 0 && + congestion && + atomic_read(&fs_info->stripe_parked_now) > (int)congestion) { + timeout_ms = sync_timeout_ms; + atomic64_inc(&fs_info->stripe_park_stats.congestion_short); + } + } + spin_lock(&table->parked_lock); set_bit(RBIO_PARKED_BIT, &rbio->flags); set_bit(RBIO_WAS_PARKED_BIT, &rbio->flags); @@ -862,6 +886,7 @@ static bool rbio_try_park(struct btrfs_raid_bio *rbio) rbio->park_stuck_deadline = jiffies + msecs_to_jiffies(timeout_ms * 10); list_add_tail(&rbio->parked_node, &table->parked); + atomic_inc(&fs_info->stripe_parked_now); spin_unlock(&table->parked_lock); atomic64_inc(&fs_info->stripe_park_stats.parked); queue_delayed_work(system_percpu_wq, &table->parked_work, diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index bf38732b4c650..bd96e81fa5edd 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1248,7 +1248,9 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj, "meta_rmw %lld\n" "meta_rmw_cur %lld\n" "meta_rmw_old %lld\n" - "meta_rmw_free %lld\n", + "meta_rmw_free %lld\n" + "congestion_short %lld\n" + "parked_now %d\n", atomic64_read(&fs_info->stripe_park_stats.parked), atomic64_read(&fs_info->stripe_park_stats.merged_bytes), atomic64_read(&fs_info->stripe_park_stats.filled), @@ -1266,7 +1268,9 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj, atomic64_read(&fs_info->stripe_park_stats.meta_rmw), atomic64_read(&fs_info->stripe_park_stats.meta_rmw_cur), atomic64_read(&fs_info->stripe_park_stats.meta_rmw_old), - atomic64_read(&fs_info->stripe_park_stats.meta_rmw_free)); + atomic64_read(&fs_info->stripe_park_stats.meta_rmw_free), + atomic64_read(&fs_info->stripe_park_stats.congestion_short), + atomic_read(&fs_info->stripe_parked_now)); } BTRFS_ATTR(, stripe_park_stats, btrfs_stripe_park_stats_show); @@ -1303,6 +1307,7 @@ BTRFS_ATTR_RW(, _name, btrfs_##_name##_show, btrfs_##_name##_store) BTRFS_PARK_TIMEOUT_ATTR(stripe_park_timeout_ms, stripe_park_timeout_ms); BTRFS_PARK_TIMEOUT_ATTR(stripe_park_sync_timeout_ms, stripe_park_sync_timeout_ms); +BTRFS_PARK_TIMEOUT_ATTR(stripe_park_congestion, stripe_park_congestion); @@ -1720,6 +1725,7 @@ static const struct attribute *btrfs_attrs[] = { BTRFS_ATTR_PTR(, stripe_park_stats), BTRFS_ATTR_PTR(, stripe_park_timeout_ms), BTRFS_ATTR_PTR(, stripe_park_sync_timeout_ms), + BTRFS_ATTR_PTR(, stripe_park_congestion), BTRFS_ATTR_PTR(, temp_fsid), #ifdef CONFIG_BTRFS_EXPERIMENTAL BTRFS_ATTR_PTR(, offload_csum), -- 2.53.0