Skip to content

L2ARC: Prune L2ARC-only headers over l2arc_meta_percent - #19383

Open
adamguo0 wants to merge 1 commit into
openzfs:masterfrom
adamguo0:l2arc_header_pruning
Open

adamguo0 wants to merge 1 commit into
openzfs:masterfrom
adamguo0:l2arc_header_pruning

Conversation

@adamguo0

Copy link
Copy Markdown

Motivation and Context

This change addresses #11702. Two prior attempts are #1612 and #1936, more on that later.

l2arc_meta_percent limits the memory used by L2ARC-only headers. Once the headers exceed it, the feed thread stops writing, but the headers that already exist remain. Old L2ARC entries are normally evicted as the feed thread advances the write hand, so eviction stops with the writes.

The L2ARC header limit is a fraction of arc_c, but the L2ARC headers are not evictable by memory pressure. Before the ARC first comes under memory pressure, the limit is a fraction of arc_c_max instead, so the headers can grow to l2arc_meta_percent of arc_c_max. When memory pressure later lowers the ARC target, the headers exceed the limit and the feed thread stops writing. Importing a pool with an L2ARC can also drive header memory usage above l2arc_meta_percent. Once this happens, ZFS can neither evict the headers nor cache new data in the L2ARC until the ARC target grows enough to fit them, which may never happen.

The fundamental problem is that the L2ARC header footprint can increase with arc_c, but once the headers are there they cannot decrease with arc_c. We get into a situation where system memory is permanently occupied by L2ARC headers. There is no memory headroom for (1) the regular ARC, or (2) feeding the L2ARC. Not only is there less/no room for ARC data, the L2ARC itself gradually becomes stale over time. The only mitigation is to delete L2ARC data, make more memory available to ARC, or remove/resize the L2ARC altogether. It would be much more preferable for ZFS to under-utilize the available L2ARC rather than get into this pathological low-performance state.

Description

This commit introduces L2ARC header pruning. When the headers exceed the limit, prune the oldest L2ARC-only headers on the device until they fit, so that writes resume. Headers that still have an L1 part do not count toward the limit and are kept. That includes ghost headers, whose L2ARC copy may be the only cached copy of recently evicted data. A per-device marker keeps the scan position across lock drops and feed iterations, so kept headers are not rescanned on every pass. General memory pressure alone does not prune, since dropping headers frees little memory compared to the cache it discards.

Pruning only forgets in-memory entries. The device hands and log blocks are untouched, so a later import may restore the entries, subject to the same limit.

In 2013, #1612 and #1936 proposed reclaiming header memory by advancing the write hand with l2arc_evict() until enough headers were freed. Both were carried into #2110, and the L2ARC patches were removed before it merged because they needed more testing. This change differs in three ways.

  1. This change removes only L2ARC-only headers, while evicting by device address also drops the L2ARC copies of buffers still in the ARC. The latter does not help with memory pressure and makes the l2arc feeder repeat work.
  2. This change leaves the write hand and log blocks alone, so a later import can restore pruned entries.
  3. This change enforces the existing l2arc_meta_percent limit and never prunes on general memory pressure, while Arc+l2arc #1612 also reclaimed when metadata exceeded its limit and Limit L2ARC headers and don't reclaim from ARC when over "arc_meta_used" #1936 added a separate fixed limit. This change does not touch the ARC evictor, only the L2ARC feeder.

The new l2arc_hdr_prune_budget tunable bounds the headers each device examines per feed iteration, including those it keeps. Setting it to 0 disables pruning. The new l2arc_hdr_prune_batch_limit tunable sets how many headers pruning examines each time it takes a device's buffer list lock. The new l2_hdr_pruned arcstat counts pruned headers, and zarcsummary reports it as "Pruned headers".

How Has This Been Tested?

I added ZTS tests that exercise the new functionality to the l2arc suite, and ran the l2arc suite on an Amazon Linux 2023, kernel 6.12, aarch64 (EC2 m7g.4xlarge) machine.

I also built ZFS, created a pool, and messed around manually with l2arc_meta_percent and the new parameters to verify that (1) pruning takes place, (2) the new arcstats work, and (3) after lifting l2arc_meta_percent, l2arc feed resumes.

I also ran the QEMU CI test suite on Ubuntu 24.04 and FreeBSD 15.1 x86_64 virtual machines.

Types of Changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Performance enhancement (non-breaking change which improves efficiency)
  • Code cleanup (non-breaking change which makes code smaller or more readable)
  • Quality assurance (non-breaking change which makes the code more robust against bugs)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Library ABI change (libzfs, libzfs_core, libnvpair and libzfsbootenv)
  • Documentation (a change to man pages or other documentation)

Checklist

@adamguo0
adamguo0 force-pushed the l2arc_header_pruning branch from 13f0422 to 2ddf4a0 Compare October 14, 2026 14:41
l2arc_meta_percent limits the memory used by L2ARC-only headers.  Once
the headers exceed it, the feed thread stops writing, but the headers
that already exist remain.  Old L2ARC entries are normally evicted as
the feed thread advances the write hand, so eviction stops with the
writes.

The L2ARC header limit is a fraction of arc_c, but the L2ARC headers are
not evictable by memory pressure.  Before the ARC first comes under
memory pressure, the limit is a fraction of arc_c_max instead, so the
headers can grow to l2arc_meta_percent of arc_c_max.  When memory
pressure later lowers the ARC target, the headers exceed the limit and
the feed thread stops writing.  Importing a pool with an L2ARC can also
drive header memory usage above l2arc_meta_percent.  Once this happens,
ZFS can neither evict the headers nor cache new data in the L2ARC until
the ARC target grows enough to fit them, which may never happen.

This commit introduces L2ARC header pruning.  When the headers exceed
the limit, prune the oldest L2ARC-only headers on the device until they
fit, so that writes resume.  Headers that still have an L1 part do not
count toward the limit and are kept.  That includes ghost headers, whose
L2ARC copy may be the only cached copy of recently evicted data.  A
per-device marker keeps the scan position across lock drops and feed
iterations, so kept headers are not rescanned on every pass.  General
memory pressure alone does not prune, since dropping headers frees
little memory compared to the cache it discards.

Pruning only forgets in-memory entries.  The device hands and log
blocks are untouched, so a later import may restore the entries,
subject to the same limit.

The new l2arc_hdr_prune_budget tunable bounds the headers each device
examines per feed iteration, including those it keeps.  Setting it to 0
disables pruning.  The new l2arc_hdr_prune_batch_limit tunable sets how
many headers pruning examines each time it takes a device's buffer list
lock.  The new l2_hdr_pruned arcstat counts pruned headers, and
zarcsummary reports it as "Pruned headers".

Signed-off-by: Adam Guo <adamguo@amazon.com>
@adamguo0
adamguo0 force-pushed the l2arc_header_pruning branch from 2ddf4a0 to 202ce83 Compare October 15, 2026 14:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants