diff options
| author | Boris Burkov <boris@bur.io> | 2026-06-15 10:40:59 -0700 |
|---|---|---|
| committer | David Sterba <dsterba@suse.com> | 2026-08-07 19:16:28 +0200 |
| commit | 030d3514c6bb289cd3557a74b8d560dc0a26d8f8 (patch) | |
| tree | cbc153cd9f9fb11f5f9fcf4e087e903519007509 /tools/perf/scripts/python/parallel-perf.py | |
| parent | 0a9c35d3040d5aa6bb449b2f1e206cc0ac094343 (diff) | |
btrfs: release extent lock per folio in readahead
In Meta production, we have observed a large number of hosts running
kernels newer than 6.13 which hit hung tasks on
btrfs_read_folio()->lock_extents_for_read(). Looking through the history
in this codepath reveals an interesting history.
in 6.12, we merged
commit ac325fc2aad5 ("btrfs: do not hold the extent lock for entire read")
which holds the extent lock very narrowly while looking up the
extent_map. However, this proved to introduce a serious race with DIO
writes which was fixed in 6.14 with
commit acc18e1c1d8c0 ("btrfs: fix stale page cache after race between readahead and direct IO write")
That latter fix subtly changed the extent unlock point from the pre-6.12
regime. In 6.11, each read endio unlocked the extent it finished
reading, but in 6.14, the extent is locked/unlocked as a unit around the
entire readahead loop, while the individual folios are still unlocked as
the endios finish. This is mostly the same behavior, as all successful
reads will populate the page cache, so subsequent reads won't enter
btrfs and hit the extent lock. But in the case where the readahead
fails, perhaps because of a memory allocation failure doing compressed
reads, the page will not be brought up to date and a later read of an
overlapping range *will* block on the extent lock.
Why is this a problem?
On sufficiently large loaded systems, I have observed that direct
reclaim can run for minutes. Given that, consider two tasks on such a
system reading an overlapping range of a compressed file:
Task 1 locks the whole range and starts to read. Some allocation for
the compressed read for folio F fails and we carry on while holding the
extent lock for the full range.
Task 2 wants to read F, which is not uptodate and in page cache, so it
blocks on the extent lock held by Task 1.
Task 1 keeps getting stuck in direct reclaim (likely, we already
supposed an allocation failure above)
Task 2 stays blocked on the extent lock the whole time.
If you consider the effects of readahead_expand and imagine a file with
a 128k compressed extent followed by many smaller compressed extents,
you can imagine that the expanded window will result in subsequent reads
hitting many extents (128k/4k = 32) per lock window in the worst case.
The system likeley wouldn't be all that healthy anyway, so this is
likely not a critical improvement, but it does alleviate this one source
of stress and one thread's slowdown escalating to others.
To bring this behavior back to the old model, we should unlock the
extent at each loop of the readahead loop rather than in one shot at the
end. This allows such overlapping reads to proceed as they should.
Writes are fine because either the page has already been read and has an
appropriate state in the page cache to be invalidated (or not uptodate)
or it is still-to-be-read and the extent lock is still held protecting
it.
Reviewed-by: Filipe Manana <fdmanana@suse.com>
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Boris Burkov <boris@bur.io>
Signed-off-by: David Sterba <dsterba@suse.com>
Diffstat (limited to 'tools/perf/scripts/python/parallel-perf.py')
0 files changed, 0 insertions, 0 deletions
