public inbox for pbs-devel@lists.proxmox.com
 help / color / mirror / Atom feed
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


      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
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal