From 8ed55c06c15b8fdced661293d924cc81d0355de7 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Wed, 17 Dec 2025 14:01:08 +0100 Subject: [PATCH 01/19] GITHUB: btrfs: add build workflow Signed-off-by: Kai Krakow --- .github/workflows/makefile.yml | 34 ++++++++++++++++++++++++++++++++++ .gitignore | 3 +++ 2 files changed, 37 insertions(+) create mode 100644 .github/workflows/makefile.yml diff --git a/.github/workflows/makefile.yml b/.github/workflows/makefile.yml new file mode 100644 index 00000000000000..e1a5d4bd88c467 --- /dev/null +++ b/.github/workflows/makefile.yml @@ -0,0 +1,34 @@ +name: Makefile CI + +on: + push: + branches: + - 'rebase-*/btrfs-patches' + pull_request: + branches: + - 'rebase-*/btrfs-patches' + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Configure minimal kernel + run: make tinyconfig + + - name: Configure btrfs + run: | + echo "CONFIG_BLOCK=y" >>.config + echo "CONFIG_BTRFS_FS=y" >>.config + echo "CONFIG_BTRFS_FS_POSIX_ACL=y" >>.config + echo "CONFIG_BTRFS_ALLOCATOR_HINTS=y" >>.config + echo "CONFIG_BTRFS_PER_DEVICE_IO_STATS=y" >>.config + echo "CONFIG_BTRFS_READ_POLICIES=y" >>.config + make oldconfig + + - name: Compile kernel + run: make -j$(nproc) all diff --git a/.gitignore b/.gitignore index 86a1ba0d903539..d09ac6e2f574a3 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,6 @@ sphinx_*/ # Rust analyzer configuration /rust-project.json + +# Allow Github workflows +!/.github From ae18500974c9b7f0f2e1e4c37b1a02d48f0a87b9 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Mon, 23 Jun 2025 13:56:36 +0200 Subject: [PATCH 02/19] btrfs: avoid RCU stall during backref list construction The following kernel message may be logged if `add_inline_refs()` or `add_keyed_refs()` block for too long: > kernel: rcu: INFO: rcu_sched self-detected stall on CPU > kernel: rcu: 10-....: (2100 ticks this GP) idle=0494/1/0x4000000000000000 softirq=164826140/164826187 fqs=1052 > kernel: rcu: (t=2100 jiffies g=358306033 q=2241752 ncpus=16) > kernel: CPU: 10 UID: 0 PID: 1524681 Comm: map_0x178e45670 Not tainted 6.12.21-gentoo #1 > kernel: Hardware name: Red Hat KVM, BIOS 0.5.1 01/01/2011 > kernel: RIP: 0010:btrfs_get_64+0x65/0x110 > kernel: Code: d3 ed 48 8b 4f 70 48 8b 31 83 e6 40 74 11 0f b6 49 40 41 bc 00 10 00 00 49 d3 e4 49 83 ec 01 4a 8b 5c ed 70 49 21 d4 45 89 c9 <48> 2b 1d 7c 99 09 01 49 01 c1 8b 55 08 49 8d 49 08 44 8b 75 0c 48 > kernel: RSP: 0018:ffffbb7ad531bba0 EFLAGS: 00000202 > kernel: RAX: 0000000000001f15 RBX: fffff437ea382200 RCX: fffff437cb891200 > kernel: RDX: 000001922b68df2a RSI: 0000000000000000 RDI: ffffa434c3e66d20 > kernel: RBP: ffffa434c3e66d20 R08: 000001922b68c000 R09: 0000000000000015 > kernel: R10: 6c0000000000000a R11: 0000000009fe7000 R12: 0000000000000f2a > kernel: R13: 0000000000000001 R14: ffffa43192e6d230 R15: ffffa43160c4c800 > kernel: FS: 000055d07085e6c0(0000) GS:ffffa4452bc80000(0000) knlGS:0000000000000000 > kernel: CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 > kernel: CR2: 00007fff204ecfc0 CR3: 0000000121a0b000 CR4: 00000000001506f0 > kernel: DR0: 0000000000000000 DR1: 0000000000000000 DR2: 0000000000000000 > kernel: DR3: 0000000000000000 DR6: 00000000fffe0ff0 DR7: 0000000000000400 > kernel: Call Trace: > kernel: > kernel: ? rcu_dump_cpu_stacks+0xd3/0x100 > kernel: ? rcu_sched_clock_irq+0x4ff/0x920 > kernel: ? update_process_times+0x6c/0xa0 > kernel: ? tick_nohz_handler+0x82/0x110 > kernel: ? tick_do_update_jiffies64+0xd0/0xd0 > kernel: ? __hrtimer_run_queues+0x10b/0x190 > kernel: ? hrtimer_interrupt+0xf1/0x200 > kernel: ? __sysvec_apic_timer_interrupt+0x44/0x50 > kernel: ? sysvec_apic_timer_interrupt+0x60/0x80 > kernel: > kernel: > kernel: ? asm_sysvec_apic_timer_interrupt+0x16/0x20 > kernel: ? btrfs_get_64+0x65/0x110 > kernel: find_parent_nodes+0x1b84/0x1dc0 > kernel: btrfs_find_all_leafs+0x31/0xd0 > kernel: ? queued_write_lock_slowpath+0x30/0x70 > kernel: iterate_extent_inodes+0x6f/0x370 > kernel: ? update_share_count+0x60/0x60 > kernel: ? extent_from_logical+0x139/0x190 > kernel: ? release_extent_buffer+0x96/0xb0 > kernel: iterate_inodes_from_logical+0xaa/0xd0 > kernel: btrfs_ioctl_logical_to_ino+0xaa/0x150 > kernel: __x64_sys_ioctl+0x84/0xc0 > kernel: do_syscall_64+0x47/0x100 > kernel: entry_SYSCALL_64_after_hwframe+0x4b/0x53 > kernel: RIP: 0033:0x55d07617eaaf > kernel: Code: 00 48 89 44 24 18 31 c0 48 8d 44 24 60 c7 04 24 10 00 00 00 48 89 44 24 08 48 8d 44 24 20 48 89 44 24 10 b8 10 00 00 00 0f 05 <89> c2 3d 00 f0 ff ff 77 18 48 8b 44 24 18 64 48 2b 04 25 28 00 00 > kernel: RSP: 002b:000055d07085bc20 EFLAGS: 00000246 ORIG_RAX: 0000000000000010 > kernel: RAX: ffffffffffffffda RBX: 000055d0402f8550 RCX: 000055d07617eaaf > kernel: RDX: 000055d07085bca0 RSI: 00000000c038943b RDI: 0000000000000003 > kernel: RBP: 000055d07085bea0 R08: 00007fee46c84080 R09: 0000000000000000 > kernel: R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000003 > kernel: R13: 000055d07085bf80 R14: 000055d07085bf48 R15: 000055d07085c0b0 > kernel: The RCU stall could be because there's a large number of backrefs for some extents and we're spending too much time looping over them without ever yielding the cpu. Avoid the stall warning by adding `conf_resched()`. Link: https://lore.kernel.org/linux-btrfs/CAMthOuP_AE9OwiTQCrh7CK73xdTZvHsLTB1JU2WBK6cCc05JYg@mail.gmail.com/T/#md2e3504a1885c63531f8eefc70c94cff571b7a72 Suggested-by: Filipe Manana Signed-off-by: Kai Krakow --- fs/btrfs/backref.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/backref.c b/fs/btrfs/backref.c index 2ab550a1e715a7..8895a2b446eb5d 100644 --- a/fs/btrfs/backref.c +++ b/fs/btrfs/backref.c @@ -1126,6 +1126,7 @@ static int add_inline_refs(struct btrfs_backref_walk_ctx *ctx, if (ret) return ret; ptr += btrfs_extent_inline_ref_size(type); + cond_resched(); } return 0; @@ -1229,7 +1230,7 @@ static int add_keyed_refs(struct btrfs_backref_walk_ctx *ctx, } if (ret) return ret; - + cond_resched(); } return ret; From c07dc111d5cfd3dbf4eada858265d4723fc84c7f Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Fri, 10 Jul 2026 23:49:05 +0200 Subject: [PATCH 03/19] btrfs: add new Kconfig option for btrfs allocator hints Signed-off-by: Kai Krakow --- fs/btrfs/Kconfig | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/fs/btrfs/Kconfig b/fs/btrfs/Kconfig index 4438637c8900cd..1a3617725ed9cf 100644 --- a/fs/btrfs/Kconfig +++ b/fs/btrfs/Kconfig @@ -84,6 +84,45 @@ config BTRFS_ASSERT If unsure, say N. +config BTRFS_ALLOCATOR_HINTS + bool "Btrfs allocator hints" + depends on BTRFS_FS + default n + help + Enable support for allocator hints. This feature allows to select + dedicated or preferred devices for meta data vs data, or prevent + allocation from a device at all. This feature does not interact + well with free space calculation because the formula expects to + allocate space always from a device with most free space which is + not true when hints are applied. It may also create issues if a + device from the pool dies resulting in a situation where there are + still enough RAID mirror members but the allocation hints don't + allow to allocate from specific devices. + + You are advised to watch your free space closely with btrfs tools + instead of relying on df only. + + Mounting a btrfs with this feature on or off is always possible, + there are no incompatible changes to the file system. But running + without this feature may place new chunks on unwanted devices and + you may want to clean up later by balancing the affected chunks. + + Supported hint types in /sys/fs/btrfs/BTRFS-UUID/devinfo/ID/type: + + - type = 0 - allocate data chunks from this ID first (recommended + for big disks with good sequential performance, e.g. + HDDs), prefers data on this device + - type = 1 - allocate meta data chunks from this ID first + (recommended for fast and small disks with good + latency, e.g. SSD/NVMe), prefers meta data on this + device + - type = 2 - allocate only meta data chunks from this ID, no data + chunks will ever be allocated from this device + - type = 3 - allocate only data chunks from this ID, no meta data + chunks will ever be allocated from this device + + If unsure, say N. + config BTRFS_EXPERIMENTAL bool "Btrfs experimental features" depends on BTRFS_FS From d2b42627e0addfe2ac6771c3d85dd7fed5b3ece6 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:51:17 +0200 Subject: [PATCH 04/19] btrfs: add flags to give a hint to the chunk allocator Add the following flags to give a hint about which chunk should be allocated on which a disk. The following flags are created: - BTRFS_DEV_ALLOCATION_PREFERRED_DATA preferred data chunk, but metadata chunk allowed - BTRFS_DEV_ALLOCATION_PREFERRED_METADATA preferred metadata chunk, but data chunk allowed - BTRFS_DEV_ALLOCATION_METADATA_ONLY only metadata chunk allowed - BTRFS_DEV_ALLOCATION_DATA_ONLY only data chunk allowed Co-authored-by: Goffredo Baroncelli Signed-off-by: Kai Krakow --- include/uapi/linux/btrfs_tree.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h index fc29d273845d84..f004ab4341537f 100644 --- a/include/uapi/linux/btrfs_tree.h +++ b/include/uapi/linux/btrfs_tree.h @@ -578,6 +578,27 @@ struct btrfs_node { struct btrfs_key_ptr ptrs[]; } __attribute__ ((__packed__)); +/* + * This defines the chunk allocation hints for a device. It uses an ifdef guard + * in UAPI to avoid exposing it to userspace. + */ +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS +/* dev_item.type */ + +/* btrfs chunk allocation hints */ +#define BTRFS_DEV_ALLOCATION_MASK_BIT_COUNT 3 +/* preferred data chunk, but metadata chunk allowed */ +#define BTRFS_DEV_ALLOCATION_PREFERRED_DATA (0ULL) +/* preferred metadata chunk, but data chunk allowed */ +#define BTRFS_DEV_ALLOCATION_PREFERRED_METADATA (1ULL) +/* only metadata chunk allowed */ +#define BTRFS_DEV_ALLOCATION_METADATA_ONLY (2ULL) +/* only data chunk allowed */ +#define BTRFS_DEV_ALLOCATION_DATA_ONLY (3ULL) +/* 5..7 are unused values */ + +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + struct btrfs_dev_item { /* the internal btrfs device id */ __le64 devid; From 1692c5a3773e74950f8b94c3aee5c69196f090e1 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:22:50 +0200 Subject: [PATCH 05/19] btrfs: export dev_item.type in /sys/fs/btrfs//devinfo//type Co-authored-by: Goffredo Baroncelli Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 81f52c1f55ce57..9cdfda9a0c03c0 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2140,6 +2140,18 @@ static ssize_t btrfs_devinfo_error_stats_show(struct kobject *kobj, } BTRFS_ATTR(devid, error_stats, btrfs_devinfo_error_stats_show); +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS +static ssize_t btrfs_devinfo_type_show(struct kobject *kobj, + struct kobj_attribute *a, char *buf) +{ + struct btrfs_device *device = container_of(kobj, struct btrfs_device, + devid_kobj); + + return scnprintf(buf, PAGE_SIZE, "0x%016llx\n", device->type); +} +BTRFS_ATTR(devid, type, btrfs_devinfo_type_show); +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + /* * Information about one device. * @@ -2153,6 +2165,9 @@ static struct attribute *devid_attrs[] = { BTRFS_ATTR_PTR(devid, replace_target), BTRFS_ATTR_PTR(devid, scrub_speed_max), BTRFS_ATTR_PTR(devid, writeable), +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + BTRFS_ATTR_PTR(devid, type), +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ NULL }; ATTRIBUTE_GROUPS(devid); From 95d21ebdf8a8648658989bbdfcd51e0117bd11e4 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:25:21 +0200 Subject: [PATCH 06/19] btrfs: change the DEV_ITEM 'type' field via sysfs v2: Adds a check to prevent modification while the file system is still mounting. v3: Accept only valid allocation hint types Todo: - Transactions should not be triggered from sysfw writes, see: https://lore.kernel.org/linux-btrfs/20251213200920.1808679-1-kai@kaishome.de/ Link: https://github.com/kakra/linux/pull/36#issuecomment-3406301805 Reported-by: Eli Venter Co-authored-by: Goffredo Baroncelli Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 80 +++++++++++++++++++++++++++++++++++++++++++++- fs/btrfs/volumes.c | 4 +-- fs/btrfs/volumes.h | 2 ++ 3 files changed, 83 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 9cdfda9a0c03c0..9c2eade10cbb7f 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2141,6 +2141,22 @@ static ssize_t btrfs_devinfo_error_stats_show(struct kobject *kobj, BTRFS_ATTR(devid, error_stats, btrfs_devinfo_error_stats_show); #ifdef CONFIG_BTRFS_ALLOCATOR_HINTS +static bool btrfs_dev_allocation_hint_valid(u64 type) +{ + if (type & ~((1ULL << BTRFS_DEV_ALLOCATION_MASK_BIT_COUNT) - 1)) + return false; + + switch (type) { + case BTRFS_DEV_ALLOCATION_PREFERRED_DATA: + case BTRFS_DEV_ALLOCATION_PREFERRED_METADATA: + case BTRFS_DEV_ALLOCATION_METADATA_ONLY: + case BTRFS_DEV_ALLOCATION_DATA_ONLY: + return true; + default: + return false; + } +} + static ssize_t btrfs_devinfo_type_show(struct kobject *kobj, struct kobj_attribute *a, char *buf) { @@ -2149,7 +2165,69 @@ static ssize_t btrfs_devinfo_type_show(struct kobject *kobj, return scnprintf(buf, PAGE_SIZE, "0x%016llx\n", device->type); } -BTRFS_ATTR(devid, type, btrfs_devinfo_type_show); + +static ssize_t btrfs_devinfo_type_store(struct kobject *kobj, + struct kobj_attribute *a, + const char *buf, size_t len) +{ + struct btrfs_fs_info *fs_info; + struct btrfs_root *root; + struct btrfs_device *device; + int ret; + struct btrfs_trans_handle *trans; + + u64 type, prev_type; + + device = container_of(kobj, struct btrfs_device, devid_kobj); + fs_info = device->fs_info; + if (!fs_info) + return -EPERM; + + /* + * Changing the type field requires starting a transaction which will cause a NULL dereference in + * __reserve_bytes if the file system is not fully open. Thus, return EBUSY if the file system is not fully + * initialized. + */ + if (!test_bit(BTRFS_FS_OPEN, &fs_info->flags)) + return -EBUSY; + + root = fs_info->chunk_root; + if (sb_rdonly(fs_info->sb)) + return -EROFS; + + ret = kstrtou64(buf, 0, &type); + if (ret < 0) + return -EINVAL; + + /* for now, only allow defined allocation hint values */ + if (!btrfs_dev_allocation_hint_valid(type)) + return -EINVAL; + + trans = btrfs_start_transaction(root, 1); + if (IS_ERR(trans)) + return PTR_ERR(trans); + + prev_type = device->type; + device->type = type; + + ret = btrfs_update_device(trans, device); + + if (ret < 0) { + btrfs_abort_transaction(trans, ret); + btrfs_end_transaction(trans); + goto abort; + } + + ret = btrfs_commit_transaction(trans); + if (ret < 0) + goto abort; + + return len; +abort: + device->type = prev_type; + return ret; +} +BTRFS_ATTR_RW(devid, type, btrfs_devinfo_type_show, btrfs_devinfo_type_store); #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ /* diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 2bec544d8ba300..708b8d13184e01 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -2942,8 +2942,8 @@ int btrfs_init_new_device(struct btrfs_fs_info *fs_info, const char *device_path return ret; } -static noinline int btrfs_update_device(struct btrfs_trans_handle *trans, - struct btrfs_device *device) +noinline int btrfs_update_device(struct btrfs_trans_handle *trans, + struct btrfs_device *device) { int ret; struct btrfs_path *path; diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 2cbf8080eade06..aec7eaa48d30b5 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -890,6 +890,8 @@ int btrfs_bg_type_to_factor(u64 flags); const char *btrfs_bg_type_to_raid_name(u64 flags); int btrfs_verify_dev_extents(struct btrfs_fs_info *fs_info); bool btrfs_repair_one_zone(struct btrfs_fs_info *fs_info, u64 logical); +int btrfs_update_device(struct btrfs_trans_handle *trans, + struct btrfs_device *device); bool btrfs_pinned_by_swapfile(struct btrfs_fs_info *fs_info, void *ptr); const u8 *btrfs_sb_fsid_ptr(const struct btrfs_super_block *sb); From 5808892d10ac363bab23d2fd9afcab3f3215863e Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:18:02 +0200 Subject: [PATCH 07/19] btrfs: add allocator_hint mode When this mode is enabled, the chunk allocation policy is modified as follows: Each disk may have a different tag: - BTRFS_DEV_ALLOCATION_PREFERRED_METADATA - BTRFS_DEV_ALLOCATION_METADATA_ONLY - BTRFS_DEV_ALLOCATION_DATA_ONLY - BTRFS_DEV_ALLOCATION_PREFERRED_DATA (default) Where: - ALLOCATION_PREFERRED_X means that it is preferred to use this disk for the X chunk type (the other type may be allowed when the space is low) - ALLOCATION_X_ONLY means that it is used *only* for the X chunk type. This means also that it is a preferred choice. Each time the allocator allocates a chunk of type X, first it takes the disks tagged as ALLOCATION_X_ONLY or ALLOCATION_PREFERRED_X. If the space is not enough, it uses also the disks tagged as ALLOCATION_METADATA_ONLY. If the space is not enough, it uses also the other disks, with the exception of the one marked as ALLOCATION_PREFERRED_Y, where Y is the other type of chunk (i.e. not X). Co-authored-by: Goffredo Baroncelli Signed-off-by: Kai Krakow --- fs/btrfs/volumes.c | 105 ++++++++++++++++++++++++++++++++++++++++++++- fs/btrfs/volumes.h | 3 ++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 708b8d13184e01..5eb1134102a737 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -184,6 +184,21 @@ enum btrfs_raid_types __attribute_const__ btrfs_bg_flags_to_raid_index(u64 flags return BTRFS_BG_FLAG_TO_INDEX(profile); } +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS +#define BTRFS_DEV_ALLOCATION_MASK ((1ULL << \ + BTRFS_DEV_ALLOCATION_MASK_BIT_COUNT) - 1) +#define BTRFS_DEV_ALLOCATION_MASK_COUNT (1ULL << \ + BTRFS_DEV_ALLOCATION_MASK_BIT_COUNT) + +static const int alloc_hint_map[BTRFS_DEV_ALLOCATION_MASK_COUNT] = { + [BTRFS_DEV_ALLOCATION_DATA_ONLY] = -1, + [BTRFS_DEV_ALLOCATION_PREFERRED_DATA] = 0, + [BTRFS_DEV_ALLOCATION_PREFERRED_METADATA] = 1, + [BTRFS_DEV_ALLOCATION_METADATA_ONLY] = 2, + /* the other values are set to 0 */ +}; +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + const char *btrfs_bg_type_to_raid_name(u64 flags) { const int index = btrfs_bg_flags_to_raid_index(flags); @@ -5089,13 +5104,20 @@ static int btrfs_add_system_chunk(struct btrfs_fs_info *fs_info, } /* - * sort the devices in descending order by max_avail, total_avail + * sort the devices in descending order by alloc_hint (optional), + * max_avail, total_avail */ static int btrfs_cmp_device_info(const void *a, const void *b) { const struct btrfs_device_info *di_a = a; const struct btrfs_device_info *di_b = b; +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + if (di_a->alloc_hint > di_b->alloc_hint) + return -1; + if (di_a->alloc_hint < di_b->alloc_hint) + return 1; +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ if (di_a->max_avail > di_b->max_avail) return -1; if (di_a->max_avail < di_b->max_avail) @@ -5303,16 +5325,97 @@ static int gather_device_info(struct btrfs_fs_devices *fs_devices, devices_info[ndevs].max_avail = max_avail; devices_info[ndevs].total_avail = total_avail; devices_info[ndevs].dev = device; + +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + if ((ctl->type & BTRFS_BLOCK_GROUP_DATA) && + (ctl->type & BTRFS_BLOCK_GROUP_METADATA)) { + /* + * if mixed bg set all the alloc_hint + * fields to the same value, so the sorting + * is not affected + */ + devices_info[ndevs].alloc_hint = 0; + } else if (ctl->type & BTRFS_BLOCK_GROUP_DATA) { + int hint = device->type & BTRFS_DEV_ALLOCATION_MASK; + + /* + * skip BTRFS_DEV_METADATA_ONLY disks + */ + if (BTRFS_DEV_ALLOCATION_METADATA_ONLY == hint) + continue; + /* + * if a data chunk must be allocated, + * sort also by hint (data disk + * higher priority) + */ + devices_info[ndevs].alloc_hint = -alloc_hint_map[hint]; + } else { /* BTRFS_BLOCK_GROUP_METADATA */ + int hint = device->type & BTRFS_DEV_ALLOCATION_MASK; + + /* + * skip BTRFS_DEV_DATA_ONLY disks + */ + if (BTRFS_DEV_ALLOCATION_DATA_ONLY == hint) + continue; + /* + * if a metadata chunk must be allocated, + * sort also by hint (metadata hint + * higher priority) + */ + devices_info[ndevs].alloc_hint = alloc_hint_map[hint]; + } +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + ++ndevs; } ctl->ndevs = ndevs; +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + /* + * no devices available + */ + if (!ndevs) + return 0; +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + /* * now sort the devices by hole size / available space */ sort(devices_info, ndevs, sizeof(struct btrfs_device_info), btrfs_cmp_device_info, NULL); +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + /* + * select the minimum set of disks grouped by hint that + * can host the chunk + */ + ndevs = 0; + while (ndevs < ctl->ndevs) { + int hint = devices_info[ndevs++].alloc_hint; + while (ndevs < ctl->ndevs && + devices_info[ndevs].alloc_hint == hint) + ndevs++; + if (ndevs >= ctl->devs_min) + break; + } + + BUG_ON(ndevs > ctl->ndevs); + ctl->ndevs = ndevs; + + /* + * the next layers require the devices_info ordered by + * max_avail. If we are returning two (or more) different + * group of alloc_hint, this is not always true. So sort + * these again. + */ + + for (int i = 0 ; i < ndevs ; i++) + devices_info[i].alloc_hint = 0; + + sort(devices_info, ndevs, sizeof(struct btrfs_device_info), + btrfs_cmp_device_info, NULL); +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ + return 0; } diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index aec7eaa48d30b5..6ba641f9f1ecb3 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -599,6 +599,9 @@ struct btrfs_device_info { u64 dev_offset; u64 max_avail; u64 total_avail; +#ifdef CONFIG_BTRFS_ALLOCATOR_HINTS + int alloc_hint; +#endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ }; struct btrfs_raid_attr { From 67347fae0de7d6b8025d659769354c72c7d8c19a Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:09:55 +0200 Subject: [PATCH 08/19] btrfs: add allocator_hint for no allocation preferred This is useful where you want to avoid new allocations of chunks on a disk which is going to be removed from the pool anyways, e.g. due to bad blocks or because it's slow. Signed-off-by: Kai Krakow --- fs/btrfs/Kconfig | 3 +++ fs/btrfs/sysfs.c | 1 + fs/btrfs/volumes.c | 6 +++++- include/uapi/linux/btrfs_tree.h | 2 ++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/Kconfig b/fs/btrfs/Kconfig index 1a3617725ed9cf..9348be9d920b92 100644 --- a/fs/btrfs/Kconfig +++ b/fs/btrfs/Kconfig @@ -120,6 +120,9 @@ config BTRFS_ALLOCATOR_HINTS chunks will ever be allocated from this device - type = 3 - allocate only data chunks from this ID, no meta data chunks will ever be allocated from this device + - type = 4 - allocate any chunks from this device last, will never + allocate any space from this device unless there isn't + enough space on other devices If unsure, say N. diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 9c2eade10cbb7f..fa453a045a5419 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2151,6 +2151,7 @@ static bool btrfs_dev_allocation_hint_valid(u64 type) case BTRFS_DEV_ALLOCATION_PREFERRED_METADATA: case BTRFS_DEV_ALLOCATION_METADATA_ONLY: case BTRFS_DEV_ALLOCATION_DATA_ONLY: + case BTRFS_DEV_ALLOCATION_PREFERRED_NONE: return true; default: return false; diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 5eb1134102a737..9d6343ed28e18f 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -195,6 +195,7 @@ static const int alloc_hint_map[BTRFS_DEV_ALLOCATION_MASK_COUNT] = { [BTRFS_DEV_ALLOCATION_PREFERRED_DATA] = 0, [BTRFS_DEV_ALLOCATION_PREFERRED_METADATA] = 1, [BTRFS_DEV_ALLOCATION_METADATA_ONLY] = 2, + [BTRFS_DEV_ALLOCATION_PREFERRED_NONE] = 99, /* the other values are set to 0 */ }; #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ @@ -5362,7 +5363,10 @@ static int gather_device_info(struct btrfs_fs_devices *fs_devices, * sort also by hint (metadata hint * higher priority) */ - devices_info[ndevs].alloc_hint = alloc_hint_map[hint]; + if (BTRFS_DEV_ALLOCATION_PREFERRED_NONE == hint) + devices_info[ndevs].alloc_hint = -alloc_hint_map[hint]; + else + devices_info[ndevs].alloc_hint = alloc_hint_map[hint]; } #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h index f004ab4341537f..c32a3be41dc909 100644 --- a/include/uapi/linux/btrfs_tree.h +++ b/include/uapi/linux/btrfs_tree.h @@ -595,6 +595,8 @@ struct btrfs_node { #define BTRFS_DEV_ALLOCATION_METADATA_ONLY (2ULL) /* only data chunk allowed */ #define BTRFS_DEV_ALLOCATION_DATA_ONLY (3ULL) +/* avoid chunk allocation if possible */ +#define BTRFS_DEV_ALLOCATION_PREFERRED_NONE (4ULL) /* 5..7 are unused values */ #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ From eec9811931d8f170262812e4bfffd5132d62de0b Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Fri, 10 Jul 2026 23:51:46 +0200 Subject: [PATCH 09/19] btrfs: add allocator_hint to disable allocation completely This is useful where you want to prevent new allocations of chunks to a set of multiple disks which are going to be removed from the pool. This acts as a multiple `btrfs dev remove` on steroids that can remove multiple disks in parallel without moving data to disks which would be removed in the next round. In such cases, it will avoid moving the same data multiple times, and thus avoid placing it on potentially bad disks. Thanks to @Zygo for the explanation and suggestion. Link: https://github.com/kdave/btrfs-progs/issues/907#issuecomment-2520897104 Signed-off-by: Kai Krakow --- fs/btrfs/Kconfig | 3 +++ fs/btrfs/sysfs.c | 1 + fs/btrfs/volumes.c | 11 +++++++++++ include/uapi/linux/btrfs_tree.h | 4 +++- 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/Kconfig b/fs/btrfs/Kconfig index 9348be9d920b92..940a24b3402598 100644 --- a/fs/btrfs/Kconfig +++ b/fs/btrfs/Kconfig @@ -123,6 +123,9 @@ config BTRFS_ALLOCATOR_HINTS - type = 4 - allocate any chunks from this device last, will never allocate any space from this device unless there isn't enough space on other devices + - type = 5 - never allocate any new chunks, useful when putting a + device out of use and to avoid redundant chunk writes + during balance/replace If unsure, say N. diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index fa453a045a5419..f6d5846e46837c 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2152,6 +2152,7 @@ static bool btrfs_dev_allocation_hint_valid(u64 type) case BTRFS_DEV_ALLOCATION_METADATA_ONLY: case BTRFS_DEV_ALLOCATION_DATA_ONLY: case BTRFS_DEV_ALLOCATION_PREFERRED_NONE: + case BTRFS_DEV_ALLOCATION_NONE_ONLY: return true; default: return false; diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 9d6343ed28e18f..816a3124bb7702 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -191,6 +191,7 @@ enum btrfs_raid_types __attribute_const__ btrfs_bg_flags_to_raid_index(u64 flags BTRFS_DEV_ALLOCATION_MASK_BIT_COUNT) static const int alloc_hint_map[BTRFS_DEV_ALLOCATION_MASK_COUNT] = { + [BTRFS_DEV_ALLOCATION_NONE_ONLY] = -99, [BTRFS_DEV_ALLOCATION_DATA_ONLY] = -1, [BTRFS_DEV_ALLOCATION_PREFERRED_DATA] = 0, [BTRFS_DEV_ALLOCATION_PREFERRED_METADATA] = 1, @@ -5344,6 +5345,11 @@ static int gather_device_info(struct btrfs_fs_devices *fs_devices, */ if (BTRFS_DEV_ALLOCATION_METADATA_ONLY == hint) continue; + /* + * skip BTRFS_DEV_NONE_ONLY disks + */ + if (BTRFS_DEV_ALLOCATION_NONE_ONLY == hint) + continue; /* * if a data chunk must be allocated, * sort also by hint (data disk @@ -5358,6 +5364,11 @@ static int gather_device_info(struct btrfs_fs_devices *fs_devices, */ if (BTRFS_DEV_ALLOCATION_DATA_ONLY == hint) continue; + /* + * skip BTRFS_DEV_NONE_ONLY disks + */ + if (BTRFS_DEV_ALLOCATION_NONE_ONLY == hint) + continue; /* * if a metadata chunk must be allocated, * sort also by hint (metadata hint diff --git a/include/uapi/linux/btrfs_tree.h b/include/uapi/linux/btrfs_tree.h index c32a3be41dc909..8a6087d6e967aa 100644 --- a/include/uapi/linux/btrfs_tree.h +++ b/include/uapi/linux/btrfs_tree.h @@ -597,7 +597,9 @@ struct btrfs_node { #define BTRFS_DEV_ALLOCATION_DATA_ONLY (3ULL) /* avoid chunk allocation if possible */ #define BTRFS_DEV_ALLOCATION_PREFERRED_NONE (4ULL) -/* 5..7 are unused values */ +/* deny chunk allocation */ +#define BTRFS_DEV_ALLOCATION_NONE_ONLY (5ULL) +/* 6..7 are unused values */ #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ From fd31bbf241428aa79cc6fcd0d51c0db441f35f47 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 11 Jul 2026 00:44:51 +0200 Subject: [PATCH 10/19] btrfs: add io read stats per device to devinfo This adds read stats per device to devinfo to evaluate the effects of different read policies better. This adds a new file /sys/fs/btrfs/BTRFS-UUID/devinfo/ID/read_stats. Signed-off-by: Kai Krakow --- fs/btrfs/Kconfig | 12 ++++++++++++ fs/btrfs/sysfs.c | 28 ++++++++++++++++++++++++++++ fs/btrfs/volumes.c | 30 +++++++++++++++++++++++++++--- fs/btrfs/volumes.h | 8 ++++++++ 4 files changed, 75 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/Kconfig b/fs/btrfs/Kconfig index 940a24b3402598..888561b2a9ed79 100644 --- a/fs/btrfs/Kconfig +++ b/fs/btrfs/Kconfig @@ -129,6 +129,18 @@ config BTRFS_ALLOCATOR_HINTS If unsure, say N. +config BTRFS_PER_DEVICE_IO_STATS + bool "Btrfs per io devices stats" + depends on BTRFS_FS + default n + help + Enable collecting io read stats per devices to evaluate the effects + of different read policies better. + + This adds a new file /sys/fs/btrfs/BTRFS-UUID/devinfo/ID/read_stats. + + If unsure, say N. + config BTRFS_EXPERIMENTAL bool "Btrfs experimental features" depends on BTRFS_FS diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index f6d5846e46837c..e85dae02aef746 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -26,6 +26,10 @@ #include "fs.h" #include "accessors.h" +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#include +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + /* * Structure name Path * -------------------------------------------------------------------------- @@ -2232,12 +2236,36 @@ static ssize_t btrfs_devinfo_type_store(struct kobject *kobj, BTRFS_ATTR_RW(devid, type, btrfs_devinfo_type_show, btrfs_devinfo_type_store); #endif /* CONFIG_BTRFS_ALLOCATOR_HINTS */ +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +static ssize_t btrfs_devinfo_read_stats_show(struct kobject *kobj, + struct kobj_attribute *a, char *buf) +{ + struct btrfs_device *device = container_of(kobj, struct btrfs_device, + devid_kobj); + u64 read_wait = part_stat_read(device->bdev, nsecs[READ]); + unsigned long read_ios = part_stat_read(device->bdev, ios[READ]); + + u64 avg_wait = 0; + if (read_wait && read_ios && read_wait >= read_ios) + avg_wait = div_u64(read_wait, read_ios); + + return scnprintf(buf, PAGE_SIZE, "ios %lu wait %llu avg %llu age %llu ignored %llu\n", + read_ios, read_wait, avg_wait, + (u64)atomic64_read(&device->last_io_age), + (u64)atomic64_read(&device->stripe_ignored)); +} +BTRFS_ATTR(devid, read_stats, btrfs_devinfo_read_stats_show); +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + /* * Information about one device. * * Path: /sys/fs/btrfs//devinfo// */ static struct attribute *devid_attrs[] = { +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + BTRFS_ATTR_PTR(devid, read_stats), +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ BTRFS_ATTR_PTR(devid, error_stats), BTRFS_ATTR_PTR(devid, fsid), BTRFS_ATTR_PTR(devid, in_fs_metadata), diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 816a3124bb7702..64be2d6d42acf3 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6139,6 +6139,15 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, else num_stripes = map->num_stripes; +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + /* age each possible stripe by 1 IO */ + for (int i = first; i < first + num_stripes; i++) { + struct btrfs_device *device = map->stripes[i].dev; + atomic64_inc(&device->last_io_age); + atomic64_inc(&device->stripe_ignored); + } +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + switch (policy) { default: /* Shouldn't happen, just warn and use pid instead of failing */ @@ -6174,14 +6183,29 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, for (tolerance = 0; tolerance < 2; tolerance++) { if (map->stripes[preferred_mirror].dev->bdev && (tolerance || map->stripes[preferred_mirror].dev != srcdev)) - return preferred_mirror; + goto out; for (i = first; i < first + num_stripes; i++) { if (map->stripes[i].dev->bdev && - (tolerance || map->stripes[i].dev != srcdev)) - return i; + (tolerance || map->stripes[i].dev != srcdev)) { + preferred_mirror = i; + goto out; + } } } +out: +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + do { + struct btrfs_device *preferred_device = map->stripes[preferred_mirror].dev; + + /* reset age of selected stripe */ + atomic64_set(&preferred_device->last_io_age, 0); + + /* do not count ignores for the selected stripe */ + atomic64_dec(&preferred_device->stripe_ignored); + } while (0); +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + /* we couldn't find one that doesn't fail. Just return something * and the io error handling code will clean up eventually */ diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 6ba641f9f1ecb3..21d5af56a407d4 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -201,6 +201,14 @@ struct btrfs_device { /* Bandwidth limit for scrub, in bytes */ u64 scrub_speed_max; + +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + /* store an age of last read access */ + atomic64_t last_io_age; + + /* store how often a stripe has been ignored as a read candidate */ + atomic64_t stripe_ignored; +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ }; /* From d334c30b11c248a45208d9b33851aa30749ba068 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sat, 13 Dec 2025 18:34:36 +0100 Subject: [PATCH 11/19] btrfs: move read policies out of experimental Read policies seem safe and stable enough to move it out of the experimental feature set. This allows us to add more policies without forcing users to enable the full experimental feature set. Signed-off-by: Kai Krakow --- fs/btrfs/Kconfig | 15 +++++++++++++++ fs/btrfs/super.c | 8 ++++---- fs/btrfs/sysfs.c | 24 ++++++++++++------------ fs/btrfs/sysfs.h | 4 ++-- fs/btrfs/volumes.c | 12 ++++++------ fs/btrfs/volumes.h | 10 ++++++---- 6 files changed, 45 insertions(+), 28 deletions(-) diff --git a/fs/btrfs/Kconfig b/fs/btrfs/Kconfig index 888561b2a9ed79..e30e5f153d7bfd 100644 --- a/fs/btrfs/Kconfig +++ b/fs/btrfs/Kconfig @@ -141,6 +141,21 @@ config BTRFS_PER_DEVICE_IO_STATS If unsure, say N. +config BTRFS_READ_POLICIES + bool "Btrfs read policies" + depends on BTRFS_FS + default n + help + This enables btrfs read policies to control how btrfs selects stripes + from a mirror during read operations. This was originally part of + the experimental feature set but it is safe to use and can provide + huge performance benefits in certain scenarios without causing any + performance regressions. + + This adds a new file /sys/fs/btrfs/BTRFS-UUID/read_policy. + + If unsure, say N. + config BTRFS_EXPERIMENTAL bool "Btrfs experimental features" depends on BTRFS_FS diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index 430e7419349c9a..8d05a3a975abc7 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -2498,7 +2498,7 @@ static int __init btrfs_print_mod_info(void) #endif ; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES if (btrfs_get_mod_read_policy() == NULL) pr_info("Btrfs loaded%s\n", options); else @@ -2506,7 +2506,7 @@ static int __init btrfs_print_mod_info(void) options, btrfs_get_mod_read_policy()); #else pr_info("Btrfs loaded%s\n", options); -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ return 0; } @@ -2565,11 +2565,11 @@ static const struct init_sequence mod_init_seq[] = { }, { .init_func = btrfs_extent_map_init, .exit_func = btrfs_extent_map_exit, -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES }, { .init_func = btrfs_read_policy_init, .exit_func = NULL, -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ }, { .init_func = ordered_data_init, .exit_func = ordered_data_exit, diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index e85dae02aef746..0f9d017b1f56b8 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1323,13 +1323,13 @@ BTRFS_ATTR(, temp_fsid, btrfs_temp_fsid_show); static const char *btrfs_read_policy_name[] = { "pid", -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES "round-robin", "devid", -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ }; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES /* Global module configuration parameters. */ static char *read_policy; @@ -1342,7 +1342,7 @@ char *btrfs_get_mod_read_policy(void) module_param(read_policy, charp, 0); MODULE_PARM_DESC(read_policy, "Global read policy: pid (default), round-robin[:], devid[:]"); -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ int btrfs_read_policy_to_enum(const char *str, s64 *value_ret) { @@ -1354,7 +1354,7 @@ int btrfs_read_policy_to_enum(const char *str, s64 *value_ret) strscpy(param, str); -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES /* Separate value from input in policy:value format. */ value_str = strchr(param, ':'); if (value_str) { @@ -1371,12 +1371,12 @@ int btrfs_read_policy_to_enum(const char *str, s64 *value_ret) if (*retptr != 0 || *value_ret <= 0) return -EINVAL; } -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ return sysfs_match_string(btrfs_read_policy_name, param); } -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES int __init btrfs_read_policy_init(void) { s64 value; @@ -1388,7 +1388,7 @@ int __init btrfs_read_policy_init(void) return 0; } -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ static ssize_t btrfs_read_policy_show(struct kobject *kobj, struct kobj_attribute *a, char *buf) @@ -1407,7 +1407,7 @@ static ssize_t btrfs_read_policy_show(struct kobject *kobj, ret += sysfs_emit_at(buf, ret, "%s", btrfs_read_policy_name[i]); -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES if (i == BTRFS_READ_POLICY_RR) ret += sysfs_emit_at(buf, ret, ":%u", READ_ONCE(fs_devices->rr_min_contig_read)); @@ -1415,7 +1415,7 @@ static ssize_t btrfs_read_policy_show(struct kobject *kobj, if (i == BTRFS_READ_POLICY_DEVID) ret += sysfs_emit_at(buf, ret, ":%llu", READ_ONCE(fs_devices->read_devid)); -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ if (i == policy) ret += sysfs_emit_at(buf, ret, "]"); } @@ -1437,7 +1437,7 @@ static ssize_t btrfs_read_policy_store(struct kobject *kobj, if (index < 0) return -EINVAL; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES /* If moving from RR then disable collecting fs stats. */ if (fs_devices->read_policy == BTRFS_READ_POLICY_RR && index != BTRFS_READ_POLICY_RR) fs_devices->collect_fs_stats = false; @@ -1496,7 +1496,7 @@ static ssize_t btrfs_read_policy_store(struct kobject *kobj, return len; } -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ if (index != READ_ONCE(fs_devices->read_policy)) { WRITE_ONCE(fs_devices->read_policy, index); btrfs_info(fs_devices->fs_info, "read policy set to '%s'", diff --git a/fs/btrfs/sysfs.h b/fs/btrfs/sysfs.h index 0f94ae9232101b..957688f746fb29 100644 --- a/fs/btrfs/sysfs.h +++ b/fs/btrfs/sysfs.h @@ -50,9 +50,9 @@ void btrfs_sysfs_del_one_qgroup(struct btrfs_fs_info *fs_info, struct btrfs_qgroup *qgroup); int btrfs_read_policy_to_enum(const char *str, s64 *value); -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES int __init btrfs_read_policy_init(void); char *btrfs_get_mod_read_policy(void); -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ #endif diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 64be2d6d42acf3..64014be6828675 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -1282,7 +1282,7 @@ static int open_fs_devices(struct btrfs_fs_devices *fs_devices, fs_devices->latest_dev = latest_dev; fs_devices->total_rw_bytes = 0; fs_devices->chunk_alloc_policy = BTRFS_CHUNK_ALLOC_REGULAR; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES fs_devices->rr_min_contig_read = BTRFS_DEFAULT_RR_MIN_CONTIG_READ; fs_devices->read_devid = latest_dev->devid; fs_devices->read_policy = btrfs_read_policy_to_enum(btrfs_get_mod_read_policy(), @@ -1298,7 +1298,7 @@ static int open_fs_devices(struct btrfs_fs_devices *fs_devices, } #else fs_devices->read_policy = BTRFS_READ_POLICY_PID; -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ return 0; } @@ -6050,7 +6050,7 @@ unsigned long btrfs_full_stripe_len(struct btrfs_fs_info *fs_info, return len; } -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES static int btrfs_read_preferred(struct btrfs_chunk_map *map, int first, int num_stripes) { for (int index = first; index < first + num_stripes; index++) { @@ -6118,7 +6118,7 @@ static int btrfs_read_rr(const struct btrfs_chunk_map *map, int first, int num_s read_cycle = total_reads / min_reads_per_dev; return stripes[read_cycle % num_stripes].num; } -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ static int find_live_mirror(struct btrfs_fs_info *fs_info, struct btrfs_chunk_map *map, int first, @@ -6158,14 +6158,14 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, case BTRFS_READ_POLICY_PID: preferred_mirror = first + (current->pid % num_stripes); break; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES case BTRFS_READ_POLICY_RR: preferred_mirror = btrfs_read_rr(map, first, num_stripes); break; case BTRFS_READ_POLICY_DEVID: preferred_mirror = btrfs_read_preferred(map, first, num_stripes); break; -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ } if (dev_replace_is_ongoing && diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 21d5af56a407d4..5d7bc046635b90 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -318,12 +318,12 @@ enum btrfs_chunk_allocation_policy { enum btrfs_read_policy { /* Use process PID to choose the stripe */ BTRFS_READ_POLICY_PID, -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES /* Balancing RAID1 reads across all striped devices (round-robin). */ BTRFS_READ_POLICY_RR, /* Read from a specific device. */ BTRFS_READ_POLICY_DEVID, -#endif +#endif /* CONFIG_BTRFS_READ_POLICIES */ BTRFS_NR_READ_POLICY, }; @@ -463,7 +463,7 @@ struct btrfs_fs_devices { /* Policy used to read the mirrored stripes. */ enum btrfs_read_policy read_policy; -#ifdef CONFIG_BTRFS_EXPERIMENTAL +#ifdef CONFIG_BTRFS_READ_POLICIES /* * Minimum contiguous reads before switching to next device, the unit * is one block/sectorsize. @@ -472,10 +472,12 @@ struct btrfs_fs_devices { /* Device to be used for reading in case of RAID1. */ u64 read_devid; +#endif /* CONFIG_BTRFS_READ_POLICIES */ +#ifdef CONFIG_BTRFS_EXPERIMENTAL /* Checksum mode - offload it or do it synchronously. */ enum btrfs_offload_csum_mode offload_csum_mode; -#endif +#endif /* CONFIG_BTRFS_EXPERIMENTAL */ }; #define BTRFS_MAX_DEVS(info) ((BTRFS_MAX_ITEM_SIZE(info) \ From 6c1fcfe273dd123991eab6d530d62a5d421f1f1f Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Fri, 25 Apr 2025 21:46:52 +0200 Subject: [PATCH 12/19] btrfs: add in-flight queue read policy Select the preferred stripe based on the mirror with the least in-flight requests. v2: - Skip missing mirrors before reading their in-flight statistics so the policy remains safe on degraded arrays. - Initialize the fallback stripe from the current mirror set instead of assuming stripe zero. Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 3 ++- fs/btrfs/volumes.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++ fs/btrfs/volumes.h | 2 ++ 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 0f9d017b1f56b8..4f87e438b721e4 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1325,6 +1325,7 @@ static const char *btrfs_read_policy_name[] = { "pid", #ifdef CONFIG_BTRFS_READ_POLICIES "round-robin", + "queue", "devid", #endif /* CONFIG_BTRFS_READ_POLICIES */ }; @@ -1341,7 +1342,7 @@ char *btrfs_get_mod_read_policy(void) /* Set perms to 0, disable /sys/module/btrfs/parameter/read_policy interface. */ module_param(read_policy, charp, 0); MODULE_PARM_DESC(read_policy, -"Global read policy: pid (default), round-robin[:], devid[:]"); +"Global read policy: pid (default), round-robin[:], queue, devid[:]"); #endif /* CONFIG_BTRFS_READ_POLICIES */ int btrfs_read_policy_to_enum(const char *str, s64 *value_ret) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 64014be6828675..91a2a7f57b790e 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -34,6 +34,10 @@ #include "super.h" #include "raid-stripe-tree.h" +#ifdef CONFIG_BTRFS_READ_POLICIES +#include +#endif /* CONFIG_BTRFS_READ_POLICIES */ + #define BTRFS_BLOCK_GROUP_STRIPE_MASK (BTRFS_BLOCK_GROUP_RAID0 | \ BTRFS_BLOCK_GROUP_RAID10 | \ BTRFS_BLOCK_GROUP_RAID56_MASK) @@ -6051,6 +6055,50 @@ unsigned long btrfs_full_stripe_len(struct btrfs_fs_info *fs_info, } #ifdef CONFIG_BTRFS_READ_POLICIES +static unsigned int part_in_flight(struct block_device *part) +{ + unsigned int inflight = 0; + int cpu; + + for_each_possible_cpu(cpu) { + inflight += part_stat_local_read_cpu(part, in_flight[READ], cpu) + + part_stat_local_read_cpu(part, in_flight[WRITE], cpu); + } + if ((int)inflight < 0) + inflight = 0; + + return inflight; +} + +/* + * btrfs_earliest_stripe + * + * Select a stripe from the device with shortest in-flight requests. + */ +static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, + struct btrfs_chunk_map *map, int first, + int num_stripes) +{ + u64 best_in_flight = U64_MAX; + int best_stripe = first; + + for (int index = first; index < first + num_stripes; index++) { + struct btrfs_device *device = map->stripes[index].dev; + u64 in_flight; + + if (!device->bdev) + continue; + + in_flight = part_in_flight(device->bdev); + if (best_in_flight > in_flight) { + best_in_flight = in_flight; + best_stripe = index; + } + } + + return best_stripe; +} + static int btrfs_read_preferred(struct btrfs_chunk_map *map, int first, int num_stripes) { for (int index = first; index < first + num_stripes; index++) { @@ -6162,6 +6210,10 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, case BTRFS_READ_POLICY_RR: preferred_mirror = btrfs_read_rr(map, first, num_stripes); break; + case BTRFS_READ_POLICY_QUEUE: + preferred_mirror = btrfs_read_earliest(fs_info, map, first, + num_stripes); + break; case BTRFS_READ_POLICY_DEVID: preferred_mirror = btrfs_read_preferred(map, first, num_stripes); break; diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 5d7bc046635b90..eae340fd068f79 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -321,6 +321,8 @@ enum btrfs_read_policy { #ifdef CONFIG_BTRFS_READ_POLICIES /* Balancing RAID1 reads across all striped devices (round-robin). */ BTRFS_READ_POLICY_RR, + /* Read from the device with the least in-flight requests */ + BTRFS_READ_POLICY_QUEUE, /* Read from a specific device. */ BTRFS_READ_POLICY_DEVID, #endif /* CONFIG_BTRFS_READ_POLICIES */ From c6a0ea087ea2c530e63e5bc8343d3060c9735c72 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Thu, 9 Jul 2026 12:35:14 +0200 Subject: [PATCH 13/19] btrfs: move age io stat out of CONFIG_BTRFS_PER_DEVICE_IO_STATS In a follow up commit, we will be using the age counter as a tie break if two candidate devices currently have the same amount of inflight requests pending. To use this effectively, the age counter must always exist, no matter the io stats configuration. Signed-off-by: Kai Krakow --- fs/btrfs/volumes.c | 13 +++++++++---- fs/btrfs/volumes.h | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 91a2a7f57b790e..f739557fc7e62a 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6187,14 +6187,17 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, else num_stripes = map->num_stripes; -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#if defined(CONFIG_BTRFS_READ_POLICIES) || defined(CONFIG_BTRFS_PER_DEVICE_IO_STATS) /* age each possible stripe by 1 IO */ for (int i = first; i < first + num_stripes; i++) { struct btrfs_device *device = map->stripes[i].dev; + atomic64_inc(&device->last_io_age); +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS atomic64_inc(&device->stripe_ignored); - } #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + } +#endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ switch (policy) { default: @@ -6246,17 +6249,19 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, } out: -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#if defined(CONFIG_BTRFS_READ_POLICIES) || defined(CONFIG_BTRFS_PER_DEVICE_IO_STATS) do { struct btrfs_device *preferred_device = map->stripes[preferred_mirror].dev; /* reset age of selected stripe */ atomic64_set(&preferred_device->last_io_age, 0); +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS /* do not count ignores for the selected stripe */ atomic64_dec(&preferred_device->stripe_ignored); - } while (0); #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + } while (0); +#endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ /* we couldn't find one that doesn't fail. Just return something * and the io error handling code will clean up eventually diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index eae340fd068f79..766af038dc547a 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -202,10 +202,10 @@ struct btrfs_device { /* Bandwidth limit for scrub, in bytes */ u64 scrub_speed_max; -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS /* store an age of last read access */ atomic64_t last_io_age; +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS /* store how often a stripe has been ignored as a read candidate */ atomic64_t stripe_ignored; #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ From 5cada41987494bcb4eec6a80445777195b4da08e Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Thu, 9 Jul 2026 12:27:18 +0200 Subject: [PATCH 14/19] btrfs: break queue read ties by mirror age There's an unfair bias towards the first device in the pool of candidate devices for queue-based read balancing: If two candidates are mostly idle or share the same amount of inflight requests, the first candidate always wins, no matter the history. The age counter already provides history: Use the age counter to select the device which has recently been ignored among the candidates if there's a tie break on inflight IO requests. This distributes read requests more evenly across all devices. As part of this, a device with bad latency can be selected more often now if it is last in the pool, or less often, if it is first in the pool. This will be addressed by a later commit by adapting latency history into the decision. Signed-off-by: Kai Krakow --- fs/btrfs/volumes.c | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index f739557fc7e62a..f04b9b90deb543 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6070,28 +6070,48 @@ static unsigned int part_in_flight(struct block_device *part) return inflight; } +static bool btrfs_queue_read_candidate_better(u64 in_flight, u64 last_io_age, + u64 best_in_flight, + u64 best_last_io_age) +{ + if (in_flight < best_in_flight) + return true; + + if (in_flight == best_in_flight && last_io_age > best_last_io_age) + return true; + + return false; +} + /* * btrfs_earliest_stripe * - * Select a stripe from the device with shortest in-flight requests. + * Select a stripe from the device with shortest in-flight requests. If several + * stripes have the same queue depth, prefer the stripe that has gone the + * longest without being selected. */ static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, struct btrfs_chunk_map *map, int first, - int num_stripes) + int num_stripes) { u64 best_in_flight = U64_MAX; + u64 best_last_io_age = 0; int best_stripe = first; for (int index = first; index < first + num_stripes; index++) { struct btrfs_device *device = map->stripes[index].dev; u64 in_flight; + u64 last_io_age = atomic64_read(&device->last_io_age); if (!device->bdev) continue; in_flight = part_in_flight(device->bdev); - if (best_in_flight > in_flight) { + if (btrfs_queue_read_candidate_better(in_flight, last_io_age, + best_in_flight, + best_last_io_age)) { best_in_flight = in_flight; + best_last_io_age = last_io_age; best_stripe = index; } } From 391019b9b88c913ec01601439bff8a218b7678d1 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Thu, 9 Jul 2026 13:10:10 +0200 Subject: [PATCH 15/19] btrfs: add read latency health stats to per-device io read stats Track a windowed average read latency per device and expose it through the per-device read_stats file. The health value is updated from block-layer read ios and wait-time deltas in find_live_mirror(), independently of any adaptive read policy. This lets read_stats diagnose latency-sick devices before queue-adaptive selection is introduced. The patch deliberately does not derive a slow flag from the per-device stats. Whether a device is slow depends on the actual mirror candidates for a read and must be decided later in the queue-adaptive selection path. v2: - Leave missing mirrors unclassified instead of dereferencing a NULL block device on degraded arrays. - Pace each device independently so one candidate cannot reset another candidate's sampling interval. - Claim each update interval with cmpxchg() so concurrent readers cannot race or move the statistics checkpoint backwards. Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 7 +++-- fs/btrfs/volumes.c | 74 ++++++++++++++++++++++++++++++++++++++++++++-- fs/btrfs/volumes.h | 14 +++++++++ 3 files changed, 91 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 4f87e438b721e4..6b3e2ca1411b6f 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2245,15 +2245,18 @@ static ssize_t btrfs_devinfo_read_stats_show(struct kobject *kobj, devid_kobj); u64 read_wait = part_stat_read(device->bdev, nsecs[READ]); unsigned long read_ios = part_stat_read(device->bdev, ios[READ]); + u64 health_avg = READ_ONCE(device->health_avg_ns); u64 avg_wait = 0; if (read_wait && read_ios && read_wait >= read_ios) avg_wait = div_u64(read_wait, read_ios); - return scnprintf(buf, PAGE_SIZE, "ios %lu wait %llu avg %llu age %llu ignored %llu\n", + return scnprintf(buf, PAGE_SIZE, + "ios %lu wait %llu avg %llu age %llu ignored %llu health avg %llu\n", read_ios, read_wait, avg_wait, (u64)atomic64_read(&device->last_io_age), - (u64)atomic64_read(&device->stripe_ignored)); + (u64)atomic64_read(&device->stripe_ignored), + health_avg); } BTRFS_ATTR(devid, read_stats, btrfs_devinfo_read_stats_show); #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index f04b9b90deb543..3bf5a4d8e82dc0 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -34,9 +34,12 @@ #include "super.h" #include "raid-stripe-tree.h" -#ifdef CONFIG_BTRFS_READ_POLICIES +#if defined(CONFIG_BTRFS_READ_POLICIES) || defined(CONFIG_BTRFS_PER_DEVICE_IO_STATS) #include -#endif /* CONFIG_BTRFS_READ_POLICIES */ +#endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#include +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ #define BTRFS_BLOCK_GROUP_STRIPE_MASK (BTRFS_BLOCK_GROUP_RAID0 | \ BTRFS_BLOCK_GROUP_RAID10 | \ @@ -6119,6 +6122,69 @@ static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, return best_stripe; } +#endif /* CONFIG_BTRFS_READ_POLICIES */ + +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#define BTRFS_READ_HEALTH_RECHECK_INTERVAL HZ +#define BTRFS_READ_HEALTH_MIN_SAMPLE_IOS 256ULL + +/* + * Refresh the cached windowed avg read latency for the current candidates. + * The avg is a delta against the previous checkpoint, not a lifetime average, + * so unrelated bulk reads can only skew the current window. + */ +static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, + int num_stripes) +{ + for (int index = first; index < first + num_stripes; index++) { + struct btrfs_device *device = map->stripes[index].dev; + unsigned long checked_jiffies = READ_ONCE(device->health_check_jiffies); + u64 read_ios, read_wait, checked_ios, checked_wait; + u64 delta_ios, delta_wait; + + /* + * A missing mirror has no bdev; part_stat_read() would + * dereference it. Leave it unclassified (health_avg_ns stays + * 0, "no data") - find_live_mirror()'s own tolerance loop + * already avoids selecting it regardless of read policy. + */ + if (!device->bdev) + continue; + + if (!time_after(jiffies, checked_jiffies + BTRFS_READ_HEALTH_RECHECK_INTERVAL)) + continue; + + /* Only one reader may advance a device's checkpoint each interval. */ + if (cmpxchg(&device->health_check_jiffies, checked_jiffies, jiffies) != + checked_jiffies) + continue; + + read_ios = part_stat_read(device->bdev, ios[READ]); + read_wait = part_stat_read(device->bdev, nsecs[READ]); + checked_ios = READ_ONCE(device->health_check_ios); + checked_wait = READ_ONCE(device->health_check_wait); + + if (checked_ios == 0 && checked_wait == 0 && read_ios != 0) { + WRITE_ONCE(device->health_check_ios, read_ios); + WRITE_ONCE(device->health_check_wait, read_wait); + continue; + } + + delta_ios = read_ios >= checked_ios ? read_ios - checked_ios : read_ios; + delta_wait = read_wait >= checked_wait ? read_wait - checked_wait : read_wait; + + if (delta_ios < BTRFS_READ_HEALTH_MIN_SAMPLE_IOS) + continue; + + WRITE_ONCE(device->health_avg_ns, div64_u64(delta_wait, delta_ios)); + WRITE_ONCE(device->health_check_ios, read_ios); + WRITE_ONCE(device->health_check_wait, read_wait); + } +} +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + +#ifdef CONFIG_BTRFS_READ_POLICIES + static int btrfs_read_preferred(struct btrfs_chunk_map *map, int first, int num_stripes) { for (int index = first; index < first + num_stripes; index++) { @@ -6219,6 +6285,10 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, } #endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + btrfs_update_read_health(map, first, num_stripes); +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + switch (policy) { default: /* Shouldn't happen, just warn and use pid instead of failing */ diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 766af038dc547a..4742d2c6b38500 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -208,6 +208,20 @@ struct btrfs_device { #ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS /* store how often a stripe has been ignored as a read candidate */ atomic64_t stripe_ignored; + + /* + * Cached windowed avg read latency (nsec/io), refreshed from a delta + * against health_check_ios/health_check_wait, not from the lifetime + * total. 0 means "not yet classified this window." + */ + u64 health_avg_ns; + + /* ios[READ]/nsecs[READ] checkpoint at the last health refresh. */ + u64 health_check_ios; + u64 health_check_wait; + + /* jiffies at the last health refresh attempt. */ + unsigned long health_check_jiffies; #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ }; From b62fcfa704e7eb964bf9cd14107e54f4c9acd31b Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Wed, 8 Jul 2026 16:38:51 +0200 Subject: [PATCH 16/19] btrfs: add queue-adaptive read policy Add a new RAID1 read policy, queue-adaptive, selected independently of the existing queue policy. queue picks the mirror with the fewest in-flight requests and uses last_io_age to break equal-depth ties. This works well on healthy devices but can still flap onto a latency-sick device: a stuck or slow device can show a lower in-flight count simply because its last request has not completed yet, so queue repeatedly picks it whenever the healthy device has one extra in-flight request. queue-adaptive keeps queue's in-flight comparison and shared age tie-break as the primary selector, but adds a coarse latency veto. For each read, it compares only the actual current mirror candidates' cached windowed latency averages. A device more than 4x worse than the best current peer is treated as slow for that call. The slow in-flight winner is rejected in favor of the best non-slow candidate only if its in-flight lead is not more than the guard margin. This keeps the latency guard from turning into a hard device ban: a slow mirror can still absorb overflow once the healthier peer's queue backs up enough. When per-device IO stats are enabled, read_stats also reports the age of the last successful health refresh, a health score relative to the lifetime average, how often a device was vetoed by this guard and how often it was still selected as slow because its in-flight lead exceeded the guard margin. These counters are diagnostic only and do not feed back into selection. v2: - Refresh health samples only while queue-adaptive is active, avoiding block-stat and division overhead for all other read policies. The ago field exposes when the retained sample was last refreshed. - Skip missing mirrors before reading health or in-flight state so the policy remains safe on degraded arrays. - Clarify that score compares the current window with the device's own lifetime history and is not part of the peer-relative decision. Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 43 ++++++++++--- fs/btrfs/volumes.c | 146 +++++++++++++++++++++++++++++++++++++++++---- fs/btrfs/volumes.h | 30 +++++++++- 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 6b3e2ca1411b6f..7001f6a924c077 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -11,6 +11,10 @@ #include #include #include +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS +#include +#include +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ #include "messages.h" #include "ctree.h" #include "discard.h" @@ -26,10 +30,6 @@ #include "fs.h" #include "accessors.h" -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS -#include -#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ - /* * Structure name Path * -------------------------------------------------------------------------- @@ -1326,6 +1326,7 @@ static const char *btrfs_read_policy_name[] = { #ifdef CONFIG_BTRFS_READ_POLICIES "round-robin", "queue", + "queue-adaptive", "devid", #endif /* CONFIG_BTRFS_READ_POLICIES */ }; @@ -1342,7 +1343,7 @@ char *btrfs_get_mod_read_policy(void) /* Set perms to 0, disable /sys/module/btrfs/parameter/read_policy interface. */ module_param(read_policy, charp, 0); MODULE_PARM_DESC(read_policy, -"Global read policy: pid (default), round-robin[:], queue, devid[:]"); +"Global read policy: pid (default), round-robin[:], queue, queue-adaptive, devid[:]"); #endif /* CONFIG_BTRFS_READ_POLICIES */ int btrfs_read_policy_to_enum(const char *str, s64 *value_ret) @@ -2246,17 +2247,45 @@ static ssize_t btrfs_devinfo_read_stats_show(struct kobject *kobj, u64 read_wait = part_stat_read(device->bdev, nsecs[READ]); unsigned long read_ios = part_stat_read(device->bdev, ios[READ]); u64 health_avg = READ_ONCE(device->health_avg_ns); + unsigned long health_avg_jiffies = READ_ONCE(device->health_avg_jiffies); u64 avg_wait = 0; + u64 health_ago = 0; + u64 health_score = 0; if (read_wait && read_ios && read_wait >= read_ios) avg_wait = div_u64(read_wait, read_ios); + if (health_avg_jiffies) + health_ago = (jiffies - health_avg_jiffies) / HZ; + /* + * "score" is avg_wait (this device's lifetime avg) relative to + * health_avg (its current short window), as a percentage: ~100 means + * "behaving like its own history", higher means "currently healthier + * than its own history". This mixes two different time bases on + * purpose - it is a quick, human-readable "how is this device doing + * right now compared to itself" signal, which vetoed/overflow alone + * don't give you at a glance. It intentionally does NOT reflect + * queue-adaptive's actual peer-relative veto decision (see + * btrfs_read_queue_adaptive() in volumes.c, which compares + * health_avg_ns only against other *current* mirror candidates, never + * against this device's own lifetime avg) - once the read policies + * are more settled, some of these diagnostic fields may be trimmed or + * split into their own sysfs attributes. + */ + if (health_avg) { + if (avg_wait > U64_MAX / 100) + health_score = U64_MAX; + else + health_score = div64_u64(avg_wait * 100, health_avg); + } return scnprintf(buf, PAGE_SIZE, - "ios %lu wait %llu avg %llu age %llu ignored %llu health avg %llu\n", + "ios %lu wait %llu avg %llu age %llu ignored %llu health avg %llu ago %llu score %llu vetoed %llu overflow %llu\n", read_ios, read_wait, avg_wait, (u64)atomic64_read(&device->last_io_age), (u64)atomic64_read(&device->stripe_ignored), - health_avg); + health_avg, health_ago, health_score, + (u64)atomic64_read(&device->health_vetoed), + (u64)atomic64_read(&device->health_overflow)); } BTRFS_ATTR(devid, read_stats, btrfs_devinfo_read_stats_show); #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 3bf5a4d8e82dc0..2e036d3c1ebac5 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -35,11 +35,9 @@ #include "raid-stripe-tree.h" #if defined(CONFIG_BTRFS_READ_POLICIES) || defined(CONFIG_BTRFS_PER_DEVICE_IO_STATS) +#include #include #endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS -#include -#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ #define BTRFS_BLOCK_GROUP_STRIPE_MASK (BTRFS_BLOCK_GROUP_RAID0 | \ BTRFS_BLOCK_GROUP_RAID10 | \ @@ -6122,9 +6120,8 @@ static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, return best_stripe; } -#endif /* CONFIG_BTRFS_READ_POLICIES */ +#define BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN 2ULL -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS #define BTRFS_READ_HEALTH_RECHECK_INTERVAL HZ #define BTRFS_READ_HEALTH_MIN_SAMPLE_IOS 256ULL @@ -6132,6 +6129,15 @@ static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, * Refresh the cached windowed avg read latency for the current candidates. * The avg is a delta against the previous checkpoint, not a lifetime average, * so unrelated bulk reads can only skew the current window. + * + * Only queue-adaptive (btrfs_read_queue_adaptive(), below) calls this, so it + * lives right above its one caller instead of needing a forward declaration. + * It stays gated by CONFIG_BTRFS_READ_POLICIES alone (this whole block + * already is), matching that caller's own requirement - not the broader + * OR-guard the health_* struct fields use, since sysfs.c's read_stats + * display still needs those fields to exist whenever + * CONFIG_BTRFS_PER_DEVICE_IO_STATS is on, independent of whether + * CONFIG_BTRFS_READ_POLICIES (and therefore queue-adaptive) is enabled. */ static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, int num_stripes) @@ -6179,11 +6185,129 @@ static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, WRITE_ONCE(device->health_avg_ns, div64_u64(delta_wait, delta_ios)); WRITE_ONCE(device->health_check_ios, read_ios); WRITE_ONCE(device->health_check_wait, read_wait); + WRITE_ONCE(device->health_avg_jiffies, jiffies); } } -#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ -#ifdef CONFIG_BTRFS_READ_POLICIES +/* + * btrfs_read_queue_adaptive + * + * Select the stripe with the lowest in-flight request count, exactly + * like btrfs_read_earliest(), with ties broken by device->last_io_age + * (most-overdue device wins) so healthy/idle mirrors still rotate fairly + * instead of collapsing onto the first stripe - the same tie-break + * btrfs_read_earliest() itself now uses via + * btrfs_queue_read_candidate_better(), so this is not a behavioral + * difference from plain queue. + * + * The one behavioral difference from plain queue: candidates are + * classified "slow" fresh, right here, by comparing each candidate's + * cached device->health_avg_ns against the best (lowest) value among + * only the *actual current* stripe candidates - never a cached verdict, + * so it can never be stale from a different mirror pairing on a larger + * pool. If the in-flight winner is not derived-slow, done. If it is, + * it's only trusted when its in-flight lead over the best non-slow + * candidate is MORE than BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN + * requests; otherwise the best non-slow candidate is used instead. This + * is a coarse veto, not the primary selector - on an all-healthy, + * same-tier array, no candidate is ever derived-slow and behavior is + * identical to queue. On a mixed-tier array (e.g. HDD mirrored against + * SSD) the slower device is durably derived-slow and mostly avoided, + * but can still absorb overflow once the faster candidate's queue backs + * up past the guard margin. + */ +static int btrfs_read_queue_adaptive(struct btrfs_fs_info *fs_info, + struct btrfs_chunk_map *map, int first, + int num_stripes) +{ + u64 avg[BTRFS_RAID1_MAX_MIRRORS]; + u64 best_avg = U64_MAX; + u64 sick_threshold; + u64 best_inflight = U64_MAX; + u64 best_age = 0; + int best_stripe = first; + bool best_slow; + u64 best_healthy_inflight = U64_MAX; + u64 best_healthy_age = 0; + int best_healthy_stripe = -1; + + btrfs_update_read_health(map, first, num_stripes); + + /* + * Gather this stripe's actual current candidates' cached avgs and find + * the best among only them - this is the peer-relative derivation, done + * fresh every call. + */ + for (int index = first; index < first + num_stripes; index++) { + struct btrfs_device *device = map->stripes[index].dev; + u64 my_avg; + + if (!device->bdev) { + avg[index - first] = 0; + continue; + } + + my_avg = READ_ONCE(device->health_avg_ns); + avg[index - first] = my_avg; + best_avg = min_not_zero(best_avg, my_avg); + } + + /* + * Overflow-safe threshold: saturate instead of wrapping in the + * practically-impossible case that best_avg is already close to + * U64_MAX; best_avg == U64_MAX (no candidate has data yet) also + * saturates here, so nothing can be derived-slow without data. + */ + if (best_avg == U64_MAX || best_avg > U64_MAX / BTRFS_READ_HEALTH_SICK_MULTIPLIER) + sick_threshold = U64_MAX; + else + sick_threshold = best_avg * BTRFS_READ_HEALTH_SICK_MULTIPLIER; + + for (int index = first; index < first + num_stripes; index++) { + struct btrfs_device *device = map->stripes[index].dev; + u64 in_flight; + u64 age = atomic64_read(&device->last_io_age); + u64 my_avg = avg[index - first]; + bool slow = (my_avg != 0 && my_avg > sick_threshold); + + if (!device->bdev) + continue; + + in_flight = part_in_flight(device->bdev); + if (btrfs_queue_read_candidate_better(in_flight, age, + best_inflight, + best_age)) { + best_inflight = in_flight; + best_age = age; + best_stripe = index; + } + if (!slow && + btrfs_queue_read_candidate_better(in_flight, age, + best_healthy_inflight, + best_healthy_age)) { + best_healthy_inflight = in_flight; + best_healthy_age = age; + best_healthy_stripe = index; + } + } + + best_slow = (avg[best_stripe - first] != 0 && + avg[best_stripe - first] > sick_threshold); + if (!best_slow) + return best_stripe; + if (best_healthy_stripe < 0) + return best_stripe; /* every candidate derived-slow, no alternative */ + if (best_healthy_inflight <= best_inflight + BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN) { +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + atomic64_inc(&map->stripes[best_stripe].dev->health_vetoed); +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + return best_healthy_stripe; /* slow device's lead is not more than the margin */ + } +#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS + atomic64_inc(&map->stripes[best_stripe].dev->health_overflow); +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + return best_stripe; /* slow device's lead is more than the margin - trust it anyway */ +} static int btrfs_read_preferred(struct btrfs_chunk_map *map, int first, int num_stripes) { @@ -6285,10 +6409,6 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, } #endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ -#ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS - btrfs_update_read_health(map, first, num_stripes); -#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ - switch (policy) { default: /* Shouldn't happen, just warn and use pid instead of failing */ @@ -6307,6 +6427,10 @@ static int find_live_mirror(struct btrfs_fs_info *fs_info, preferred_mirror = btrfs_read_earliest(fs_info, map, first, num_stripes); break; + case BTRFS_READ_POLICY_QUEUE_ADAPTIVE: + preferred_mirror = btrfs_read_queue_adaptive(fs_info, map, first, + num_stripes); + break; case BTRFS_READ_POLICY_DEVID: preferred_mirror = btrfs_read_preferred(map, first, num_stripes); break; diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 4742d2c6b38500..4dbeecd74c26a8 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -209,6 +209,14 @@ struct btrfs_device { /* store how often a stripe has been ignored as a read candidate */ atomic64_t stripe_ignored; + /* store how often queue-adaptive rejected this device due to latency */ + atomic64_t health_vetoed; + + /* store how often queue-adaptive still selected this slow device */ + atomic64_t health_overflow; +#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + +#if defined(CONFIG_BTRFS_READ_POLICIES) || defined(CONFIG_BTRFS_PER_DEVICE_IO_STATS) /* * Cached windowed avg read latency (nsec/io), refreshed from a delta * against health_check_ios/health_check_wait, not from the lifetime @@ -222,9 +230,20 @@ struct btrfs_device { /* jiffies at the last health refresh attempt. */ unsigned long health_check_jiffies; -#endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ + + /* jiffies at the last successful health_avg_ns update. */ + unsigned long health_avg_jiffies; +#endif /* CONFIG_BTRFS_READ_POLICIES || CONFIG_BTRFS_PER_DEVICE_IO_STATS */ }; +#ifdef CONFIG_BTRFS_READ_POLICIES +/* + * How many times worse than the best current peer's windowed avg read + * latency counts as anomalous for queue-adaptive. + */ +#define BTRFS_READ_HEALTH_SICK_MULTIPLIER 4ULL +#endif /* CONFIG_BTRFS_READ_POLICIES */ + /* * Block group or device which contains an active swapfile. Used for preventing * unsafe operations while a swapfile is active. @@ -337,6 +356,15 @@ enum btrfs_read_policy { BTRFS_READ_POLICY_RR, /* Read from the device with the least in-flight requests */ BTRFS_READ_POLICY_QUEUE, + /* + * Like QUEUE, but veto picking a device whose cached windowed avg + * read latency is durably worse than its current mirror peers' + * (derived fresh at selection time, never cached as a verdict - + * whether the cause is malfunction or simply a slower device tier, + * e.g. HDD mirrored against SSD), unless its in-flight lead is large + * enough to trust anyway. + */ + BTRFS_READ_POLICY_QUEUE_ADAPTIVE, /* Read from a specific device. */ BTRFS_READ_POLICY_DEVID, #endif /* CONFIG_BTRFS_READ_POLICIES */ From 417c317d7355e815c7b4fab7becba5e1ef2fc320 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Sun, 12 Jul 2026 14:15:19 +0200 Subject: [PATCH 17/19] btrfs: scale adaptive queue guard with latency The fixed queue guard allows a slow mirror to absorb overflow once its in-flight count leads a healthier peer by more than two requests, regardless of their latency difference. This can favor throughput over tail latency when a mirror is substantially slower. Scale the guard with the rounded-up ratio between the slow queue winner and the selected healthy alternative. Clamp it between 2 and 16 requests to preserve the existing minimum while bounding how far load can accumulate on the healthier mirror. Keep the minimum guard when the alternative has no health sample yet. The additional 64-bit division only runs when the normal queue winner has already been classified as slow and a non-slow alternative exists. Signed-off-by: Kai Krakow --- fs/btrfs/volumes.c | 37 +++++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 2e036d3c1ebac5..b702636d5f6c27 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6120,7 +6120,8 @@ static int btrfs_read_earliest(struct btrfs_fs_info *fs_info, return best_stripe; } -#define BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN 2ULL +#define BTRFS_READ_QUEUE_ADAPTIVE_MIN_GUARD 2ULL +#define BTRFS_READ_QUEUE_ADAPTIVE_MAX_GUARD 16ULL #define BTRFS_READ_HEALTH_RECHECK_INTERVAL HZ #define BTRFS_READ_HEALTH_MIN_SAMPLE_IOS 256ULL @@ -6207,14 +6208,14 @@ static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, * so it can never be stale from a different mirror pairing on a larger * pool. If the in-flight winner is not derived-slow, done. If it is, * it's only trusted when its in-flight lead over the best non-slow - * candidate is MORE than BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN - * requests; otherwise the best non-slow candidate is used instead. This - * is a coarse veto, not the primary selector - on an all-healthy, - * same-tier array, no candidate is ever derived-slow and behavior is - * identical to queue. On a mixed-tier array (e.g. HDD mirrored against - * SSD) the slower device is durably derived-slow and mostly avoided, - * but can still absorb overflow once the faster candidate's queue backs - * up past the guard margin. + * candidate is more than their rounded-up latency ratio, clamped to a + * reasonable range. Otherwise the best non-slow candidate is used + * instead. This is a coarse veto, not the primary selector - on an + * all-healthy, same-tier array, no candidate is ever derived-slow and + * behavior is identical to queue. On a mixed-tier array (e.g. HDD + * mirrored against SSD) the slower device is durably derived-slow and + * mostly avoided, but can still absorb overflow once the faster + * candidate's queue backs up past the dynamic guard margin. */ static int btrfs_read_queue_adaptive(struct btrfs_fs_info *fs_info, struct btrfs_chunk_map *map, int first, @@ -6230,6 +6231,8 @@ static int btrfs_read_queue_adaptive(struct btrfs_fs_info *fs_info, u64 best_healthy_inflight = U64_MAX; u64 best_healthy_age = 0; int best_healthy_stripe = -1; + u64 healthy_avg; + u64 guard; btrfs_update_read_health(map, first, num_stripes); @@ -6297,7 +6300,21 @@ static int btrfs_read_queue_adaptive(struct btrfs_fs_info *fs_info, return best_stripe; if (best_healthy_stripe < 0) return best_stripe; /* every candidate derived-slow, no alternative */ - if (best_healthy_inflight <= best_inflight + BTRFS_READ_QUEUE_ADAPTIVE_GUARD_MARGIN) { + + /* + * Scale the guard with the latency difference to favor tail latency. + * An unknown alternative keeps the previous minimum guard behavior. + */ + healthy_avg = avg[best_healthy_stripe - first]; + guard = BTRFS_READ_QUEUE_ADAPTIVE_MIN_GUARD; + + if (healthy_avg != 0) { + guard = DIV64_U64_ROUND_UP(avg[best_stripe - first], healthy_avg); + guard = clamp_t(u64, guard, BTRFS_READ_QUEUE_ADAPTIVE_MIN_GUARD, + BTRFS_READ_QUEUE_ADAPTIVE_MAX_GUARD); + } + + if (best_healthy_inflight - best_inflight <= guard) { #ifdef CONFIG_BTRFS_PER_DEVICE_IO_STATS atomic64_inc(&map->stripes[best_stripe].dev->health_vetoed); #endif /* CONFIG_BTRFS_PER_DEVICE_IO_STATS */ From a9ebcbc6878f9818beebb5edb063c7df1e07f670 Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Thu, 16 Jul 2026 18:57:21 +0200 Subject: [PATCH 18/19] fixup! btrfs: add io read stats per device to devinfo btrfs_devinfo_read_stats_show() calls part_stat_read() on device->bdev unconditionally. A missing mirror on a degraded array has no bdev, so the first read of that device's read_stats file dereferences a NULL block device and crashes. Guard both part_stat_read() calls behind device->bdev and fall back to 0 for read_ios/read_wait when it is absent. The existing zero check before computing avg_wait already turns that into a report with zeroed queue stats instead of a crash, and the remaining fields (age, ignored, health avg/ago, vetoed, overflow) never depended on bdev to begin with, so they keep reporting whatever history the device still has. v2: - Guard part_stat_read() behind device->bdev to fix a NULL-pointer dereference when reading a missing mirror's stats on a degraded array. Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 7001f6a924c077..cf2c4ee9699866 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2244,8 +2244,9 @@ static ssize_t btrfs_devinfo_read_stats_show(struct kobject *kobj, { struct btrfs_device *device = container_of(kobj, struct btrfs_device, devid_kobj); - u64 read_wait = part_stat_read(device->bdev, nsecs[READ]); - unsigned long read_ios = part_stat_read(device->bdev, ios[READ]); + u64 read_wait = device->bdev ? part_stat_read(device->bdev, nsecs[READ]) : 0; + unsigned long read_ios = device->bdev ? + part_stat_read(device->bdev, ios[READ]) : 0; u64 health_avg = READ_ONCE(device->health_avg_ns); unsigned long health_avg_jiffies = READ_ONCE(device->health_avg_jiffies); From cfbba579dfaf3750344df4e8b32affd59e29bd3d Mon Sep 17 00:00:00 2001 From: Kai Krakow Date: Thu, 16 Jul 2026 18:57:47 +0200 Subject: [PATCH 19/19] fixup! btrfs: add read latency health stats to per-device io read stats health_avg_ns, health_check_ios and health_check_wait are plain u64 fields updated via READ_ONCE()/WRITE_ONCE(). On 32-bit architectures a 64-bit load or store is not atomic, so a concurrent reader can observe a torn value made up of half the old sample and half the new one. This is advisory data only (read policy hints and sysfs diagnostics), but it is inconsistent with the other per-device counters in the same struct (last_io_age, stripe_ignored, health_vetoed, health_overflow), which are already atomic64_t. Make all three fields atomic64_t and switch their accessors to atomic64_read()/atomic64_set(), matching the existing counters. On 64-bit architectures this compiles to the same plain load/store as before. health_avg_jiffies and health_check_jiffies stay as they are: they are unsigned long, already word-sized on every architecture, so they cannot tear. v3: - Switch health_avg_ns, health_check_ios and health_check_wait to atomic64_t to avoid torn 64-bit reads/writes on 32-bit architectures, matching the other per-device counters already using atomic64_t. Signed-off-by: Kai Krakow --- fs/btrfs/sysfs.c | 2 +- fs/btrfs/volumes.c | 16 ++++++++-------- fs/btrfs/volumes.h | 9 +++++---- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index cf2c4ee9699866..c61afc502812a0 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -2247,7 +2247,7 @@ static ssize_t btrfs_devinfo_read_stats_show(struct kobject *kobj, u64 read_wait = device->bdev ? part_stat_read(device->bdev, nsecs[READ]) : 0; unsigned long read_ios = device->bdev ? part_stat_read(device->bdev, ios[READ]) : 0; - u64 health_avg = READ_ONCE(device->health_avg_ns); + u64 health_avg = (u64)atomic64_read(&device->health_avg_ns); unsigned long health_avg_jiffies = READ_ONCE(device->health_avg_jiffies); u64 avg_wait = 0; diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index b702636d5f6c27..e0083afde25722 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6168,12 +6168,12 @@ static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, read_ios = part_stat_read(device->bdev, ios[READ]); read_wait = part_stat_read(device->bdev, nsecs[READ]); - checked_ios = READ_ONCE(device->health_check_ios); - checked_wait = READ_ONCE(device->health_check_wait); + checked_ios = atomic64_read(&device->health_check_ios); + checked_wait = atomic64_read(&device->health_check_wait); if (checked_ios == 0 && checked_wait == 0 && read_ios != 0) { - WRITE_ONCE(device->health_check_ios, read_ios); - WRITE_ONCE(device->health_check_wait, read_wait); + atomic64_set(&device->health_check_ios, read_ios); + atomic64_set(&device->health_check_wait, read_wait); continue; } @@ -6183,9 +6183,9 @@ static void btrfs_update_read_health(struct btrfs_chunk_map *map, int first, if (delta_ios < BTRFS_READ_HEALTH_MIN_SAMPLE_IOS) continue; - WRITE_ONCE(device->health_avg_ns, div64_u64(delta_wait, delta_ios)); - WRITE_ONCE(device->health_check_ios, read_ios); - WRITE_ONCE(device->health_check_wait, read_wait); + atomic64_set(&device->health_avg_ns, div64_u64(delta_wait, delta_ios)); + atomic64_set(&device->health_check_ios, read_ios); + atomic64_set(&device->health_check_wait, read_wait); WRITE_ONCE(device->health_avg_jiffies, jiffies); } } @@ -6250,7 +6250,7 @@ static int btrfs_read_queue_adaptive(struct btrfs_fs_info *fs_info, continue; } - my_avg = READ_ONCE(device->health_avg_ns); + my_avg = atomic64_read(&device->health_avg_ns); avg[index - first] = my_avg; best_avg = min_not_zero(best_avg, my_avg); } diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 4dbeecd74c26a8..cdbe30dfc6dad9 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -220,13 +220,14 @@ struct btrfs_device { /* * Cached windowed avg read latency (nsec/io), refreshed from a delta * against health_check_ios/health_check_wait, not from the lifetime - * total. 0 means "not yet classified this window." + * total. 0 means "not yet classified this window." atomic64_t so + * readers on 32-bit architectures cannot observe a torn 64-bit value. */ - u64 health_avg_ns; + atomic64_t health_avg_ns; /* ios[READ]/nsecs[READ] checkpoint at the last health refresh. */ - u64 health_check_ios; - u64 health_check_wait; + atomic64_t health_check_ios; + atomic64_t health_check_wait; /* jiffies at the last health refresh attempt. */ unsigned long health_check_jiffies;