Skip to content

added dbuf_cache_extra to push arc target size up to support direct I/O - #19085

Open
tiehexue wants to merge 4 commits into
openzfs:masterfrom
tiehexue:break-link-dbuf-arc-cache-size
Open

tiehexue wants to merge 4 commits into
openzfs:masterfrom
tiehexue:break-link-dbuf-arc-cache-size

Conversation

@tiehexue

@tiehexue tiehexue commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

While working on #18902 and #19076 , how to manage dbuf cache size is discussed, and this PR does the work.

As @mkhllr found that in direct I/O or cache-off I/O, arc target size may not increase, and dbuf_cache_shift even set to 0, the max dbuf cache is align to arc target size. However, we should argue, to maximize IOPS in direct I/O, especially read, the dbuf that contains L1 indirect blocks should dominate arc sizing, and chase to arc_c_max if necessary. This PR introduce a new dbuf_cache_extra variable to make the dbuf target increasing as needed.

The whole change is "off" I/O path, so, there should not be performance regression.

Description

dbuf_cache_shift is added and updated since 2016 by commit d3c2ae1, e71cade and de4f8d5. It is well designed, however, in direct I/O or cache-off I/O, arc may not grow, arc target size is not growing too. Then, in high IOPS, dbuf cache will be in a busy evicting cycle. To ensure dbuf not starving, an extra is added while arc current size is small or dbuf cache dominates arc size. With this extra increased in arc recap thread, and downgrade too based "trimming" signal of dbuf stats.

I tried a lot of ways, include breaking the link between dbuf and arc, however this would introduce a lot more code change for accounting etc. Also, for normal arc I/O path, the binding of dbuf cache and arc target is still a very sound design. The new dbuf_cache_extra just make things work and would decay after a direct I/O peek streaming.

In a word, this PR is very similar like de4f8d5, but accounting for direct I/O.

I copied related commit message below for reviewers:

OpenZFS 6950 - ARC should cache compressed data

Authored by: George Wilson <george.wilson@delphix.com>
Reviewed by: Prakash Surya <prakash.surya@delphix.com>
Reviewed by: Dan Kimmel <dan.kimmel@delphix.com>
Reviewed by: Matt Ahrens <mahrens@delphix.com>
Reviewed by: Paul Dagnelie <pcd@delphix.com>
Reviewed by: Tom Caputi <tcaputi@datto.com>
Reviewed by: Brian Behlendorf <behlendorf1@llnl.gov>
Ported by: David Quigley <david.quigley@intel.com>

This review covers the reading and writing of compressed arc headers, sharing
data between the arc_hdr_t and the arc_buf_t, and the implementation of a new
dbuf cache to keep frequently access data uncompressed.

I've added a new member to l1 arc hdr called b_pdata. The b_pdata always hangs
off the arc_buf_hdr_t (if an L1 hdr is in use) and points to the physical block
for that DVA. The physical block may or may not be compressed. If compressed
arc is enabled and the block on-disk is compressed, then the b_pdata will match
the block on-disk and remain compressed in memory. If the block on disk is not
compressed, then neither will the b_pdata. Lastly, if compressed arc is
disabled, then b_pdata will always be an uncompressed version of the on-disk
block.

Typically the arc will cache only the arc_buf_hdr_t and will aggressively evict
any arc_buf_t's that are no longer referenced. This means that the arc will
primarily have compressed blocks as the arc_buf_t's are considered overhead and
are always uncompressed. When a consumer reads a block we first look to see if
the arc_buf_hdr_t is cached. If the hdr is cached then we allocate a new
arc_buf_t and decompress the b_pdata contents into the arc_buf_t's b_data. If
the hdr already has a arc_buf_t, then we will allocate an additional arc_buf_t
and bcopy the uncompressed contents from the first arc_buf_t to the new one.

Writing to the compressed arc requires that we first discard the b_pdata since
the physical block is about to be rewritten. The new data contents will be
passed in via an arc_buf_t (uncompressed) and during the I/O pipeline stages we
will copy the physical block contents to a newly allocated b_pdata.

When an l2arc is inuse it will also take advantage of the b_pdata. Now the
l2arc will always write the contents of b_pdata to the l2arc. This means that
when compressed arc is enabled that the l2arc blocks are identical to those
stored in the main data pool. This provides a significant advantage since we
can leverage the bp's checksum when reading from the l2arc to determine if the
contents are valid. If the compressed arc is disabled, then we must first
transform the read block to look like the physical block in the main data pool
before comparing the checksum and determining it's valid.

OpenZFS-issue: https://www.illumos.org/issues/6950
OpenZFS-commit: https://github.com/openzfs/openzfs/commit/7fc10f0
Issue #5078
提交:
d3c2ae1c0806b183a315e3d43cc8018cfdca79b5 [d3c2ae1c08]
父级:
b8eb3c4e3cbdf284087ea0eda58a2681ab3e8e11
作者:
George Wilson <george.wilson@delphix.com>
日期:
2016年6月2日 GMT+8 12:04:53
提交者:
Brian Behlendorf <behlendorf1@llnl.gov>
提交日期:
2016年9月14日 GMT+8 00:58:33

Scale the dbuf cache with arc_c

Commit d3c2ae1 introduced a dbuf cache with a default size of the
minimum of 100M or 1/32 maximum ARC size. (These figures may be adjusted
using dbuf_cache_max_bytes and dbuf_cache_max_shift.) The dbuf cache
is counted as metadata for the purposes of ARC size calculations.

On a 1GB box the ARC maximum size defaults to c_max 493M which gives a
dbuf cache default minimum size of 15.4M, and the ARC metadata defaults
to minimum 16M. I.e. the dbuf cache is an significant proportion of the
minimum metadata size. With other overheads involved this actually means
the ARC metadata doesn't get down to the minimum.

This patch dynamically scales the dbuf cache to the target ARC size
instead of statically scaling it to the maximum ARC size. (The scale is
still set by dbuf_cache_max_shift and the maximum size is still fixed by
dbuf_cache_max_bytes.) Using the target ARC size rather than the current
ARC size is done to help the ARC reach the target rather than simply
focusing on the current size.

