From 0a1ffefed384d4042383d93e444bc773ae084387 Mon Sep 17 00:00:00 2001 From: Bharath Rupireddy Date: Thu, 22 Oct 2026 18:16:40 +0000 Subject: [PATCH v3] Parallel vacuum: workers together with leader could overrun cost budget Each process taking part in a parallel vacuum sleeps for every cost_limit cost units it incurs on its own, so the leader and its workers each run at the full budget and together overrun it. This commit makes the leader and its workers share the cost limit. For each process, the delay is now calculated as cost_delay * accumulated_balance * active_processes / cost_limit, with a maximum of cost_delay * 4 * active_processes. That maximum grows with the number of processes, so it is also capped at one second, as sleeping any longer than that in pg_usleep() keeps a process from responding to latches and signals promptly. With this change, parallel autovacuum doesn't silently overrun the budget and affect foreground performance, while the VACUUM command with the parallel option can get slower, and users need to tune cost_limit and cost_delay. Oversight in commit 40d964ec99, but back-patched only up to PG19 where parallel autovacuum is added. Author: Bharath Rupireddy Reviewed-by: Masahiko Sawada Reviewed-by: Chao Li Reviewed-by: surya poondla Tested-by: Manu Discussion: https://postgr.es/m/CALj2ACWDhGH_ACJYHvxHVbL9GZ8AgMoJFnf-u5%3DbWx7NZtYBRQ%40mail.gmail.com Backpatch-through: 19 --- doc/src/sgml/config.sgml | 11 ++++++++++- doc/src/sgml/maintenance.sgml | 4 ++-- doc/src/sgml/ref/vacuum.sgml | 7 +++++-- src/backend/commands/vacuum.c | 35 ++++++++++++++++++++++++++--------- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index f7db2995c3e..61fda40d771 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -10012,7 +10012,16 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; delay is calculated as vacuum_cost_delay * accumulated_balance / vacuum_cost_limit with a maximum of - vacuum_cost_delay * 4. + vacuum_cost_delay * 4 for a non-parallel vacuum. + For , each process + uses a share of the limit, so the delay is calculated as + vacuum_cost_delay * + accumulated_balance * + active_processes / + vacuum_cost_limit with a maximum of + vacuum_cost_delay * 4 * + active_processes, and never more than one second + at once. diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index fc89c03279b..e3f5cedd589 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -1051,8 +1051,8 @@ analyze threshold = analyze base threshold + analyze scale factor * number of tu per-table autovacuum_vacuum_cost_delay or autovacuum_vacuum_cost_limit storage parameters have been set are not considered in the balancing algorithm. - Parallel workers launched for are using - the same cost delay parameters as the leader worker. If any of these + For , the leader and its parallel workers + use the same cost delay but share the cost limit. If any of these parameters are changed in the leader worker, it will propagate the new parameter values to all of its parallel workers. diff --git a/doc/src/sgml/ref/vacuum.sgml b/doc/src/sgml/ref/vacuum.sgml index 3c85846df26..25234a17baf 100644 --- a/doc/src/sgml/ref/vacuum.sgml +++ b/doc/src/sgml/ref/vacuum.sgml @@ -481,8 +481,11 @@ VACUUM [ ( option [, ...] ) ] [ VACUUM causes a substantial increase in I/O traffic, which might cause poor performance for other active sessions. Therefore, it is sometimes advisable to use the cost-based vacuum delay feature. For - parallel vacuum, each worker sleeps in proportion to the work done by that - worker. See for + parallel vacuum, the leader and its parallel workers use the same cost + delay but share the cost limit. Each of them sleeps in proportion to the + work it has done, and sleeps longer when more of them are working at the + same time. + See for details. diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index c7595d59bb2..3a5c1efd6ac 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -2545,16 +2545,22 @@ vacuum_delay_point(bool is_analyze) if (VacuumSharedCostBalance != NULL) msec = compute_parallel_delay(); else if (VacuumCostBalance >= vacuum_cost_limit) + { msec = vacuum_cost_delay * VacuumCostBalance / vacuum_cost_limit; + /* + * Avoid uselessly long delays. The cost can accumulate far higher + * than the limit, as delays are not applied while critical locks are + * held or interrupts are pending. + */ + msec = Min(msec, vacuum_cost_delay * 4); + } + /* Nap if appropriate */ if (msec > 0) { instr_time delay_start; - if (msec > vacuum_cost_delay * 4) - msec = vacuum_cost_delay * 4; - if (track_cost_delay_timing) INSTR_TIME_SET_CURRENT(delay_start); @@ -2649,10 +2655,11 @@ vacuum_delay_point(bool is_analyze) * We allow each worker to update it as and when it has incurred any cost and * then based on that decide whether it needs to sleep. We compute the time * to sleep for a worker based on the cost it has incurred - * (VacuumCostBalanceLocal) and then reduce the VacuumSharedCostBalance by - * that amount. This avoids putting to sleep those workers which have done less - * I/O than other workers and therefore ensure that workers - * which are doing more I/O got throttled more. + * (VacuumCostBalanceLocal) and the number of active workers, as the cost + * limit is for the whole vacuum and not for each worker, and then reduce the + * VacuumSharedCostBalance by that amount. This avoids putting to sleep those + * workers which have done less I/O than other workers and therefore ensure + * that workers which are doing more I/O got throttled more. * * We allow a worker to sleep only if it has performed I/O above a certain * threshold, which is calculated based on the number of active workers @@ -2685,8 +2692,18 @@ compute_parallel_delay(void) if ((shared_balance >= vacuum_cost_limit) && (VacuumCostBalanceLocal > 0.5 * ((double) vacuum_cost_limit / nworkers))) { - /* Compute sleep time based on the local cost balance */ - msec = vacuum_cost_delay * VacuumCostBalanceLocal / vacuum_cost_limit; + /* Compute sleep time based on the local cost balance and nworkers */ + msec = vacuum_cost_delay * VacuumCostBalanceLocal * nworkers / + vacuum_cost_limit; + + /* + * Avoid uselessly long delays, as in a non-parallel vacuum. Also + * never sleep more than 1s at once, as sleeping any longer in + * pg_usleep() keeps the process from responding to latches and + * signals promptly. + */ + msec = Min(msec, Min(vacuum_cost_delay * 4 * nworkers, 1000)); + pg_atomic_sub_fetch_u32(VacuumSharedCostBalance, VacuumCostBalanceLocal); VacuumCostBalanceLocal = 0; } -- 2.47.3