From: Samuel Rufinatscha <s.rufinatscha@proxmox.com>
To: pbs-devel@lists.proxmox.com
Subject: [pbs-devel] superseded: [PATCH proxmox-backup v2 0/4] datastore: remove config reload on hot path
Date: Thu, 20 Nov 2025 14:07:27 +0100 [thread overview]
Message-ID: <83333385-ee83-470e-83d1-36932768f5f1@proxmox.com> (raw)
In-Reply-To: <20251114150544.224839-1-s.rufinatscha@proxmox.com>
https://lore.proxmox.com/pbs-devel/20251120130342.248815-1-s.rufinatscha@proxmox.com/T/#t
On 11/14/25 4:05 PM, Samuel Rufinatscha wrote:
> Hi,
>
> this series reduces CPU time in datastore lookups by avoiding repeated
> datastore.cfg reads/parses in both `lookup_datastore()` and
> `DataStore::Drop`. It also adds a TTL so manual config edits are
> noticed without reintroducing hashing on every request.
>
> While investigating #6049 [1], cargo-flamegraph [2] showed hotspots during
> repeated `/status` calls in `lookup_datastore()` and in `Drop`,
> dominated by `pbs_config::datastore::config()` (config parse).
>
> The parsing cost itself should eventually be investigated in a future
> effort. Furthermore, cargo-flamegraph showed that when using a
> token-based auth method to access the API, a significant amount of time
> is spent in validation on every request request [3].
>
> ## Approach
>
> [PATCH 1/4] Extend ConfigVersionCache for datastore generation
> Expose a dedicated datastore generation counter and an increment
> helper so callers can cheaply track datastore.cfg versions.
>
> [PATCH 2/4] Fast path for datastore lookups
> Cache the parsed datastore.cfg keyed by the shared datastore
> generation. lookup_datastore() reuses both the cached config and an
> existing DataStoreImpl when the generation matches, and falls back
> to the old slow path otherwise.
>
> [PATCH 3/4] Fast path for Drop
> Make DataStore::Drop use the cached config if possible instead of
> rereading datastore.cfg from disk.
>
> [PATCH 4/4] TTL to catch manual edits
> Add a small TTL around the cached config and bump the datastore
> generation whenever the config is reloaded. This catches manual
> edits to datastore.cfg without reintroducing hashing or
> config parsing on every request.
>
> ## Benchmark results
>
> All the following benchmarks are based on top of
> https://lore.proxmox.com/pbs-devel/20251112131525.645971-1-f.gruenbichler@proxmox.com/T/#u
>
> ### End-to-end
>
> Testing `/status?verbose=0` end-to-end with 1000 stores, 5 req/store
> and parallel=16 before/after the series:
>
> Metric Before After
> ----------------------------------------
> Total time 12s 9s
> Throughput (all) 416.67 555.56
> Cold RPS (round #1) 83.33 111.11
> Warm RPS (#2..N) 333.33 444.44
>
> Running under flamegraph [2], TLS appears to consume a significant
> amount of CPU time and blur the results. Still, a ~33% higher overall
> throughput and ~25% less end-to-end time for this workload.
>
> ### Isolated benchmarks (hyperfine)
>
> In addition to the end-to-end tests, I measured two standalone benchmarks
> with hyperfine, each using a config with 1000
> datastores. `M` is the number of distinct datastores looked up and
> `N` is the number of lookups per datastore.
>
> Drop-direct variant:
>
> Drops the `DataStore` after every lookup, so the `Drop` path runs on
> every iteration:
>
> use anyhow::Error;
>
> use pbs_api_types::Operation;
> use pbs_datastore::DataStore;
>
> fn main() -> Result<(), Error> {
> let mut args = std::env::args();
> args.next();
>
> let datastores = if let Some(n) = args.next() {
> n.parse::<usize>()?
> } else {
> 1000
> };
>
> let iterations = if let Some(n) = args.next() {
> n.parse::<usize>()?
> } else {
> 1000
> };
>
> for d in 1..=datastores {
> let name = format!("ds{:04}", d);
>
> for i in 1..=iterations {
> DataStore::lookup_datastore(&name, Some(Operation::Write))?;
> }
> }
>
> Ok(())
> }
>
> +----+------+-----------+-----------+---------+
> | M | N | Baseline | Patched | Speedup |
> +----+------+-----------+-----------+---------+
> | 1 | 1000 | 1.670 s | 34.3 ms | 48.7x |
> | 10 | 100 | 1.672 s | 34.5 ms | 48.4x |
> | 100| 10 | 1.679 s | 35.1 ms | 47.8x |
> |1000| 1 | 1.787 s | 38.2 ms | 46.8x |
> +----+------+-----------+-----------+---------+
>
> Bulk-drop variant:
>
> Keeps the `DataStore` instances alive for
> all `N` lookups of a given datastore and then drops them in bulk,
> mimicking a task that performs many lookups while it is running and
> only triggers the expensive `Drop` logic when the last user exits.
>
> use anyhow::Error;
>
> use pbs_api_types::Operation;
> use pbs_datastore::DataStore;
>
> fn main() -> Result<(), Error> {
> let mut args = std::env::args();
> args.next();
>
> let datastores = if let Some(n) = args.next() {
> n.parse::<usize>()?
> } else {
> 1000
> };
>
> let iterations = if let Some(n) = args.next() {
> n.parse::<usize>()?
> } else {
> 1000
> };
>
> for d in 1..=datastores {
> let name = format!("ds{:04}", d);
>
> let mut stores = Vec::with_capacity(iterations);
> for i in 1..=iterations {
> stores.push(DataStore::lookup_datastore(&name, Some(Operation::Write))?);
> }
> }
>
> Ok(())
> }
>
> +------+------+---------------+--------------+---------+
> | M | N | Baseline mean | Patched mean | Speedup |
> +------+------+---------------+--------------+---------+
> | 1 | 1000 | 884.0 ms | 33.9 ms | 26.1x |
> | 10 | 100 | 881.8 ms | 35.3 ms | 25.0x |
> | 100 | 10 | 969.3 ms | 35.9 ms | 27.0x |
> | 1000 | 1 | 1827.0 ms | 40.7 ms | 44.9x |
> +------+------+---------------+--------------+---------+
>
> Both variants show that the combination of the cached config lookups
> and the cheaper `Drop` handling reduces the hot-path cost from ~1.7 s
> per run to a few tens of milliseconds in these benchmarks.
>
> ## Reproduction steps
>
> VM: 4 vCPU, ~8 GiB RAM, VirtIO-SCSI; disks:
> - scsi0 32G (OS)
> - scsi1 1000G (datastores)
>
> Install PBS from ISO on the VM.
>
> Set up ZFS on /dev/sdb (adjust if different):
>
> zpool create -f -o ashift=12 pbsbench /dev/sdb
> zfs set mountpoint=/pbsbench pbsbench
> zfs create pbsbench/pbs-bench
>
> Raise file-descriptor limit:
>
> sudo systemctl edit proxmox-backup-proxy.service
>
> Add the following lines:
>
> [Service]
> LimitNOFILE=1048576
>
> Reload systemd and restart the proxy:
>
> sudo systemctl daemon-reload
> sudo systemctl restart proxmox-backup-proxy.service
>
> Verify the limit:
>
> systemctl show proxmox-backup-proxy.service | grep LimitNOFILE
>
> Create 1000 ZFS-backed datastores (as used in #6049 [1]):
>
> seq -w 001 1000 | xargs -n1 -P1 bash -c '
> id=$0
> name="ds${id}"
> dataset="pbsbench/pbs-bench/${name}"
> path="/pbsbench/pbs-bench/${name}"
> zfs create -o mountpoint="$path" "$dataset"
> proxmox-backup-manager datastore create "$name" "$path" \
> --comment "ZFS dataset-based datastore"
> '
>
> Build PBS from this series, then run the server under manually
> under flamegraph:
>
> systemctl stop proxmox-backup-proxy
> cargo flamegraph --release --bin proxmox-backup-proxy
>
> ## Other resources:
>
> ### E2E benchmark script:
>
> #!/usr/bin/env bash
> set -euo pipefail
>
> # --- Config ---------------------------------------------------------------
> HOST='https://localhost:8007'
> USER='root@pam'
> PASS="$(cat passfile)"
>
> DATASTORE_PATH="/pbsbench/pbs-bench"
> MAX_STORES=1000 # how many stores to include
> PARALLEL=16 # concurrent workers
> REPEAT=5 # requests per store (1 cold + REPEAT-1 warm)
>
> PRINT_FIRST=false # true => log first request's HTTP code per store
>
> # --- Helpers --------------------------------------------------------------
> fmt_rps () {
> local n="$1" t="$2"
> awk -v n="$n" -v t="$t" 'BEGIN { if (t > 0) printf("%.2f\n", n/t); else print "0.00" }'
> }
>
> # --- Login ---------------------------------------------------------------
> auth=$(curl -ks -X POST "$HOST/api2/json/access/ticket" \
> -d "username=$USER" -d "password=$PASS")
> ticket=$(echo "$auth" | jq -r '.data.ticket')
>
> if [[ -z "${ticket:-}" || "$ticket" == "null" ]]; then
> echo "[ERROR] Login failed (no ticket)"
> exit 1
> fi
>
> # --- Collect stores (deterministic order) --------------------------------
> mapfile -t STORES < <(
> find "$DATASTORE_PATH" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' \
> | sort | head -n "$MAX_STORES"
> )
>
> USED_STORES=${#STORES[@]}
> if (( USED_STORES == 0 )); then
> echo "[ERROR] No datastore dirs under $DATASTORE_PATH"
> exit 1
> fi
>
> echo "[INFO] Running with stores=$USED_STORES, repeat=$REPEAT, parallel=$PARALLEL"
>
> # --- Temp counters --------------------------------------------------------
> SUCCESS_ALL="$(mktemp)"
> FAIL_ALL="$(mktemp)"
> COLD_OK="$(mktemp)"
> WARM_OK="$(mktemp)"
> trap 'rm -f "$SUCCESS_ALL" "$FAIL_ALL" "$COLD_OK" "$WARM_OK"' EXIT
>
> export HOST ticket REPEAT SUCCESS_ALL FAIL_ALL COLD_OK WARM_OK PRINT_FIRST
>
> SECONDS=0
>
> # --- Fire requests --------------------------------------------------------
> printf "%s\n" "${STORES[@]}" \
> | xargs -P"$PARALLEL" -I{} bash -c '
> store="$1"
> url="$HOST/api2/json/admin/datastore/$store/status?verbose=0"
>
> for ((i=1;i<=REPEAT;i++)); do
> code=$(curl -ks -o /dev/null -w "%{http_code}" -b "PBSAuthCookie=$ticket" "$url" || echo 000)
>
> if [[ "$code" == "200" ]]; then
> echo 1 >> "$SUCCESS_ALL"
> if (( i == 1 )); then
> echo 1 >> "$COLD_OK"
> else
> echo 1 >> "$WARM_OK"
> fi
> if [[ "$PRINT_FIRST" == "true" && $i -eq 1 ]]; then
> ts=$(date +%H:%M:%S)
> echo "[$ts] $store #$i HTTP:200"
> fi
> else
> echo 1 >> "$FAIL_ALL"
> if [[ "$PRINT_FIRST" == "true" && $i -eq 1 ]]; then
> ts=$(date +%H:%M:%S)
> echo "[$ts] $store #$i HTTP:$code (FAIL)"
> fi
> fi
> done
> ' _ {}
>
> # --- Summary --------------------------------------------------------------
> elapsed=$SECONDS
> ok=$(wc -l < "$SUCCESS_ALL" 2>/dev/null || echo 0)
> fail=$(wc -l < "$FAIL_ALL" 2>/dev/null || echo 0)
> cold_ok=$(wc -l < "$COLD_OK" 2>/dev/null || echo 0)
> warm_ok=$(wc -l < "$WARM_OK" 2>/dev/null || echo 0)
>
> expected=$(( USED_STORES * REPEAT ))
> total=$(( ok + fail ))
>
> rps_all=$(fmt_rps "$ok" "$elapsed")
> rps_cold=$(fmt_rps "$cold_ok" "$elapsed")
> rps_warm=$(fmt_rps "$warm_ok" "$elapsed")
>
> echo "===== Summary ====="
> echo "Stores used: $USED_STORES"
> echo "Expected requests: $expected"
> echo "Executed requests: $total"
> echo "OK (HTTP 200): $ok"
> echo "Failed: $fail"
> printf "Total time: %dm %ds\n" $((elapsed/60)) $((elapsed%60))
> echo "Throughput all RPS: $rps_all"
> echo "Cold RPS (round #1): $rps_cold"
> echo "Warm RPS (#2..N): $rps_warm"
>
> ## Maintainer notes
>
> No dependency bumps, no API changes and no breaking changes.
>
> ## Patch summary
>
> [PATCH 1/4] partial fix #6049: config: enable config version cache for datastore
> [PATCH 2/4] partial fix #6049: datastore: impl ConfigVersionCache fast path for lookups
> [PATCH 3/4] partial fix #6049: datastore: use config fast-path in Drop
> [PATCH 4/4] partial fix #6049: datastore: add TTL fallback to catch manual config edits
>
> Thanks,
> Samuel
>
> [1] Bugzilla #6049: https://bugzilla.proxmox.com/show_bug.cgi?id=6049
> [2] cargo-flamegraph: https://github.com/flamegraph-rs/flamegraph
> [3] Bugzilla #7017: https://bugzilla.proxmox.com/show_bug.cgi?id=7017
>
> Samuel Rufinatscha (4):
> partial fix #6049: config: enable config version cache for datastore
> partial fix #6049: datastore: impl ConfigVersionCache fast path for
> lookups
> partial fix #6049: datastore: use config fast-path in Drop
> partial fix #6049: datastore: add TTL fallback to catch manual config
> edits
>
> pbs-config/src/config_version_cache.rs | 10 +-
> pbs-datastore/Cargo.toml | 1 +
> pbs-datastore/src/datastore.rs | 187 +++++++++++++++++++------
> 3 files changed, 152 insertions(+), 46 deletions(-)
>
_______________________________________________
pbs-devel mailing list
pbs-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
prev parent reply other threads:[~2025-11-20 13:07 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-11-14 15:05 [pbs-devel] " Samuel Rufinatscha
2025-11-14 15:05 ` [pbs-devel] [PATCH proxmox-backup v2 1/4] partial fix #6049: config: enable config version cache for datastore Samuel Rufinatscha
2025-11-14 15:05 ` [pbs-devel] [PATCH proxmox-backup v2 2/4] partial fix #6049: datastore: impl ConfigVersionCache fast path for lookups Samuel Rufinatscha
2025-11-19 13:24 ` Fabian Grünbichler
2025-11-14 15:05 ` [pbs-devel] [PATCH proxmox-backup v2 3/4] partial fix #6049: datastore: use config fast-path in Drop Samuel Rufinatscha
2025-11-14 15:05 ` [pbs-devel] [PATCH proxmox-backup v2 4/4] partial fix #6049: datastore: add TTL fallback to catch manual config edits Samuel Rufinatscha
2025-11-19 13:24 ` Fabian Grünbichler
2025-11-19 17:25 ` Samuel Rufinatscha
2025-11-20 13:07 ` Samuel Rufinatscha [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=83333385-ee83-470e-83d1-36932768f5f1@proxmox.com \
--to=s.rufinatscha@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