Reviewed-by: Chunwei Chen <tuxoko@gmail.com>
Reviewed-by: Brian Behlendorf <behlendorf1@llnl.gov>
Reviewed-by: George Melikov <mail@gmelikov.ru>
Signed-off-by: Chris Dunlop <chris@onthe.net.au>
Issue #6506
Closes #6561
提交:
e71cade67d48495db46fb6eed29b88b895bcb2d8 [e71cade67d]
父级:
b59b22972db5913000ca157c24a254182df8d957
作者:
chrisrd <chris@onthe.net.au>
日期:
2017年9月30日 GMT+8 06:49:19
提交者:
Brian Behlendorf <behlendorf1@llnl.gov>


OpenZFS 9188 - increase size of dbuf cache to reduce indirect block decompression

With compressed ARC (bug 6950) we use up to 25% of our CPU to decompress
indirect blocks, under a workload of random cached reads. To reduce this
decompression cost, we would like to increase the size of the dbuf cache so
that more indirect blocks can be stored uncompressed.

If we are caching entire large files of recordsize=8K, the indirect blocks
use 1/64th as much memory as the data blocks (assuming they have the same
compression ratio). We suggest making the dbuf cache be 1/32nd of all memory,
so that in this scenario we should be able to keep all the indirect blocks
decompressed in the dbuf cache. (We want it to be more than the 1/64th that
the indirect blocks would use because we need to cache other stuff in the dbuf
cache as well.)

In real world workloads, this won't help as dramatically as the example above,
but we think it's still worth it because the risk of decreasing performance is
low. The potential negative performance impact is that we will be slightly
reducing the size of the ARC (by ~3%).

Porting Notes:
* Added modules options to zfs-module-parameters.5 man page.
* Preserved scaling based on target ARC size rather than max ARC size.

Authored by: George Wilson <george.wilson@delphix.com>
Reviewed by: Dan Kimmel <dan.kimmel@delphix.com>
Reviewed by: Prashanth Sreenivasa <pks@delphix.com>
Reviewed by: Paul Dagnelie <pcd@delphix.com>
Reviewed-by: Richard Elling <Richard.Elling@RichardElling.com>
Reviewed-by: George Melikov <mail@gmelikov.ru>
Reviewed-by: Giuseppe Di Natale <dinatale2@llnl.gov>
Ported-by: Brian Behlendorf <behlendorf1@llnl.gov>

OpenZFS-issue: https://www.illumos.org/issues/9188
OpenZFS-commit: https://github.com/openzfs/openzfs/pull/564
Upstream bug: DLPX-46942
Closes #7273
提交:
de4f8d5d2620b550b99a7bdbfbeb49214a38dc79 [de4f8d5d26]
父级:
a6cc97566c54255c98612c801f2c9d9be2d0e8f8
作者:
Brian Behlendorf <behlendorf1@llnl.gov>
日期:
2018年3月14日 GMT+8 01:52:48
提交者:
GitHub <noreply@github.com>

How Has This Been Tested?

A new test is added, and CI forks.

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

@mkhllr

mkhllr commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

I measured this against its parent on a VM to see where dbuf_cache_extra helps and
where it stops. Numbers are from e3a03b988 versus its parent 33ae06bb6. The head has
since moved to 4e2ec24fa, but the whole non-test diff is byte identical between the
two, so these still describe the code as it stands.

Setup: 31 GiB guest, one virtio disk as a single vdev, ashift=12, recordsize=4k,
primarycache=metadata, zfs_vdev_sync_read_max_active=80 (default 10), one 100 GiB
file, 4k random reads with --direct=1. Each cell is 30 seconds of warmup at 32 jobs,
then 45 seconds measured at 128, four repeats per cell, the module reloaded and the pool
re-imported for every cell, with the two builds alternating which runs first so neither
always runs first. The working set is varied by reading a prefix of the same file, which
moves the indirect set without refilling. One 128 KiB L1 holds 1024 block pointers, so at
4k it covers 4 MiB of file and the L1 set is 1/32 of the working set.

  working set   L1 set   evictions per 45 s (median)     dbuf target
                         parent          this PR         parent   this PR
      12500M     391M    2,021,808               6        31 MiB   473 MiB
         25G     800M    2,125,584              24        35 MiB   904 MiB
         50G    1600M    1,931,146       1,317,464        68 MiB   971 MiB
        100G    3200M    2,010,232       1,752,019       134 MiB   972 MiB

At the two small sizes the indirect churn stops outright: 4 to 54 evictions in 45
seconds against 7.8 to 9.3 million reads in those cells. At the two large sizes it still
falls, by less, and the ranges across repeats do not overlap at any of the four sizes:
about two thirds of the parent's evictions at 50G and seven eighths at 100G.

The target column is a mean of one-second samples, and at the two small sizes the target
did not hold still. It cycled between 433 and 651 MiB at 12500M and between 883 and 971
MiB at 25G, three or four steps up in each 45 s window with a decay in between. At the
two large sizes it held flat for the whole window, at 971 and 972 MiB. The dbuf cache
held 99.9% of the working set's L1 blocks at the two small sizes, never below 98.7% in
any one-second sample, and averaged 85% and 79% at the two large; the parent averaged 71
to 74% at every size. At the two large sizes the cache itself ran above its target in
both builds: at 100G it averaged 2.3 to 2.6 GiB against a 972 MiB target under the patch,
and 2.1 to 2.3 GiB against 134 MiB on the parent.

What bounds the target is the cap in dbuf_cache_adjust_tick(): the allowance is capped
at arc_c_max >> dbuf_cache_shift less the arc_c share at that tick, so the target
tops out near arc_c_max >> dbuf_cache_shift, about 971 MiB here. With the default shift
of 5 that covers an L1 set of roughly 1/32 of arc_c_max, which with a default
zfs_arc_max is memory less 1 GiB. At recordsize=4k that puts the knee near a 30 GiB
working set on this box, and the sweep brackets it between 25 and 50 GiB. Doubling the
recordsize halves the L1 set for a given file, so the knee moves out by the same factor
as the recordsize, which would put it near 60 GiB at 8k. dbuf_cache_shift=0 raises the
ceiling by 32.

On throughput the ratios of the medians came out 1.107, 1.112, 1.086 and 1.061 across the
four sizes, and 56 of the 64 cross-arm pairs favour the patch. I would not lean on that
part: three of the four sizes have overlapping ranges across repeats, and the per-cell
spread is about the size of the difference. The eviction result is the one I would trust.

