public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Enrico Plantulli <plantulli@gmail.com>
To: pbs-devel@lists.proxmox.com
Cc: Enrico Plantulli <plantulli@gmail.com>
Subject: [PATCH proxmox-backup 2/3] datastore: gc: optionally prefetch chunk metadata before phase 1
Date: Mon, 10 Aug 2026 07:01:59 +0200	[thread overview]
Message-ID: <20260810050201.347124-3-plantulli@gmail.com> (raw)
In-Reply-To: <20260810050201.347124-1-plantulli@gmail.com>

Phase 1 of garbage collection resolves each chunk from its digest and
calls utimensat() on it directly. It never iterates the chunk
directories, so on a cold store every chunk costs an independent,
serialized metadata read, 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.

On file systems that prefetch inode metadata while iterating a directory
this can be avoided almost entirely: iterating the chunk directories
once brings in that metadata in one go, and phase 1 then finds it
cached.

Measured on a ZFS datastore on spinning disks (7x raidz1 of 4 HDDs, ZFS
2.4.3, cold cache), over 32768 of its 65536 chunk directories, holding
5231473 chunks:

  readdir over those 32768 directories:  270.9 s,  869726 physical reads
  lstat() over the same 5231473 entries:  80.1 s,       0 physical reads

That is 0.166 physical reads per chunk brought in. 94.7% of the
resulting ARC misses were prefetch misses, and zfs_readdir() calls
dmu_prefetch_dnode() for the entries it returns, which is where the win
comes from - getdents itself needs only the directory entry, not the
inode. The pass only helps as long as that metadata is still cached when
phase 1 uses it.

Note that the pass removes the read stalls only: phase 1 still
dirties every chunk's dnode via utimensat(), so once the reads are
cached the marking rate is expected to cap at the pool's metadata
write-back rate, not at readdir/lstat speed.

The pass is not free and it does not help everywhere, so it is opt-in
via the new gc-chunk-metadata-prefetch tuning option, and it is skipped
for S3 backed datastores: there the local chunk store only holds the
cached chunks and the in-use markers, not the full chunk set, so a full
pass over the local chunk directories is not what phase 1 has to wait
for. A failing prefetch is logged and ignored, except for abort and
shutdown requests, which are re-checked in the error path: the pass is
an optimization and must never become a new way for garbage collection
to fail, but it must not swallow a cancellation either.

Signed-off-by: Enrico Plantulli <plantulli@gmail.com>
---
 docs/storage.rst                 | 12 ++++++++++
 pbs-datastore/src/chunk_store.rs | 38 ++++++++++++++++++++++++++++++++
 pbs-datastore/src/datastore.rs   | 23 +++++++++++++++++++
 3 files changed, 73 insertions(+)

diff --git a/docs/storage.rst b/docs/storage.rst
index 2c6ba0a..575ece9 100644
--- a/docs/storage.rst
+++ b/docs/storage.rst
@@ -686,6 +686,18 @@ There are some tuning related options for the datastore that are more advanced:
   cache slots, 1048576 (= 1024 * 1024) being the default, 8388608 (= 8192 *
   1024) the maximum value.
 
+* ``gc-chunk-metadata-prefetch``: Prefetch chunk metadata before GC phase 1:
+  Phase 1 reaches each chunk directly by digest and never iterates the chunk
+  directories. On file systems that prefetch inode metadata while iterating a
+  directory, such as ZFS, a single pass over the chunk directories before
+  phase 1 brings in that metadata in one go, instead of leaving phase 1 to
+  fault it in one chunk at a time. This can reduce phase 1 runtime
+  substantially on large datastores on rotational disks, at the cost of one
+  extra pass. The pass only pays off if the metadata is still cached once
+  phase 1 uses it: on ZFS that is roughly 512 bytes of dnode per chunk, so a
+  datastore whose dnodes do not fit into the ARC will not benefit. It is
+  disabled by default and has no effect on S3 backed datastores.
+
 * ``default-verification-workers`` and ``default-verification-readers``:
   Define the default number of threads used for verification and reading of chunks,
   respectively. By default, 4 threads are used for verification and 1 thread is used
