btrfs: stop extent map shrinker if reschedule is needed

The extent map shrinker can be called in a variety of contexts where we
are under memory pressure, and of them is when a task is trying to
allocate memory. For this reason the shrinker is typically called with a
value of struct shrink_control::nr_to_scan that is much smaller than what
we return in the nr_cached_objects callback of struct super_operations
(fs/btrfs/super.c:btrfs_nr_cached_objects()), so that the shrinker does
not take a long time and cause high latencies. However we can still take
a lot of time in the shrinker even for a limited amount of nr_to_scan:

1) When traversing the red black tree that tracks open inodes in a root,
   as for example with millions of open inodes we get a deep tree which
   takes time searching for an inode;

2) Iterating over the extent map tree, which is a red black tree, of an
   inode when doing the rb_next() calls and when removing an extent map
   from the tree, since often that requires rebalancing the red black
   tree;

3) When trying to write lock an inode's extent map tree we may wait for a
   significant amount of time, because there's either another task about
   to do IO and searching for an extent map in the tree or inserting an
   extent map in the tree, and we can have thousands or even millions of
   extent maps for an inode. Furthermore, there can be concurrent calls
   to the shrinker so the lock might be busy simply because there is
   already another task shrinking extent maps for the same inode;

4) We often reschedule if we need to, which further increases latency.

So improve on this by stopping the extent map shrinking code whenever we
need to reschedule and make it skip an inode if we can't immediately lock
its extent map tree.

Reported-by: Mikhail Gavrilov <mikhail.v.gavrilov@gmail.com>
Reported-by: Andrea Gelmini <andrea.gelmini@gmail.com>
Link: https://lore.kernel.org/linux-btrfs/CABXGCsMmmb36ym8hVNGTiU8yfUS_cGvoUmGCcBrGWq9OxTrs+A@mail.gmail.com/
Reviewed-by: Josef Bacik <josef@toxicpanda.com>
Signed-off-by: Filipe Manana <fdmanana@suse.com>
Signed-off-by: David Sterba <dsterba@suse.com>
This commit is contained in:
Filipe Manana 2024-07-08 15:42:44 +01:00 committed by David Sterba
parent 68a3ebd18b
commit b3ebb9b7e9

View file

@ -1057,7 +1057,18 @@ static long btrfs_scan_inode(struct btrfs_inode *inode, long *scanned, long nr_t
if (!down_read_trylock(&inode->i_mmap_lock))
return 0;
write_lock(&tree->lock);
/*
* We want to be fast because we can be called from any path trying to
* allocate memory, so if the lock is busy we don't want to spend time
* waiting for it - either some task is about to do IO for the inode or
* we may have another task shrinking extent maps, here in this code, so
* skip this inode.
*/
if (!write_trylock(&tree->lock)) {
up_read(&inode->i_mmap_lock);
return 0;
}
node = rb_first_cached(&tree->map);
while (node) {
struct extent_map *em;
@ -1089,12 +1100,14 @@ static long btrfs_scan_inode(struct btrfs_inode *inode, long *scanned, long nr_t
break;
/*
* Restart if we had to reschedule, and any extent maps that were
* pinned before may have become unpinned after we released the
* lock and took it again.
* Stop if we need to reschedule or there's contention on the
* lock. This is to avoid slowing other tasks trying to take the
* lock and because the shrinker might be called during a memory
* allocation path and we want to avoid taking a very long time
* and slowing down all sorts of tasks.
*/
if (cond_resched_rwlock_write(&tree->lock))
node = rb_first_cached(&tree->map);
if (need_resched() || rwlock_needbreak(&tree->lock))
break;
}
write_unlock(&tree->lock);
up_read(&inode->i_mmap_lock);
@ -1120,7 +1133,13 @@ static long btrfs_scan_root(struct btrfs_root *root, long *scanned, long nr_to_s
if (*scanned >= nr_to_scan)
break;
cond_resched();
/*
* We may be called from memory allocation paths, so we don't
* want to take too much time and slowdown tasks.
*/
if (need_resched())
break;
inode = btrfs_find_first_inode(root, min_ino);
}
@ -1159,7 +1178,11 @@ long btrfs_free_extent_maps(struct btrfs_fs_info *fs_info, long nr_to_scan)
trace_btrfs_extent_map_shrinker_scan_enter(fs_info, nr_to_scan, nr);
}
while (scanned < nr_to_scan) {
/*
* We may be called from memory allocation paths, so we don't want to
* take too much time and slowdown tasks, so stop if we need reschedule.
*/
while (scanned < nr_to_scan && !need_resched()) {
struct btrfs_root *root;
unsigned long count;