@tb3088

tb3088 commented Sep 9, 2026

Copy link
Copy Markdown

while I agree there is a problem to address, this seems a bit hacky. Wouldn't it make more sense to modify the evict() logic so that it doesn't toss a dbuf unless actually stale, or is explicitly associated with a cached data block that the LRU has marked? I don't know if dbufs have a TTL (time to live) or if that would be useful.

I think the fundamental problem with the current logic is that it's based on a contrived size ratio to target_ARC, and doesn't treat L1 dbufs etc. as higher priority to stick around over other things like cached data blocks. Along similar lines, target_ARC should always grow based on metadata and dbuf_cache allocations and only tangentially in support of data blocks being read from disk.

I realize having to go back to disk to re-read data is extremely expensive, but honestly, the incidence of re-reading the same data blocks over and over is wildly overblown. And if you really do have a workload that accesses cached data blocks that unfortunately get reaped too soon by ARC pressure management, that's what L2ARC (or more RAM) is for. Or cache it yourself in the app or OS virtual memory/page_cache layer. A penchant for Over-caching is just as bad or worse than under-caching.

IMO the better solution is to let "naked" dbufs (entries without a corresponding cached data block) fill the entire ARC (or a large proportion) which will force ARC to grow to ARC_MAX if the workload is sustained. If you absolutely must insist on pinning some ratio of CURRENT_ARC_BYTES for purposes of caching data blocks (eg. 25%), then one could write the eviction logic such that these "naked" dbufs are preferentially reaped once that threshold has been met.

@tiehexue
tiehexue force-pushed the break-link-dbuf-arc-cache-size branch from 4e2ec24 to 39dbf34 Compare September 10, 2026 01:11
@tiehexue

Copy link
Copy Markdown
Contributor Author

@mkhllr thanks for your great verification, the bound "arc_c_max >> dbuf_cache_shift" is not right, I added a new module parameter (again), default as 2, that mean 1/4 of arc_c_max could be dbuf "extra" cache. Hope you could comment this change, and re-test it. Refer to commit message and code for details.

@tiehexue

Copy link
Copy Markdown
Contributor Author

Wouldn't it make more sense to modify the evict() logic so that it doesn't toss a dbuf unless actually stale, or is explicitly associated with a cached data block that the LRU has marked? I don't know if dbufs have a TTL (time to live) or if that would be useful.

As I learned from the code, the commits, the eviction logic, the overall design is quite sound. The only thing is that direct IO, nvme disks are not there before 2018. This PR inserts a logic into an existing thread which will add "extra" room for naked dbuf (I like this term), while keep other things well.

@tb3088

tb3088 commented Sep 10, 2026

Copy link
Copy Markdown

I guess what I didn't articulate well was that your patch should:

  1. change the default dbuf_cache_shift to 2 or 3 (IMO 5 is a bit silly) and adjust dbuf_metadata_cache_shift similarly.
    Q: Is the ratio of dbuf_cache to dbuf_metadata_cache properly half? I guess if it's oversized not a biggie.

  2. don't introduce extra module parameters or a '1 second tick'

Fundamentally the problem with dbuf_cache and the ensuing thrashing/unhelpful eviction under sustained direct I/O, is that it was vastly too small for starters, but artificially constrained as a percentage of target_ARC and target_ARC only grows in response to adding data blocks from "cached reads", or much more slowly from collecting "metadata".

Since dbuf_cache has its own simplistic evictor triggered by size+dbuf_cache_hiwater_pct, this looks like the place to add "try increasing target_ARC, otherwise evict()".

Since we (should) know that "naked" dbufs are being created from O_DIRECT activity we should be able to count these instances. And when the cache size breaches the hiwater_pct, instead compute and try to set the new arc_target that would allow eviction to be forestalled. ie if lowater_pct = 10 then new target_ARC = current target_ARC / 0.9 and zero the O_DIRECT dbuf counter. There may also be some merit to checking the ratio of "naked dbufs" as a percentage of the total pool since the last counter reset,and if it's below X we just evict.

If there is no directIO going on then eviction happens as usual. If setting new target_ARC is rejected, then eviction happens as well.

I'm not sure what to do about primarycache = metadata which also restricts target_ARC growth to the detriment of dbuf_cache sizing.

A patch that eliminated the size constraints altogether (IMO a case of false optimization in the first place) and instead waiting to clean up via the evictor that fires when ARC = ARC_MAX or arc_target can't grow anymore due to OS memory pressure is the best way to address the problem. ZFS seems entirely too wed to the notion that we need to cache user data we just read because it'll be read again in short order. I doubt that was a valid assumption but unfortunately it underpins a variety of data-structures.

@tiehexue

Copy link
Copy Markdown
Contributor Author

2. don't introduce extra module parameters or a '1 second tick'

Hi @tb3088 , there are two rules I got in this PR: 1) we should not set module parameters for direct I/O only, the existing link between dbuf cache target and arc target, and there defaults has there logic, and should retain; 2) we should not tune module parameter "internally", e.g. via detecting direct I/O high IOPS.

With two rules in mind, I guess we have to go to this PR, by a up/down "internal" variable which is dbuf_cache_extra based on if direct I/O coming/going, and a bounding module parameter which is dbuf_cache_extra_max_shift.

And I do think there are too many module parameters now, especially the dbuf_cache_max_bytes which is set to UINT64_MAX.

Also 1 second tick can be changed if necessary, e.g. 5 or so?

@tb3088

tb3088 commented Sep 10, 2026

Copy link
Copy Markdown

I don't like "extra" for naming because it doesn't mean anything. But leaving that aside, the 'tick' thing IS the very definition of trying to tune behavior internally in a hidden fashion based on a very coarse 'clock' interval. Strictly speaking if the sysadmin is not paying attention to his workload, and directIO is churning the (way too small) dbuf_cache that really should be on him. He should be changing dbuf_cache_shift and hiwater_pct.

However, given that ZFS currently has a naive+incomplete means of influencing target_ARC growth, I think THAT should be addressed. If we count dbufs being created in response to DirectIO, at some point the standard logic will trigger eviction once dbuf_cache_memory_footprint >= (100+hiwater)pct * targetArc * dbuf_cache_shift. and THAT is the point that we intervene by checking how much directIO was going on, and if it was a lot, then we upsize target_ARC and return instead of continuing to eviction.