diff --git a/pbs-datastore/src/chunk_store.rs b/pbs-datastore/src/chunk_store.rs
index 6d2ffdb..b595d15 100644
--- a/pbs-datastore/src/chunk_store.rs
+++ b/pbs-datastore/src/chunk_store.rs
@@ -302,6 +302,44 @@ impl ChunkStore {
         Ok(is_bad)
     }
 
+    /// Iterate all chunk directories once, discarding the entries.
+    ///
+    /// No chunk file is opened, read, written or stat'ed, so no chunk access time is touched.
+    /// The readdir does update the access time of the up to 65536 `.chunks/` subdirectories,
+    /// which garbage collection never looks at: phase 2 only ever unlinks regular files, and
+    /// the atime cutoff applies to chunks, not to directories.
+    ///
+    /// The only purpose is to let the file system fault in the chunk inode metadata in bulk, so
+    /// that garbage collection phase 1 finds it cached instead of faulting it in one chunk at a
+    /// time in digest order, which is uncorrelated with the on-disk layout of the inodes.
+    ///
+    /// Returns the number of chunk directory entries seen (chunks, bad chunks and markers).
+    pub fn prefetch_chunk_metadata(&self, worker: &dyn WorkerTaskContext) -> Result<u64, Error> {
+        let mut last_percentage = 0;
+        let mut chunk_count = 0;
+
+        for (entry, percentage, _chunk_ext) in self.get_chunk_store_iterator()? {
+            if last_percentage != percentage {
+                last_percentage = percentage;
+                info!("prefetched {percentage}% ({chunk_count} entries)");
+            }
+
+            worker.check_abort()?;
+            worker.fail_on_shutdown()?;
+
+            if let Err(err) = entry {
+                bail!(
+                    "chunk iterator on chunk store '{}' failed - {err}",
+                    self.name,
+                );
+            }
+
+            chunk_count += 1;
+        }
+
+        Ok(chunk_count)
+    }
+
     fn get_chunk_store_iterator(
         &self,
     ) -> Result<
diff --git a/pbs-datastore/src/datastore.rs b/pbs-datastore/src/datastore.rs
index c3db522..da77134 100644
--- a/pbs-datastore/src/datastore.rs
+++ b/pbs-datastore/src/datastore.rs
@@ -2459,6 +2459,29 @@ impl DataStore {
             1024 * 1024
         };
 
+        if tuning.gc_chunk_metadata_prefetch.unwrap_or(false) {
+            if s3_client.is_some() {
+                info!("Chunk metadata prefetch not supported for S3 backed datastore, skipping.");
+            } else {
+                info!("Start GC chunk metadata prefetch");
+                let start = std::time::Instant::now();
+                // Best effort only: this is an optimization, it must never fail the collection.
+                match self.inner.chunk_store.prefetch_chunk_metadata(worker) {
+                    Ok(chunk_count) => info!(
+                        "Chunk metadata prefetch done, {chunk_count} entries in {}",
+                        TimeSpan::from(start.elapsed()),
+                    ),
+                    Err(err) => {
+                        // an abort or shutdown request must not be swallowed by the
+                        // best effort handling below
+                        worker.check_abort()?;
+                        worker.fail_on_shutdown()?;
+                        warn!("Chunk metadata prefetch failed, continuing without it - {err}");
+                    }
+                }
+            }
+        }
+
         info!("Start GC phase1 (mark used chunks)");
 
         self.mark_used_chunks(

base-commit: 5d95fc20eeae6df1936216b1ab1ab6952472fdcf
-- 
2.47.3




  parent reply	other threads:[~2026-08-10  5:02 UTC|newest]

Thread overview: 7+ 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 ` Enrico Plantulli [this message]
2026-08-10 10:19   ` [PATCH proxmox-backup 2/3] datastore: gc: optionally prefetch chunk metadata before phase 1 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

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=20260810050201.347124-3-plantulli@gmail.com \
    --to=plantulli@gmail.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