Re: [RFC PATCH v3 0/2] ext4: speed up fast commit on random writes
From: Andreas Dilger
Date: Wed Jul 22 2026 - 03:28:05 EST
On Jul 21, 2026, at 18:59, Daejun Park <daejun7.park@xxxxxxxxxxx> wrote:
>
> Fast commit is meant to make fsync cheap, but on random-write workloads it
> defeats itself. ext4 still tracks a single coalesced [min,max] logical range
> per inode (i_fc_lblk_start/len). When an inode is dirtied at several disjoint
> offsets between two commits, that span widens to cover them all, and at commit
> time ext4_fc_snapshot_inode_data() walks the whole span through the extent
> status tree -- emitting an ADD_RANGE per mapped segment and a DEL_RANGE per
> hole inside it. For scattered writes that is hundreds to thousands of ranges
> even though only a handful of regions were actually modified.
>
> The recently merged fast-commit snapshot work did not change this: it caps a
> snapshot at EXT4_FC_SNAPSHOT_MAX_RANGES (2048) and fails over to a full commit
> when the span exceeds it. Measured on dev (7.1.0-rc4) with a sparse
> random-write workload (1 GiB span, R disjoint 4 KiB writes per fsync, 300
> fsyncs):
>
> R=16 regions/fsync: ranges/commit 1095, full-commit fallback 76%
> (snap_fail_ranges_cap on 226 of 300 fsyncs)
>
> Fast commit barely functions on this workload.
>
> This series tracks the actually-modified disjoint ranges instead of one span,
> and snapshots only those:
>
> 1/2 replaces the single [min,max] range with a small, bounded set of sorted,
> disjoint ranges (up to EXT4_FC_MAX_RANGES = 16; the two closest are
> merged on overflow, so the worst case degrades to the old single-span
> behaviour). ext4_fc_snapshot_inode_data() then walks only the tracked
> ranges. The on-disk TLV format is unchanged.
>
> 2/2 allocates the range array lazily: the first range stays inline, the
> array is allocated only when a second disjoint range appears, and on an
> allocation failure we fall back to the inline single range. Per-inode
> fast-commit footprint stays ~20 bytes.
>
> Result on the same workload (dev, patched):
>
> R=16: ranges/commit 1095 -> 16, fallback 76% -> 0.7%,
> snap_fail_ranges_cap 226 -> 0
>
> i.e. the over-logging and the fallback storm are gone when the number of dirty
> regions is within the bound.
>
> Honest limitation: when the number of disjoint regions per fsync exceeds
> EXT4_FC_MAX_RANGES (e.g. R=64 with the bound at 16), the overflow merge
> re-coalesces and the win largely disappears (fallback 95% -> 87%). A larger or
> tunable bound recovers it -- see the open question below.
>
> Testing (on dev, patched):
> - crash recovery: deterministic writes + fsync, kill -9 QEMU (power loss),
> reboot -> fast-commit replay -> verify every fsync'd block, e2fsck -fn.
> 9600/9600 blocks verified, 0 mismatch, e2fsck clean. Run with R=64, so the
> overflow-merge path is exercised.
> - ext4/generic fast-commit xfstests (ext4/044 ext4/045 generic/455 456 457
> 470 482): ext4/044, ext4/045, generic/456 pass; generic/457, 470 skip
> (reflink/dax unsupported on ext4); generic/455 fails identically on
> unpatched dev (pre-existing, patch-unrelated); generic/482's single failure
> in a combined run did not reproduce (3/3 pass in isolation on the patched
> kernel).
> - on-disk format unchanged, so e2fsprogs needs no update.
> - both patches are checkpatch --strict clean.
>
> Posted as RFC -- this sits right on top of the new snapshot code and I would
> like direction before a non-RFC submission:
>
> - Is tracking disjoint ranges in-core (this series) the right shape, or would
> you rather the snapshot side skip holes/unmodified extents some other way?
> - EXT4_FC_MAX_RANGES is fixed at 16. The R>bound case above wants it larger
> or tunable (sysfs/mount). Preference?
> - The array is allocated under i_fc_lock with GFP_ATOMIC, falling back to the
> inline range on failure. Acceptable, or move the allocation outside the
> lock?
> - Overflow merges the two ranges with the smallest gap -- acceptable
> heuristic?
Have you looked at implementing a more dynamic structure like another
extent status tree, rbtree, xarray or or similar instead of having a
fixed-size array? At *some* point the number of random blocks in the
fast commit will be so large that there is no value to tracking them
separately anymore, but the fixed 16-range limit is clearly too small
based on your testing. It would be useful to do some testing of how
many separate ranges gets worse than the fallback.
Getting this tuning right is always difficult, and will depend on the
application workload, IO size, the storage bandwidth/latency, etc.
Having a dynamic limit is much better than a compile-time constant.
A runtime tunable is the minimum, possibly storing one range directly
in the inode, and using that as a union with the pointer to an allocated
array of ranges. Something like the following that allows a range, with
an inode flag whether the union holds a single range or allocated array:
union {
struct ext4_fc_lblk_range i_fc_range;
struct {
unsigned int r_count;
struct ext4_fc_lblk_range *r_ranges;
} i_fc_range_array;
};
Better would be a union with the root of a dynamic range tree. It is
better to re-use some existing kernel functionality for this instead
of creating yet another tree implementation.
Cheers, Andreas
> Changes since v2 [1]:
> - Evaluate the range adjacency bounds in 64 bits to remove a latent
> ext4_lblk_t (u32) overflow. The largest valid lblk is
> EXT_MAX_BLOCKS - 1 (the shrink path in ext4_setattr() passes it; the data
> path is bounded by the m_lblk >= EXT_MAX_BLOCKS check), so "end + 1" and a
> range's "start + len" can reach EXT_MAX_BLOCKS. These were computed in u32
> in ext4_fc_range_add() (1/2) and __track_range() (2/2). On reachable inputs
> they stop one short of wrapping, so there is no behavioural change, but the
> arithmetic is now done in u64 so it cannot wrap regardless of caller --
> matching ext4_fc_snapshot_lblk_range(), which already used 64-bit bounds.
> No other site in the series shares this pattern (audited).
>
> Changes since v1 [2]:
> - Rebased from v6.17-rc3 onto ext4.git dev; re-implemented on top of the
> merged fast-commit snapshot model (v1 targeted the old
> ext4_fc_write_inode_data(), which no longer exists).
> - Motivation now uses the in-tree snap_fail_ranges_cap counter.
> - From and Signed-off-by use the same identity.
> - Reposted as RFC.
>
> [1] v2: https://lore.kernel.org/linux-ext4/20260623082331epcms2p4798e9d26b06f9a005bcca7b2edf395d3@epcms2p4/
> [2] v1: https://lore.kernel.org/linux-ext4/20260611044733epcms2p38013ae683a283555526f70e4eab6d2a9@epcms2p3/
>
> Daejun Park (2):
> ext4: fast commit: track disjoint modified ranges per inode
> ext4: fast commit: allocate the range array lazily
>
> fs/ext4/ext4.h | 42 ++++++--
> fs/ext4/fast_commit.c | 231 ++++++++++++++++++++++++++++++++++++------
> fs/ext4/super.c | 1 +
> 3 files changed, 234 insertions(+), 40 deletions(-)
>
>
> base-commit: c143957520c6c9b5cd72e0de8b52b814f0c576fe
> --
> 2.43.0
Cheers, Andreas