When the dbuf_cache grows to its upper limit again, we apply the same check. If there was little/no DirectIO, then we step out of the way and let the LRU trim the cache.

In effect the PR should only have enough code to count dbufs being allocated in response to DirectIO and a conditional right after the hiwater condition is satisfied.

@amotin amotin left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting topic. I am thinking about it on and off for quite a while, but instead from a perspective of system cold boot, when it simply haven't seen enough data yet to grow the ARC, but it would be good to make it run without constant ARC decompression. The reason why the dbuf cache limit is set so low is to not permanently duplicate data already stored in ARC. ARC is allowed to grow big because it is unmapped and compressed. By duplicating it you risk possibility to KVA exhaustion and waste its compression respectively.

I couple places in ZFS I've already used arc_warm ? arc_c : arc_c_max construct to counteract cold boot issues. Recently was introduced one more arc_warm ? arc_c : (arc_c + arc_c_max) / 2, AKA arc_boot_target_bytes(), which I think is better. I was actually thinking to extend those to other places using arc_c, just haven't got to it yet. Though it is indeed a bit simplistic.

I think it would be cool to make dbuf cache size to dynamically adjust to the active dataset size. You've tried to implement it in this patch, but I don't think good enough.

If your workload is such that your active dataset size is equal to the total dataset size, then it would be cool to make debuf cache not duplicate the ARC, but share buffers with it, giving up on compression and KVA. Though giving up on KVA would limit the maximum cache size to avoid KVA fragmentation, and open a question of what to do with blocks in ARC when their dbuf counterpart is evicted? Evict too or re-compress/re-scatter? May be you would generally like to tune ARC to not compress (some kinds of) metadata blocks to allow the buffer sharing for free, if that is your real scenario, not some absolute synthetic?

Comment thread module/zfs/dbuf.c Outdated
Comment thread module/zfs/dbuf.c Outdated
Comment thread module/zfs/dbuf.c Outdated
@amotin amotin added the Status: Revision Needed Changes are required for the PR to be accepted label Sep 10, 2026
@mkhllr

mkhllr commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

I re-tested at ac661fbcf against its parent 75bd31c28, on the rig from my previous
comment: 31 GiB guest, one virtio disk as a single vdev, ashift=12, recordsize=4k,
primarycache=metadata, zfs_vdev_sync_read_max_active=80 (default 10), one 100 GiB
file, 4k random reads with --direct=1, 30 seconds of warmup at 32 jobs then 45 seconds
measured at 128, three repeats per cell with the module reloaded and the pool re-imported
for each. Four arms from one pair of builds: the parent, and the PR head at
dbuf_cache_extra_max_shift 2, 4 and 5, differing only by the tunable. The shift=4 arm is
a second run on the same builds with its own fill; the other three share one.

  working set   L1 set     evictions per 45 s (median)         dbuf target (MiB)
                         parent    shift=2     shift=4     shift=5   sh2   sh4   sh5
       12500M   391M  1,972,831          9          31           7   470   473   474
          25G   800M  2,123,142          8           8          52   960   965   904
          50G  1600M  2,244,676          7           7   1,374,714  1900  1806   971
         100G  3200M  1,827,617          3   1,390,635   1,936,078  3802  1943   972

At the default shift of 2 the indirect churn is gone at all four sizes: 3 to 10 evictions
in 45 seconds against 8.3 to 9.2 million reads, and the dbuf cache held 99.9 to 100% of
the working set's L1 blocks. The target column is a mean of one-second samples and it
cycles as it did before, between 3555 and 5333 MiB at 100G. It tracks the working set and
stays under the cap: 470, 960, 1900 and 3802 MiB against L1 sets of 391, 800, 1600 and
3200 MiB, so even the largest averages 49% of the 7766 MiB ceiling and peaks at 69%.

arc_c_max >> 2 is 7766 MiB on this box, which puts the knee near 243 GiB of file at
recordsize=4k, past what a 100 GiB file can bracket, so I moved the ceiling with the
tunable instead. At shift=5 the expression is what it was before this change, and it
reproduces the numbers from my previous comment, now on the rebased head: target 474, 904,
971 and 972 MiB against 473, 904, 971 and 972, evictions 7, 52, 1.37M and 1.94M against 6,
24, 1.32M and 1.75M, each of those four inside the range of that run's repeats.

At 100G the two arms came out closer together this time, and one claim from my previous
comment does not hold: the parent at 1.70M to 1.97M against 2.00M to 2.38M then, shift=5
at 1.86M to 1.96M, so the ranges overlap and the shift=5 median sits about 6% above the
parent's. The non-overlap I reported at 100G, and the seven eighths with it, does not
reproduce. The two parents are 17 commits apart, so I would not read either run's 100G
figure as settled. At 50G the separation does reproduce: 2.18M to 2.36M for the parent
here against 1.32M to 1.38M, and 1.74M to 2.09M against 1.09M to 1.43M before.

At shift=4 the ceiling is 1942 MiB and the knee should fall at 60.7 GiB: 50G fits, with
the target at 1806 MiB, 93% of the ceiling, and 100G saturates. So at shift=4 and shift=5
the knee falls where arc_c_max >> dbuf_cache_extra_max_shift puts it, and at shift=2
nothing up to 100G reaches it.

@tb3088

tb3088 commented Sep 10, 2026

Copy link
Copy Markdown

@mkhllr if you run this bench but ignoring the PR entirely, and tune dbuf_cache_shift to 3, 4, 5, set arc_min = 16gb and/or 24gb or 30gb to see how the eviction rate is influenced?

@amotin maybe dbuf_cache could use something akin to zfs_arc_meta_balance does on metadata eviction? ie. a ghost hit would initiate a target_ARC increase? I think such a feedback mechanism would gradually taper the prematurely evicted dbuf entries till it arrived at a steady state? The early misses would be painful at first but would self-correct.

@tiehexue
tiehexue force-pushed the break-link-dbuf-arc-cache-size branch from ac661fb to de66c83 Compare September 11, 2026 02:07
@github-actions github-actions Bot removed the Status: Revision Needed Changes are required for the PR to be accepted label Sep 11, 2026
@tiehexue

