public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Enrico Plant <plantulli@gmail.com>
To: Christian Ebner <c.ebner@proxmox.com>
Cc: pbs-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox proxmox-backup 0/3] datastore: gc: prefetch chunk metadata before phase 1
Date: Mon, 10 Aug 2026 22:32:28 +0200	[thread overview]
Message-ID: <CAKymmRLJ_6C_4io=rgWiALDrMvtB4z7EbW3iu5CxQdrEFWu6Hg@mail.gmail.com> (raw)
In-Reply-To: <96d49b8d-5049-473d-989c-c98060810cc2@proxmox.com>

[-- Attachment #1: Type: text/plain, Size: 11122 bytes --]

Hi,

thank you for the quick and thorough review!

> For it to be considered please send the signed contributors license
> agreement

Already done - I sent the signed individual CLA to office@proxmox.com
on Aug 10, our mails probably crossed. Happy to resend if it did not
arrive.

> I think there might be additional performance improvements to be
> gained from this: In particular, instead of performing the atime
> updates on the chunks on first encounter right away, these could be
> kept in a list with upper boundary, the utimensat() call deferred
> until the list reached it's maximum or there are not further chunks
> to process. Sorting the list by digest and performing the readdir()
> calls on that ordered list should give similar benefits if multiple
> chunks are to be touched within the same directory AFAIU, but
> without having to read empty directories and with the benefit of
> warming the cache when needed.

I can offer strong empirical support for exactly this design. Since
sending the series I have run the one-pass version on datastore A
(72M chunks) in production, and the one-pass approach shows its limit
there:

- the prefetch itself was fast: 81.8M directory entries in 59m32s;
- phase 1 then started fast, but after ~4 hours collapsed to
  ~100-150 chunks/s. Kernel stack samples of the GC worker showed it
  blocked in zio_wait <- dbuf_read <- zap_get_leaf_byblk (and
  dnode_hold_impl), i.e. re-reading from disk the very metadata the
  prefetch had loaded hours earlier;
- the ARC had recycled those blocks: it was sitting at its adaptive
  target (c = 138G) even though c_max was 250G, so the 7-hour-old
  prefetched blocks were evicted long before phase 1 reached them;
- re-running the same directory walk externally, concurrently with
  phase 1, recovered the rate only modestly: about +30% on the rate
  of first-touched chunks, with cold demand reads persisting at
  ~55/s even right after the walk. With the ARC sitting at its
  adaptive target, blocks warmed minutes earlier are already being
  recycled by the time the marker reaches them.

So on a store where phase 1 runs for hours, neither one warming pass
up front nor periodic full re-walks solve it: the warming has to
happen right before use, which is exactly what your deferred-batch
design does - and it makes the survival question disappear entirely.
It should also compose nicely
with the existing LRU dedup cache: the deferred entries are precisely
the cache misses, so bounding the list relative to the LRU capacity
sounds right to me. And taking the parent directory handle for
utimensat() is a further nice win on top.

For scale: on datastore B, where phase 1 fits well inside the
eviction horizon, the steady state with the one-pass version is
prefetch 12.4s + phase 1 in 12m38s daily (down from 2h14m for
phase 1 alone before).

> If deferring utimensat() calls as suggested above, this could most
> likely be implemented without much hustle, requiring sorting by
> inode instead of by digest.

Agreed - once the touches go through a sorted batch, the sort key is
pluggable and sort-by-inode becomes almost free to try.

I will rework the series (v2) along these lines: deferred bounded
list of pending touches, sorted flush with readdir on the directories
actually needed, parent-dir file handles for utimensat(), and the
iterator's hex filtering made optional as you suggested. Two
questions before I start:

1. With the deferred design the warming happens on demand, so the
   original gc-chunk-metadata-prefetch tuning option loses most of
   its meaning. Would you prefer the batched behaviour to be
   unconditional (no new option), or should it stay behind a knob?

2. Any preference on the list bound - a fixed count, or derived from
   gc-cache-capacity? One observation: sorted by digest, a flush of N
   entries spans min(N, 65536) directories, so the bound also sets
   the readdir amplification - N/65536 chunks actually used per
   directory read. A large, LRU-sized bound (~8M entries is only a
   few hundred MB of digests) gives ~128 used chunks per readdir,
   while a small bound would warm ~1100 dnodes per directory to use
   a handful.

Thanks,
Enrico

Il giorno lun 10 ago 2026 alle ore 12:13 Christian Ebner <
c.ebner@proxmox.com> ha scritto:

> Hi Enrico,
>
> thanks for your contribution! For it to be considered please send the
> signed contributors license agreement as outlined in if you have not
> already done so:
>
>
> https://pbs.proxmox.com/wiki/Developer_Documentation#Software_License_and_Copyright
>
> In the mean time some high level comments.
>
> On 8/10/26 7:02 AM, Enrico Plantulli wrote:
> > Hi,
> >
> > garbage collection phase 1 resolves each chunk from its digest and calls
> > utimensat() on it directly, so it never iterates the chunk directories.
> > On a cold store that means every chunk costs an independent, serialized
> > metadata read, and those reads are issued in digest order - which is
> > uncorrelated with the on-disk layout of the inodes by construction,
> > since the digest picks the directory while the inode number follows
> > creation order.
> >
>
> [..]
>
> >
> > Design notes and open questions, on which I would appreciate guidance:
> >
> > * The win is conditional on the prefetched metadata surviving in the
> >    cache until phase 1 consumes it. On ZFS that is roughly 512 bytes of
> >    dnode per chunk: about 2.5 GiB for the 5.2M chunks sampled from
> >    datastore B (twice that for the full store) and about 34 GiB for
> >    datastore A. On a store whose dnodes do not fit in the ARC the pass
> >    does not pay off. And even where they fit, whether the tail of the
> >    prefetch survives until a phase 1 that runs for hours reaches it -
> >    against the index files phase 1 itself reads, the dnodes it dirties
> >    and concurrent backup traffic - is an open question on a store of A's
> >    size. If it does not, the better design would be prefetch windows
> >    interleaved with marking, which this simple one-pass version does not
> >    attempt. Treat the datastore A figures as motivation, not as a
> >    measured result of this patch.
>
> I think there might be additional performance improvements to be gained
> from this: In particular, instead of performing the atime updates on the
> chunks on first encounter right away, these could be kept in a list with
> upper boundary, the utimensat() call deferred until the list reached
> it's maximum or there are not further chunks to process. Sorting the
> list by digest and performing the readdir() calls on that ordered list
> should give similar benefits if multiple chunks are to be touched within
> the same directory AFAIU, but without having to read empty directories
> and with the benefit of warming the cache when needed. Also, utimensat()
> calls could get the open parent directory file handle, reducing
> filesystem traversal for lookup.
>
> List limits must however be considered with care, might be based on the
> LRU cache size used to avoid multiple atime updates.
>
> > * The warm figures bound the read side only. Phase 1 also dirties every
> >    chunk's dnode through utimensat(), and in digest order two touches
> >    that share a 16 KiB metadnode block are millions of operations apart,
> >    so copy-on-write rewrites the block once per touch instead of once
> >    per block: on datastore A that is ~72M block rewrites, on the order
> >    of 1 TiB of metadnode churn plus a multiple of that in dirtied
> >    indirect blocks. This is the same volume phase 1 writes today, only
> >    compressed into less wall time - a faster phase 1 even coalesces the
> >    indirect block updates better. But it does cap the marking rate well
> >    below what the lstat() figure alone would suggest: with the default
> >    4 GiB zfs_dirty_data_max the ZFS write throttle starts to shape the
> >    rate in the low thousands of utimensat() per second. So the end
> >    state I expect on datastore A is phase 1 bound by metadata
> >    write-back at a few thousand chunks per second - two orders of
> >    magnitude above the 39.2 chunks/s it does today, but not the tens of
> >    thousands the warm read numbers alone might suggest. On datastore B
> >    the end-to-end run stayed below any throttling (~13700 chunks/s with
> >    dmu_tx_dirty_delay at 0), so the cap was not reached there; for A
> >    this remains an estimate, not a measurement.
>
> Definitely worth investigation further as well.
>
> > * Opt-in was the conservative choice. Note that tying it to chunk-order
> >    would not be conservative at all: chunk-order defaults to `inode`, so
> >    that would effectively enable the pass everywhere. If you want it on
> >    by default, I would rather make the option default to true than key it
> >    off chunk-order.
> >
> > * The pass is not free, but it is also not a whole extra pass: GC phase
> >    2 already walks the same directories with the same iterator, so the
> >    prefetch can warm phase 2 as well - though after an hours-long phase 1
> >    that rewrites the dnodes it touches, how much of that warmth is left
> >    for phase 2 is equally open.
> >
> > * The pass is best effort: a failure is logged and ignored, except for
> >    abort and shutdown requests, which are re-checked in the error path so
> >    that cancelling the task still stops the collection.
> >
> > * It reuses get_chunk_store_iterator(), the same iterator phase 2 uses,
> >    so there is no second implementation of the chunk directory walk. The
> >    per-entry hex filtering is redundant for this use, but reusing the
> >    iterator seemed better than duplicating the walk.
>
> This helper is and internal implementation, so could be extended to make
> the filtering optional.
>
> > * The pass is sequential, one directory at a time, so it keeps the queue
> >    depth of the pool low - the same limitation that makes phase 1 slow in
> >    the first place. Parallelising it over ranges of the 65536
> >    subdirectories would likely help further on wide pools, but it would
> >    need a second walk implementation instead of reusing
> >    get_chunk_store_iterator(), so I left it out of this first version.
> >    Happy to add it if you would take it.
>
> This could be added at a later point IMO.
>
> > * This is complementary to the LRU cache added in [0] for #5331: that
> >    commit removes redundant atime updates, this one makes the remaining
> >    ones cheap. It does not change what phase 1 marks, only what it has to
> >    wait for.
> >
> > * A larger version of the same idea would be to have phase 1 itself walk
> >    in inode order, the way chunk-order=inode already does for verify.
> >    That is a much more invasive change and I did not attempt it; the
> >    readdir pass gets most of the benefit for a fraction of the risk.
>
> If deferring utimensat() calls as suggested above, this could most
> likely be implemented without much hustle, requiring sorting by inode
> instead of by digest.
>
> >
> > [0]
> https://git.proxmox.com/?p=proxmox-backup.git;a=commit;h=03143eee0a59cf319be0052e139f7e20e124d572
>
>
>
>

[-- Attachment #2: Type: text/html, Size: 12893 bytes --]

      reply	other threads:[~2026-08-10 20:32 UTC|newest]

Thread overview: 8+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-10  5:01 [PATCH proxmox proxmox-backup 0/3] datastore: gc: prefetch chunk metadata before phase 1 Enrico Plantulli
2026-08-10  5:01 ` [PATCH proxmox 1/3] pbs-api-types: add gc chunk metadata prefetch tuning option Enrico Plantulli
2026-08-10  5:01 ` [PATCH proxmox-backup 2/3] datastore: gc: optionally prefetch chunk metadata before phase 1 Enrico Plantulli
2026-08-10 10:19   ` Christian Ebner
2026-08-10 15:05     ` Enrico Plantulli
2026-08-10  5:02 ` [PATCH proxmox-backup 3/3] ui: tuning: add GC chunk metadata prefetch option Enrico Plantulli
2026-08-10 10:13 ` [PATCH proxmox proxmox-backup 0/3] datastore: gc: prefetch chunk metadata before phase 1 Christian Ebner
2026-08-10 20:32   ` Enrico Plant [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAKymmRLJ_6C_4io=rgWiALDrMvtB4z7EbW3iu5CxQdrEFWu6Hg@mail.gmail.com' \
    --to=plantulli@gmail.com \
    --cc=c.ebner@proxmox.com \
    --cc=pbs-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal