Re: [PATCH v3] ext4: cache full extents during mapping lookup
Jan Kara <[email protected]> Tue, 28 Jul 2026 22:25:54 +0200
| Newsgroups | org.kernel.vger.linux-ext4 |
|---|---|
| Message-ID | <f4prqhixlrwwz3oiphmzrzy6gzibbjtwhclef7muv3slcx2dam@fxv5rmyoaxsj> |
On Thu 23-07-26 20:43:26, Fengnan Chang wrote: > The extent status (ES) tree and extent tree leaf buffers are reclaimed > independently. An ES entry can be reclaimed while the leaf buffer remains > cached with BH_Verified set. > > When this happens, ext4_es_lookup_extent() misses. Since the leaf buffer > is already verified, __read_extent_tree_block() returns it without calling > ext4_cache_extents(). ext4_ext_map_blocks() returns the part of the extent > covered by the request, and ext4_map_query_blocks() caches only that range. > Reads to other blocks in the same extent then keep missing the ES tree and > walking the extent tree. > > Cache the full extent in ext4_ext_map_blocks() before the returned mapping > is limited to the requested range. Add an optional read-side lookup to > ext4_es_cache_extent(). > > Do not do this when ext4_ext_map_blocks() is called with > EXT4_GET_BLOCKS_CREATE or EXT4_EX_NOCACHE. NOCACHE paths can change extent > mappings, so caching ranges outside the request can leave stale ES entries. > Cache both written and unwritten extents, as ext4_cache_extents() does. > > Tested with fio 4K random direct reads using libaio at iodepth 128 on a > 128 GiB file with 208 on-disk extents. The results are averages of three > 10-second runs: > > before reclaim after reclaim > unpatched 419.6k IOPS 274.6k IOPS > patched 426.1k IOPS 423.7k IOPS > > Average completion latency went from 303.02 to 462.78 us without the patch, > and from 298.36 to 300.07 us with the patch. > > No latency regression was seen in A/B test. > > Suggested-by: Jan Kara <[email protected]> > Suggested-by: Zhang Yi <[email protected]> > Reviewed-by: Zhang Yi <[email protected]> > Link: https://lore.kernel.org/r/[email protected] > Signed-off-by: Fengnan Chang <[email protected]> Hum, you have taken an easy way out :) I was hoping to get rid of ext4_es_cache_extent() calls in ext4_map_query_blocks_next_in_leaf(), ext4_map_query_blocks(), and perhaps more places by a strategically placed calls in ext4_ext_map_blocks() and ext4_ind_map_blocks(). But this patch fixes your problem without adding too much technical debt so I'm fine with that. We can cleanup the ext4_es_cache_extent() calls later. The patch looks good to me so feel free to add: Reviewed-by: Jan Kara <[email protected]> Honza > --- > fs/ext4/extents.c | 17 +++++++++++++---- > fs/ext4/extents_status.c | 17 ++++++++++++++++- > fs/ext4/extents_status.h | 2 +- > fs/ext4/inode.c | 15 +++++++++++---- > 4 files changed, 41 insertions(+), 10 deletions(-) > > diff --git a/fs/ext4/extents.c b/fs/ext4/extents.c > index 91c97af64b317..85e213761a18d 100644 > --- a/fs/ext4/extents.c > +++ b/fs/ext4/extents.c > @@ -537,12 +537,12 @@ static void ext4_cache_extents(struct inode *inode, > > if (prev && (prev != lblk)) > ext4_es_cache_extent(inode, prev, lblk - prev, ~0, > - EXTENT_STATUS_HOLE); > + EXTENT_STATUS_HOLE, false); > > if (ext4_ext_is_unwritten(ex)) > status = EXTENT_STATUS_UNWRITTEN; > ext4_es_cache_extent(inode, lblk, len, > - ext4_ext_pblock(ex), status); > + ext4_ext_pblock(ex), status, false); > prev = lblk + len; > } > } > @@ -4239,7 +4239,8 @@ static ext4_lblk_t ext4_ext_determine_insert_hole(struct inode *inode, > insert_hole: > /* Put just found gap into cache to speed up subsequent requests */ > ext_debug(inode, " -> %u:%u\n", hole_start, len); > - ext4_es_cache_extent(inode, hole_start, len, ~0, EXTENT_STATUS_HOLE); > + ext4_es_cache_extent(inode, hole_start, len, ~0, EXTENT_STATUS_HOLE, > + false); > > /* Update hole_len to reflect hole size after lblk */ > if (hole_start != lblk) > @@ -4310,13 +4311,15 @@ int ext4_ext_map_blocks(handle_t *handle, struct inode *inode, > ext4_lblk_t ee_block = le32_to_cpu(ex->ee_block); > ext4_fsblk_t ee_start = ext4_ext_pblock(ex); > unsigned short ee_len; > - > + unsigned int status; > > /* > * unwritten extents are treated as holes, except that > * we split out initialized portions during a write. > */ > ee_len = ext4_ext_get_actual_len(ex); > + status = ext4_ext_is_unwritten(ex) ? > + EXTENT_STATUS_UNWRITTEN : EXTENT_STATUS_WRITTEN; > > trace_ext4_ext_show_extent(inode, ee_block, ee_start, ee_len); > > @@ -4328,6 +4331,12 @@ int ext4_ext_map_blocks(handle_t *handle, struct inode *inode, > ext_debug(inode, "%u fit into %u:%d -> %llu\n", > map->m_lblk, ee_block, ee_len, newblock); > > + if (!(flags & (EXT4_GET_BLOCKS_CREATE | > + EXT4_EX_NOCACHE))) > + ext4_es_cache_extent(inode, ee_block, ee_len, > + ee_start, status, > + true); > + > /* > * If the extent is initialized check whether the > * caller wants to convert it to unwritten. > diff --git a/fs/ext4/extents_status.c b/fs/ext4/extents_status.c > index 6e4a191e82191..5a03878b06160 100644 > --- a/fs/ext4/extents_status.c > +++ b/fs/ext4/extents_status.c > @@ -1023,7 +1023,7 @@ void ext4_es_insert_extent(struct inode *inode, ext4_lblk_t lblk, > */ > void ext4_es_cache_extent(struct inode *inode, ext4_lblk_t lblk, > ext4_lblk_t len, ext4_fsblk_t pblk, > - unsigned int status) > + unsigned int status, bool pre_search) > { > struct extent_status *es; > struct extent_status chkes, newes; > @@ -1043,6 +1043,21 @@ void ext4_es_cache_extent(struct inode *inode, ext4_lblk_t lblk, > > BUG_ON(end < lblk); > > + /* > + * Avoid taking i_es_lock for writing if the entire extent is already > + * cached. The write-locked search below rechecks after a miss. > + */ > + if (pre_search) { > + read_lock(&EXT4_I(inode)->i_es_lock); > + es = __es_tree_search(&EXT4_I(inode)->i_es_tree.root, lblk); > + if (es && es->es_lblk <= lblk && ext4_es_end(es) >= end && > + !__es_check_extent_status(es, status, NULL)) { > + read_unlock(&EXT4_I(inode)->i_es_lock); > + return; > + } > + read_unlock(&EXT4_I(inode)->i_es_lock); > + } > + > write_lock(&EXT4_I(inode)->i_es_lock); > es = __es_tree_search(&EXT4_I(inode)->i_es_tree.root, lblk); > if (es && es->es_lblk <= end) { > diff --git a/fs/ext4/extents_status.h b/fs/ext4/extents_status.h > index f3396cf32b446..c2da72e3c82ba 100644 > --- a/fs/ext4/extents_status.h > +++ b/fs/ext4/extents_status.h > @@ -139,7 +139,7 @@ extern void ext4_es_insert_extent(struct inode *inode, ext4_lblk_t lblk, > bool delalloc_reserve_used); > extern void ext4_es_cache_extent(struct inode *inode, ext4_lblk_t lblk, > ext4_lblk_t len, ext4_fsblk_t pblk, > - unsigned int status); > + unsigned int status, bool pre_search); > extern void ext4_es_remove_extent(struct inode *inode, ext4_lblk_t lblk, > ext4_lblk_t len); > extern void ext4_es_find_extent_range(struct inode *inode, > diff --git a/fs/ext4/inode.c b/fs/ext4/inode.c > index ce99807c5f5b2..d43266c489785 100644 > --- a/fs/ext4/inode.c > +++ b/fs/ext4/inode.c > @@ -523,7 +523,7 @@ static int ext4_map_query_blocks_next_in_leaf(handle_t *handle, > > if (retval <= 0) { > ext4_es_cache_extent(inode, map->m_lblk, map->m_len, > - map->m_pblk, status); > + map->m_pblk, status, true); > return map->m_len; > } > > @@ -546,11 +546,11 @@ static int ext4_map_query_blocks_next_in_leaf(handle_t *handle, > status == status2) { > ext4_es_cache_extent(inode, map->m_lblk, > map->m_len + map2.m_len, map->m_pblk, > - status); > + status, true); > map->m_len += map2.m_len; > } else { > ext4_es_cache_extent(inode, map->m_lblk, map->m_len, > - map->m_pblk, status); > + map->m_pblk, status, true); > } > > return map->m_len; > @@ -562,6 +562,13 @@ int ext4_map_query_blocks(handle_t *handle, struct inode *inode, > unsigned int status; > int retval; > unsigned int orig_mlen = map->m_len; > + /* > + * ext4_ext_map_blocks() may have already cached the full extent, so > + * search first to avoid taking i_es_lock for writing. The indirect > + * path does not prepopulate the ES tree after the initial lookup miss. > + */ > + bool pre_search = ext4_test_inode_flag(inode, > + EXT4_INODE_EXTENTS); > > flags &= EXT4_EX_QUERY_FILTER; > if (ext4_test_inode_flag(inode, EXT4_INODE_EXTENTS)) > @@ -593,7 +600,7 @@ int ext4_map_query_blocks(handle_t *handle, struct inode *inode, > status = map->m_flags & EXT4_MAP_UNWRITTEN ? > EXTENT_STATUS_UNWRITTEN : EXTENT_STATUS_WRITTEN; > ext4_es_cache_extent(inode, map->m_lblk, map->m_len, > - map->m_pblk, status); > + map->m_pblk, status, pre_search); > } else { > retval = ext4_map_query_blocks_next_in_leaf(handle, inode, map, > orig_mlen); > -- > 2.39.5 (Apple Git-154) -- Jan Kara <[email protected]> SUSE Labs, CR