tiehexue commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

@amotin , I added one internal variable and one module parameter. I will be very happy to see a cleaner solution in another PR, or just make this PR correct. The whole point is, I do not break how dbuf cache target is managed, and how dbuf and arc are evicted, just "find" a signal to add/remove an extra, and must be out of the I/O path.

The signal may be not quite "accurate" by dbuf_arc_underutilized and trimming, and the one second tick may be too often, plus the steps (up/down) may be too sharp.

The "too often" can be addressed by run the adjust 2 or 4 seconds in arc_reap_cb_check by a random picking.

if (random() > 0.5)
  dbuf_cache_adjust_tick(arc_no_grow, arc_c_max);

For the arc_warm and arc_boot_target_bytes, the problem is still, we need a signal.

"trimming", I mean the code: "uint64_t trimmed = evicts - dbuf_cache_prev_evicts;", how much evicts happens in passed 1 second.

@tiehexue

tiehexue commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

The two parents are 17 commits apart,

Thanks @mkhllr , I just do a rebase again to catch up master, especially, this PR touches arc/dbuf, core code of zfs, and may not be good for testing :-)

The test case added, and your test, confirmed this PR worked. Do you know a better way to simulate a normal I/O, like mixed ARC and direct I/O and different datasets, file sizes. And also more numbers collected, cache_extra_bytes etc.

I think we already test and verified at #19076 , that this PR could improve direct IO IOPS. But would this PR hurt the system in other ways.

@amotin

amotin commented Sep 11, 2026

Copy link
Copy Markdown
Member

I do not break how dbuf cache target is managed

Blowing dbuf cache up to 1/4 of RAM, duplicating ARC, is not the answer. It is not a question of tunables present. Lets not go insane about zillions of synthetic Direct I/O IOPS tomorrow, but think strategically. The thinking towards the mentioned ghost states similar to ARC sounds interesting, but I wonder how complicated would we like it to be? Could we differentiate different kinds of blocks? Could we integrate with ARC states? Could we improve dbuf sharing?

@tiehexue

Copy link
Copy Markdown
Contributor Author

I do not break how dbuf cache target is managed

Blowing dbuf cache up to 1/4 of RAM, duplicating ARC, is not the answer. It is not a question of tunables present. Lets not go insane about zillions of synthetic Direct I/O IOPS tomorrow, but think strategically. The thinking towards the mentioned ghost states similar to ARC sounds interesting, but I wonder how complicated would we like it to be? Could we differentiate different kinds of blocks? Could we integrate with ARC states? Could we improve dbuf sharing?

Thanks @amotin . If I understand your comments correctly, it looks I need a different PR or just replace all code here with new ones. Let me try.

@amotin

amotin commented Sep 11, 2026

Copy link
Copy Markdown
Member

First I'd say a little less rush. ;) If the code is significantly different, clean PR may have sense to compact the comments history.

@amotin amotin added the Status: Revision Needed Changes are required for the PR to be accepted label Sep 11, 2026
@tb3088

tb3088 commented Sep 11, 2026

Copy link
Copy Markdown

maybe the easiest 'fix' is documentation? sysadmins are largely ignorant of ZFS internals. sysadmins aren't paying attention to arcstat nor their workloads and its knock-on effects. sysadmins also generally don't know the ramifications of ZFS defaults nor the effect of tunables like arc_min, hiwater_pct and dbuf_cache_shift.

I could easily take a hard-line stance that the solution here is use those knobs to mitigate your problem. Personally I would run something like arc_min=16gb, hiwater_pct=400 and dbuf_cache_shift=3. (I haven't yet checked if hiwater_pct has been sabotaged to only allow values of 0-100)

This workload or say a VM boot-storm is like a pig in a python. Except that ARC doesn't stretch like a snake digestive tract since it has no signal to act upon. My trio of settings pre-stretches the digestive tract and once the pig is in and there is a huge bulge (in dbuf usage) it'll waste space for a while but once it trips hiwater_pct=400 the pig will disappear in a flurry of evict() actions. Till the next pig comes along.

A paragraph on the ZFS tuning wiki could go a long way I think?

That said adding some smarts to dbuf_cache management gets an upvote.

@tiehexue

Copy link
Copy Markdown
Contributor Author

That said adding some smarts to dbuf_cache management gets an upvote.

This PR is just doing this.

And for your suggestion about "documentation fix", the problem is what I said, the system admin could not adjust module parameters time by time, e.g. before the pig is in, make hiwater_pct up, and then set it back. And if the hiwater_pct is not set back to reasonable one, or it is set to 400% without changing, the dbuf evict thread will even do not start or stop quickly, then 400% of dbuf target contains "never accessed again" data, e.g. L1 indirect block, plus compressed ones in arc meta area.

@tb3088

tb3088 commented Sep 11, 2026

Copy link
Copy Markdown

400% of dbuf target contains "never accessed again" data, e.g. L1 indirect block, plus compressed ones in arc meta area.

agreed to a point. I would look at why L1 indirects are overlapping meta instead of there being one source of truth. Perhaps it was just expedient to duplicate. Which then begs the question, if the dbuf_cache had an entry evicted, does it go back to meta and re-insert it into the cache chain? or maybe just read it and return?

Temporarily wasting memory in dbuf_cache because of an outsized workload that came thru is not a problem. Memory is cheap. disk access is expensive. A few extra minutes spent letting the pig stick around causes no great harm. The trigger will fire soon enough and whack it back to size.

shift=3 and hiwater=400 gives me the benefit of shift=2 without the "waste" being long term. You could choose different values like shift=5 hiwater=3 and temporarily get the benefit of shift=3

@tiehexue

Copy link
Copy Markdown
Contributor Author

@amotin While I am still on the way for new solution, let me re-state my thinking to your comments.

The thinking towards the mentioned ghost states similar to ARC sounds interesting, but I wonder how complicated would we like it to be?
--- Yes, I was thinking about de-couple dbuf cache and arc (see the branch name), build another set like multilist, ghost ones. This is too complex than current solution. And we have to refactor ARC, include its accounting, eviction policy.

Could we differentiate different kinds of blocks?
--- I was thinking about why dbuf cache is linked to arc, by commit history. Still not very clear now. Maybe for the first time when introduced, dbuf cache is created for "uncompress data", and the uncompress data are actually in ARC and the size is accounting to arc. Then another commit said that need to make dbuf cache large enough to hold all L1 indirect block, which is roughly 1/64 of L0 data blocks, the dbuf_cache_shift is set to 1/32, is kind of "storing 2 file's L1 indirect blocks". The blocks are already in different kind (many flags), the problem is we do not trace them for "ghost states". Are you saying adding a multilist/ghost pair for L1 uncompressed indirect block? Looks doable.

Could we integrate with ARC states?
--- not quite sure, what is this pointing?

Could we improve dbuf sharing?
--- You mentioned "duplicate" several times, if it is saying about "compressed" and "uncompressed" pair. I think this will be off the topic of this PR. For L1 indirect blocks, if you want to drop the compressed ones, we could do another PR.

In a word, as we want to handle the "ghost states" in dbuf cache, we need a signal, a mechanism. And I do think this PR make this works, also has some "defects" I already said before.

It has several good points. And the "Blowing dbuf cache up to 1/4 of RAM", if it happens, it means it has to be, and it will drop once it can.

@mkhllr

mkhllr commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@tb3088, the sweep you asked for, and dbuf_cache_hiwater_pct as well since you named it
in the trio. Parent build 75bd31c28 throughout, no patch. One VM: 31.3 GiB of RAM, one
virtio vdev on an NVMe partition, zfs_vdev_sync_read_max_active=80, everything else at
its default. A 100 GiB file at recordsize=4k with primarycache=metadata, read with 4k
random --direct=1 at 128 jobs, 45 s cells after a 30 s warmup, three repeats each with
the order rotated. The L1 indirect set is 3200 MiB in every cell. Evictions are the
cache_total_evicts delta across the cell; both the eviction and IOPS columns are medians
of the three repeats.

dbuf_cache_shift against zfs_arc_min. arc_c sat exactly at arc_min in every
sample, so the dbuf target is arc_min >> shift:

  target   shift / arc_min   evictions per 45 s   range over repeats     IOPS
   512 M       5 / 16G            2,117,840       1.88M - 2.38M        186980
   768 M       5 / 24G            2,408,038       1.49M - 2.52M        182869
   960 M       5 / 30G            2,247,963       1.97M - 2.25M        185062
  1024 M       4 / 16G            1,912,290       1.85M - 2.26M        179850
  1536 M       4 / 24G            1,696,733       1.52M - 1.76M        184782
  1920 M       4 / 30G            1,237,370       1.20M - 1.40M        187814
  2048 M       3 / 16G            1,124,553       0.98M - 1.13M        190152
  3072 M       3 / 24G              212,439       0.19M - 0.23M        196862
  3840 M       3 / 30G                    0             0 - 0         201603

At and below a 1024 MiB target the four cells share a common interval, 1.97M to 2.25M.
From 1536 MiB up the count falls as the target rises, and the one cell whose target
exceeds the 3200 MiB L1 set evicts nothing in any repeat. Because arc_c sat at the
floor, the two knobs enter only through their quotient here, and the near-matched pairs
land within 10% (2048 against 1920) and 18% (1024 against 960) of each other, which is
inside what three repeats can resolve. On the eviction counts I would not read much into
differences under about 20%.

The zero-eviction cell ran 201,531 to 204,579 IOPS, above every repeat of every other
cell, but that is only 2% over the next best median, so I note the separation without
leaning on it.

That arithmetic holds while arc_min binds. With nothing pinning it, arc_c on this VM
floated at 4261 to 4281 MiB across my earlier parent runs at the same 100 GiB file, and at
that arc_c a shift of 3 gives a 533 MiB target, well under the 3200 MiB L1 set.

The pin cost no memory in this run: arc_size averaged 3.2 to 4.1 GiB across the nine
cells, 4226 MiB in the shift 3, 30G cell, and memory_free never fell below 25,553 MiB in
any sample. What zfs_arc_min sets is the floor arc_reduce_target_size() will not cut
arc_c below, and nothing here applied memory pressure, so this says nothing about a box
where something else wants that memory.

Then dbuf_cache_hiwater_pct, with your other two values fixed at arc_min=16G and
shift=3, so every cell has a 2048 MiB target and a 1843 MiB low-water mark. The cache
column is the mean of the one-second samples in a repeat, median of the three:

  hiwater_pct   hiwater   evictions per 45 s   range over repeats   dbuf cache      IOPS
      10        2253 M         1,243,256       1.16M - 1.26M     2769 M (2750-2783)  195307
     100        4096 M         1,313,050       1.18M - 1.63M     2733 M (2597-2744)  208309
     400       10240 M         1,561,435       1.37M - 1.56M     2691 M (2626-2699)  204203
    1000       22528 M         1,469,698       0.65M - 1.68M     2651 M (2638-2848)  200518

It accepts 400 and 1000: cache_hiwater_bytes reads 10240 MiB at 400 and 22528 MiB at
1000. The resting size does not follow it. The cell medians sit between 2651 and 2769 MiB,
a 4% span, and the widest per-repeat spread, 2638 to 2848 MiB at 1000, is larger than that
span. In every repeat the one-second samples range between roughly the low-water mark,
1843 MiB at the lowest, and 3196 to 3200 MiB, and cache_size_bytes_max reads 3199 to
3200 MiB in all twelve repeats. That top is the workload running out of L1 blocks to
insert rather than any mark: 3200 MiB is the whole set. For contrast, the one sweep cell
whose target held the set, 3840 MiB, evicted nothing and stayed between 3196 and 3203
MiB for the whole cell. A hiwater of 10240 MiB against a 2048 MiB target did not do that.

The eviction medians stayed in a narrow band, 1.24 to 1.56M per 45 s. The 400 cell did
run above the 10 cell in every repeat, a 26% gap on medians, but the 1000 cell spans 0.65M
to 1.68M, so I would not read a direction into it. The only cell in either table that
stopped evicting is the one whose target exceeded the L1 set.

IOPS did move. The 100 and 400 cells ran above every repeat of the 10 cell, by 5 to 7% on
medians. That is what dropping the inline eviction off the reader's path would predict,
and the samples agree that this is what changed: at hiwater_pct=10 the cache was above
hiwater in 122 of 129 samples, and at 100, 400 and 1000 it was above hiwater in 0 of 129.

That is the part worth knowing about this knob. dbuf_evict_thread() evicts while
dbuf_cache_above_lowater() and otherwise sleeps a second at a time, so it drains toward
the low-water mark whenever it is above it, and dbuf_evict_notify() signals it whenever
the size is above the target but calls dbuf_evict_one() in the caller's context only
above hiwater. So hiwater decides how much of the eviction happens synchronously on the
reading thread, and the low-water mark decides where the cache comes to rest. Raising it
took the inline eviction off the reader and bought a few percent of IOPS; it did not let
the cache hold more.

@tb3088

tb3088 commented Sep 12, 2026

Copy link
Copy Markdown

That is the part worth knowing about this knob. dbuf_evict_thread() evicts while
dbuf_cache_above_lowater() and otherwise sleeps a second at a time

I guess I should have read the source code. :) Yeesh, so much for my desired behavior.

I think dbuf_evict_thread() should fire on breach of hiwater to take a big bite out of the overrun (eg. half the distance between current and target?), and dbuf_evict_one() (I'm going to assume it chops 1% of dbufs, not just one dbuf entry) should be the behavior once it's crossed the 100% threshold to the downward side as it slowly approaches lowater. That way if another pig comes along and balloons way past 100% no evictions happen till once more it trips hiwater.

Alternatively, dbuf_evict_one() is the behavior any time over target, and on breach of hiwater it pivots to taking a big bite.

In any case, a bunch of ghost hits would be a nifty way to signal the evict() to back off and just keep the overrun contained to hiwater.

@mkhllr

mkhllr commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

@tb3088 on the assumption you flagged: dbuf_evict_one() evicts at most one dbuf
per call. It picks one random sublist of the dbuf cache multilist, walks back from
the tail to the first entry whose db_mtx it can tryenter, and destroys that
single buffer. If it finds no such entry it unlocks and returns having evicted
nothing.

The eviction loop in dbuf_evict_thread() is:

while (dbuf_cache_above_lowater() && !dbuf_evict_thread_exit) {
        dbuf_evict_one();
}

so once it wakes it keeps calling that until the cache is back at the low-water
mark. There is no taper, and the only mark it checks is lowater. The place
dbuf_cache_hiwater_pct enters is dbuf_evict_notify(), where each release that
finds the size above it makes the releasing thread call dbuf_evict_one() itself
before signalling the eviction thread.

In the 4k Direct I/O workload from the hiwater sweep above, the cache was above the
low-water mark in 515 of 516 one-second samples, so at essentially every instant
sampled the eviction thread had something to evict. Arrivals still outran it, which
is why the cell medians of the cache size sat at 2651 to 2769 MiB against a 2048
MiB target whatever hiwater_pct was set to.

On a bigger bite at a hiwater breach: in the cells where I raised hiwater_pct the
cache was above the mark in 0 of 129 samples, so there was no breach to act on. At
the default it was above in 122 of 129, so the single inline dbuf_evict_one() the
code already makes was firing for most of the cell, and the cache rested no lower
and evicted no more for it than in the cells where it never fired. If a bigger bite
is built from the same calls, nothing here suggests that N of them would do what one
did not.

What one-second sampling cannot show is the rate within a second, and the counts say
it is not steady. Across the twelve repeats the per-second eviction count ranged
from 0 to 127,003, and within a single repeat from 2,500 to 73,575. Two of the
twelve contain a second with no evictions at all, and I have not chased why.

Your ghost-hit idea is separate from this, and I have nothing measured that bears
on it either way.

@amotin

amotin commented Sep 12, 2026

Copy link
Copy Markdown
Member

Could we differentiate different kinds of blocks? --- I was thinking about why dbuf cache is linked to arc, by commit history. Still not very clear now. Maybe for the first time when introduced, dbuf cache is created for "uncompress data", and the uncompress data are actually in ARC and the size is accounting to arc. Then another commit said that need to make dbuf cache large enough to hold all L1 indirect block, which is roughly 1/64 of L0 data blocks, the dbuf_cache_shift is set to 1/32, is kind of "storing 2 file's L1 indirect blocks". The blocks are already in different kind (many flags), the problem is we do not trace them for "ghost states". Are you saying adding a multilist/ghost pair for L1 uncompressed indirect block? Looks doable.

IIRC dbuf cache was tied to ARC before ARC got scatter ABDs. In wast majority of cases dbufs shared uncompressed buffers with ARC. If compression was disabled or not very successful, dbuf cache cost very little, and was just differently indexed (logically instead of physically) interpretation of the same blocks. The major divergence happened with ABD introduction, when it was decided that since compressed blocks any way can't be used directly, why do we need to map them into KVA. This one change dramatically broke the buffer sharing even for uncompressed data. While FreeBSD shares buffers at least up to 4KB, for Linux that is even lower. But introduction of ABDs was a required response on a problem of KVA fragmentation -- no matter how big KVA is pre-allocated, at certain workload and time it will be exhausted due to fragmentation. I haven't dug how Linux deals with it, but for Illumos and FreeBSD it ended up in kernel panics. dbuf cache appeared as a response to the need of repeatable decompression and copying data from ARC.

So for me this is not a question of dbuf cache size. Sure, we can tune it, we can and probably should make it adaptive to optimize RAM usage, but we can not afford it to consume 1/4 of RAM. It just won't fly. And least I think not too far above that threshold. We have to look for some other ways.

Could we integrate with ARC states? --- not quite sure, what is this pointing?

I was thinking if we could use buffer state in ARC to affect its life cycle in dbuf cache too. arc_access() already updates ARC state based on dbuf accesses, so I was thinking if we could benefit from it. Though only a thinking yet.

Could we improve dbuf sharing? --- You mentioned "duplicate" several times, if it is saying about "compressed" and "uncompressed" pair. I think this will be off the topic of this PR. For L1 indirect blocks, if you want to drop the compressed ones, we could do another PR.

No. For me it is the topic, that should be answered first. Otherwise choice whether we allow 1/32 or ARC or 1/16 of ARC is not very productive. I won't object if under proper workload we allow dbuf cache to take 1/16, or may be 1/8, for a very good reason, but against each following step I'll argue more. This is a dead end.

In a word, as we want to handle the "ghost states" in dbuf cache, we need a signal, a mechanism. And I do think this PR make this works, also has some "defects" I already said before.

This PR, as I read it, effectively gradually give dbuf cache all that it wants, until we hit a memory pressure from the kernel, when it try to shrink, doing enormous amount of work at once, creating a huge load spike and stalling new I/Os. We need a way to find a balance to cache only what we really need. So that accessed once buffers would be evicted as soon as possible, not wasting expensive RAM, but buffers accessed often could live longer. All the motivation is very similar to ARC MRU/MFU, etc, except that for ARC we do want to use all memory we have, since it is our most efficient way to store data, but for dbuf cache we need it to stay small, since it is uncompressed, plus consumes KVA.

It has several good points. And the "Blowing dbuf cache up to 1/4 of RAM", if it happens, it means it has to be, and it will drop once it can.

Nope. 1/4 we just can't afford. See above.

@tiehexue

Copy link
Copy Markdown
Contributor Author

@amotin Thanks. Always learn a lot from your comments. Now I guess I have a "much clearer" full picture of your concern. I still have following points: 1) dbuf cache for direct I/O, is same thing like ARC for buffers accessed. While ARC could have the "we do want to use all memory we have", why not dbuf cache, and plus the real data is still in ARC; Look at how @mkhllr test, so much evicts if bounded dbuf cache to arc target. 2) "a huge load spike and stalling new I/Os", is the load spike really bad? And from memory view, in this PR, even very high IOPS, the evicts keep in dozens, compare to millions. Once the direct I/O flood is over, e.g. VMs start, the memory is reclaimed very quickly as It should.

The main problem now is about how dbuf cache is evicted. It just constantly evict from high water to low water. In mkhllr's test, while "extra" goes up, the low water goes up too, thus not much evicts. Your suggestion about to align with ARC MRU/MFU is very interest.

In a word, let me think about more, see if I could make this PR in a good shape, fix both direct I/O evicts and the eviction policy.

dbuf_cache_shift is added and updated since
2016 by commit d3c2ae1, e71cade and
de4f8d5. It is well designed, however,
in direct I/O or cache-off I/O, arc may not
grow, arc target size is not growing too.
Then, in high IOPS, dbuf cache will in a
busy evicting cycle. To ensure, dbuf not
starving, an extra is added while arc current
size is small or dbuf cache dominates arc size.
With this extra increased in arc recap thread,
and downgrade too based "trimming" signal
of dbuf stats, high IOPS read should be good.

Signed-off-by: tiehexue <tiehexue@hotmail.com>
This new module parameter is quite different
than dbuf_cache_max_bytes which looks not that
in use now, because it has a default value at
UINT64_MAX, and historically, the dbuf target
size is bound by dbuf_cache_shift related to
arc target size. This commit and the previous
one added an "extra" bytes to dbuf cache to
support direct I/O or cache-off I/O. Now,
the extra is bound by dbuf_cache_extra_max_shift
related to arc max size. Default is 2, that
means, a quarter of arc max can be used by
dbuf cache.

Signed-off-by: tiehexue <tiehexue@hotmail.com>
This extra value will make the dbuf cache
evit thread remove all items until low water
meets. If extra is already "big", that will
cause a load spike.

Signed-off-by: tiehexue <tiehexue@hotmail.com>
And also a ceiling "min" to ensure no above
max_extra in any way.

Signed-off-by: tiehexue <tiehexue@hotmail.com>
@tiehexue
tiehexue force-pushed the break-link-dbuf-arc-cache-size branch from 51f6164 to 05c5fdc Compare September 14, 2026 05:49
@github-actions github-actions Bot removed the Status: Revision Needed Changes are required for the PR to be accepted label Sep 14, 2026
@amotin

amotin commented Sep 14, 2026

Copy link
Copy Markdown
Member
  1. dbuf cache for direct I/O, is same thing like ARC for buffers accessed. While ARC could have the "we do want to use all memory we have", why not dbuf cache, and plus the real data is still in ARC;

As I have told, if nothing else, ARC is scattered and not mapped into KVA. If you try to use 95% of RAM, as ARC can do, with dbuf cache of buffers of different sizes, you'll end up in KVA fragmentation and exhaustion pretty soon. And you still have ARC there with a duplicate copy of the data.

Look at how @mkhllr test, so much evicts if bounded dbuf cache to arc target.

I had no time to look what that test does. Sure I can write dozen of different tests, defeating one or another aspect of ZFS. The question is how relevant is that to real life.

  1. "a huge load spike and stalling new I/Os", is the load spike really bad? And from memory view, in this PR, even very high IOPS, the evicts keep in dozens, compare to millions. Once the direct I/O flood is over, e.g. VMs start, the memory is reclaimed very quickly as It should.

I'll repeat once more, this patch gives dbuf cache all memory that it asks. So obviously when its size reaches test's active dataset size everything stabilizes. But this is one specific tests. The world is much more complicated.

@tiehexue

Copy link
Copy Markdown
Contributor Author

@amotin thanks, I guess I missed many times as you said about KVA and its fragmentation. So memory are not created equal, dbuf backed by KVA, while ARC has linear or scatter ABD, though are from Linux kernel.

And KVA allocate and deallocate not quite same, deallocation may not be easy once some of its pages are referenced.

I would go through all background more until I could catch up.

@tb3088

tb3088 commented Sep 15, 2026

Copy link
Copy Markdown

I'm going to take a crack at "fixing" dbuf_cache along a couple points.

  1. implement MRU/LRU
  2. trim dbufs from the tail (LRU) instead of random which could easily discard dbufs that are being used
  3. allow 'pig in python' by reworking eviction algorithm so it doesn't just blindly enforce lowater like ravenous Gremlins
  4. (unlikely) implement MFU counters - trying to think of a use case if there is one.

@tiehexue

Copy link
Copy Markdown
Contributor Author

I'm going to take a crack at "fixing" dbuf_cache along a couple points.

  1. implement MRU/LRU
  2. trim dbufs from the tail (LRU) instead of random which could easily discard dbufs that are being used
  3. allow 'pig in python' by reworking eviction algorithm so it doesn't just blindly enforce lowater like ravenous Gremlins
  4. (unlikely) implement MFU counters - trying to think of a use case if there is one.

#19118 is a try.

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.

4 participants