From 78d6a5c1f0c71b7c3e2ff925a6d8efa36e12a8a4 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Mon, 18 Jul 2022 15:13:27 -0700 Subject: [PATCH v8 4/6] Add eager freezing strategy to VACUUM. Avoid large build-ups of all-visible pages by making non-aggressive VACUUMs freeze pages proactively for VACUUMs/tables where eager vacuuming is deemed appropriate. Use of the eager strategy (an alternative to the classic lazy freezing strategy) is controlled by a new GUC, vacuum_freeze_strategy_threshold (and an associated autovacuum_* reloption). Tables whose rel_pages are >= the cutoff will have VACUUM use the eager freezing strategy. Otherwise we use the lazy freezing strategy, which is the classic approach. When the eager strategy is in use, lazy_scan_prune will trigger freezing a page's tuples at the point that it notices that it will at least become all-visible -- it can be made all-frozen instead. We still respect FreezeLimit, though: the presence of any XID < FreezeLimit also triggers page-level freezing (just as it would with the lazy strategy). If and when a smaller table (a table that uses lazy freezing at first) grows past the table size threshold, the next VACUUM against the table shouldn't have to do too much extra freezing to catch up when we perform eager freezing for the first time (the table still won't be very large). Once VACUUM has caught up, the amount of work required in each VACUUM operation should be roughly proportionate to the number of new pages, at least with a pure append-only table. In summary, we try to get the benefit of the lazy freezing strategy, without ever allowing VACUUM to fall uncomfortably far behind. In particular, we avoid accumulating an excessive number of unfrozen all-visible pages in any one table. This approach is often enough to keep relfrozenxid recent, but we still have antiwraparound autovacuums for tables where it doesn't work out that way. Note that freezing strategy is distinct from (though related to) the strategy for skipping pages with the visibility map. In practice tables that use eager freezing always eagerly scan all-visible pages (they prioritize advancing relfrozenxid), partly because we expect few or no all-visible pages there (at least during the second or subsequent VACUUM that uses eager freezing). When VACUUM uses the classic/lazy freezing strategy, VACUUM will also scan pages eagerly (i.e. it will scan any all-visible pages and only skip all-frozen pages) when the added cost is relatively low. This is preparation for an upcoming commit that completely removes aggressive mode VACUUMs. Author: Peter Geoghegan Reviewed-By: Jeff Davis Reviewed-By: Andres Freund Discussion: https://postgr.es/m/CAH2-WzkFok_6EAHuK39GaW4FjEFQsY=3J0AAd6FXk93u-Xq3Fg@mail.gmail.com --- src/include/access/heapam.h | 6 ++ src/include/commands/vacuum.h | 10 ++++ src/include/utils/rel.h | 1 + src/backend/access/common/reloptions.c | 11 ++++ src/backend/access/heap/heapam.c | 13 ++++- src/backend/access/heap/vacuumlazy.c | 55 ++++++++++++++++--- src/backend/commands/vacuum.c | 16 +++++- src/backend/postmaster/autovacuum.c | 10 ++++ src/backend/utils/misc/guc_tables.c | 11 ++++ src/backend/utils/misc/postgresql.conf.sample | 1 + doc/src/sgml/config.sgml | 31 ++++++++--- doc/src/sgml/maintenance.sgml | 6 +- doc/src/sgml/ref/create_table.sgml | 14 +++++ 13 files changed, 164 insertions(+), 21 deletions(-) diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index ca4fab970..57d824740 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -131,6 +131,12 @@ typedef struct HeapTupleFreeze * and all unfrozen XIDs or MXIDs that remain after VACUUM finishes _must_ * have values >= the final relfrozenxid/relminmxid values in pg_class. This * includes XIDs that remain as MultiXact members from any tuple's xmax. + * + * When 'freeze_required' flag isn't set after all tuples are examined, the + * final choice on freezing is made by VACUUM itself. We keep open the option + * to freeze or not freeze (a decision that VACUUM makes based on performance + * considerations) by maintaining an alternative set of "no freeze" variants + * of our relfrozenxid/relminmxid trackers in heap_prepare_freeze_tuple. */ typedef struct HeapPageFreeze { diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 02289f42e..122fb93e2 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -220,6 +220,9 @@ typedef struct VacuumParams * use default */ int multixact_freeze_table_age; /* multixact age at which to scan * whole table */ + int freeze_strategy_threshold; /* threshold to use eager + * freezing, in total heap blocks, + * -1 to use default */ bool is_wraparound; /* force a for-wraparound vacuum */ int log_min_duration; /* minimum execution threshold in ms at * which autovacuum is logged, -1 to use @@ -272,6 +275,12 @@ struct VacuumCutoffs */ TransactionId FreezeLimit; MultiXactId MultiXactCutoff; + + /* + * Threshold cutoff point (expressed in # of physical heap rel blocks in + * rel's main fork) for triggering eager/all-visible freezing strategy + */ + BlockNumber freeze_strategy_threshold; }; /* @@ -295,6 +304,7 @@ extern PGDLLIMPORT int vacuum_freeze_min_age; extern PGDLLIMPORT int vacuum_freeze_table_age; extern PGDLLIMPORT int vacuum_multixact_freeze_min_age; extern PGDLLIMPORT int vacuum_multixact_freeze_table_age; +extern PGDLLIMPORT int vacuum_freeze_strategy_threshold; extern PGDLLIMPORT int vacuum_failsafe_age; extern PGDLLIMPORT int vacuum_multixact_failsafe_age; diff --git a/src/include/utils/rel.h b/src/include/utils/rel.h index f383a2fca..e195b63d7 100644 --- a/src/include/utils/rel.h +++ b/src/include/utils/rel.h @@ -308,6 +308,7 @@ typedef struct AutoVacOpts int vacuum_ins_threshold; int analyze_threshold; int vacuum_cost_limit; + int freeze_strategy_threshold; int freeze_min_age; int freeze_max_age; int freeze_table_age; diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c index 75b734489..4b680501c 100644 --- a/src/backend/access/common/reloptions.c +++ b/src/backend/access/common/reloptions.c @@ -260,6 +260,15 @@ static relopt_int intRelOpts[] = }, -1, 1, 10000 }, + { + { + "autovacuum_freeze_strategy_threshold", + "Table size at which VACUUM freezes using eager strategy.", + RELOPT_KIND_HEAP | RELOPT_KIND_TOAST, + ShareUpdateExclusiveLock + }, + -1, 0, INT_MAX + }, { { "autovacuum_freeze_min_age", @@ -1851,6 +1860,8 @@ default_reloptions(Datum reloptions, bool validate, relopt_kind kind) offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, analyze_threshold)}, {"autovacuum_vacuum_cost_limit", RELOPT_TYPE_INT, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, vacuum_cost_limit)}, + {"autovacuum_freeze_strategy_threshold", RELOPT_TYPE_INT, + offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_strategy_threshold)}, {"autovacuum_freeze_min_age", RELOPT_TYPE_INT, offsetof(StdRdOptions, autovacuum) + offsetof(AutoVacOpts, freeze_min_age)}, {"autovacuum_freeze_max_age", RELOPT_TYPE_INT, diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 45cdc1ae8..caa34bd35 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -6466,7 +6466,7 @@ FreezeMultiXactId(MultiXactId multi, HeapTupleHeader tuple, * heap_prepare_freeze_tuple * * Check to see whether any of the XID fields of a tuple (xmin, xmax, xvac) - * are older than the FreezeLimit and/or MultiXactCutoff cutoffs. If so, + * are older than the OldestXmin and/or OldestMxact freeze cutoffs. If so, * setup enough state (in the *frz output argument) to later execute and * WAL-log what caller needs to do for the tuple, and return true. Return * false if nothing can be changed about the tuple right now. @@ -6478,8 +6478,15 @@ FreezeMultiXactId(MultiXactId multi, HeapTupleHeader tuple, * * VACUUM caller must assemble HeapTupleFreeze freeze plan entries for every * tuple that we returned true for, and call heap_freeze_execute_prepared to - * execute freezing. Caller must initialize pagefrz fields for page as a - * whole before first call here for each heap page. + * execute freezing for the page as a whole. Caller must initialize pagefrz + * fields for page as a whole before first call here for each heap page. + * + * VACUUM caller decides on whether or not to freeze the page as a whole. + * We'll often prepare freeze plans for a page that caller just discards. + * However, VACUUM doesn't always get to make a choice; it must freeze when + * pagefrz.freeze_required is set, to ensure that any XIDs < FreezeLimit (and + * MXIDs < MultiXactCutoff) can never be left behind. We make sure that + * VACUUM always follows that rule. * * We sometimes force freezing of xmax MultiXactId values long before it is * strictly necessary to do so just to ensure the FreezeLimit postcondition. diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 145d9f24f..a1984d68e 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -110,7 +110,7 @@ /* * Threshold that controls whether non-aggressive VACUUMs will skip any - * all-visible pages + * all-visible pages when using the lazy freezing strategy */ #define SKIPALLVIS_THRESHOLD_PAGES 0.05 /* i.e. 5% of rel_pages */ @@ -154,6 +154,8 @@ typedef struct LVRelState bool skipallvis; /* Skip (don't scan) all-frozen pages? */ bool skipallfrozen; + /* Eagerly freeze all tuples on pages about to be set all-visible? */ + bool eager_freeze_strategy; /* Wraparound failsafe has been triggered? */ bool failsafe_active; /* Consider index vacuuming bypass optimization? */ @@ -1244,10 +1246,21 @@ lazy_scan_heap(LVRelState *vacrel) } /* - * lazy_scan_strategy() -- Determine skipping strategy. + * lazy_scan_strategy() -- Determine freezing/skipping strategy. * - * Determines if the ongoing VACUUM operation should skip all-visible pages - * for non-aggressive VACUUMs, where advancing relfrozenxid is optional. + * Our traditional/lazy freezing strategy is useful when putting off the work + * of freezing totally avoids work that turns out to have been unnecessary. + * On the other hand we eagerly freeze pages when that strategy spreads out + * the burden of freezing over time. Performance stability is important; no + * one VACUUM operation should need to freeze disproportionately many pages. + * + * Also determines if the ongoing VACUUM operation should skip all-visible + * pages when advancing relfrozenxid is optional. When VACUUM freezes eagerly + * it always also scans pages eagerly, since it's important that relfrozenxid + * advance in affected tables, which are larger. When VACUUM freezes lazily + * it might make sense to scan pages lazily (skip all-visible pages) or + * eagerly (be capable of relfrozenxid advancement), depending on the extra + * cost - we might need to scan only a few extra pages. * * Returns final scanned_pages for the VACUUM operation. */ @@ -1287,17 +1300,35 @@ lazy_scan_strategy(LVRelState *vacrel, scanned_pages_skipallfrozen++; /* - * Okay, now we have all the information we need to decide on a strategy + * Okay, now we have all the information we need to decide on a strategy. + * + * We use the all-visible/eager freezing strategy when a threshold + * controlled by the freeze_strategy_threshold GUC/reloption is crossed. + * VACUUM won't accumulate any unfrozen all-visible pages over time in + * tables above the threshold. The system won't fall behind on freezing. */ + if (rel_pages >= vacrel->cutoffs.freeze_strategy_threshold) { /* - * TODO: Add code for eager freezing strategy here in next commit + * VACUUM of table whose rel_pages now exceeds GUC-based threshold for + * eager freezing. + * + * We always scan all-visible pages when the threshold is crossed, so + * that relfrozenxid can be advanced. There will typically be few or + * no all-visible pages (only all-frozen) in the table anyway, at + * least after the first VACUUM that exceeds the threshold. */ + vacrel->eager_freeze_strategy = true; + vacrel->skipallvis = false; } + else { BlockNumber nextra, nextra_threshold; + /* VACUUM of small table -- use lazy freeze strategy */ + vacrel->eager_freeze_strategy = false; + /* * Decide on whether or not we'll skip all-visible pages. * @@ -1806,8 +1837,18 @@ retry: * * Freeze the page when heap_prepare_freeze_tuple indicates that at least * one XID/MXID from before FreezeLimit/MultiXactCutoff is present. + * + * When ongoing VACUUM opted to use the eager freezing strategy, we freeze + * any page that will become all-visible, making it all-frozen instead. + * (Actually, the all-visible/eager freezing strategy doesn't quite work + * that way. It triggers freezing for pages that it sees will thereby be + * set all-frozen in the VM immediately afterwards -- a stricter test. + * Some pages that can be set all-visible cannot also be set all-frozen, + * even after freezing, due to the presence of lock-only MultiXactIds.) */ - if (pagefrz.freeze_required || tuples_frozen == 0) + if (pagefrz.freeze_required || tuples_frozen == 0 || + (prunestate->all_visible && prunestate->all_frozen && + vacrel->eager_freeze_strategy)) { /* * We're freezing the page. Our final NewRelfrozenXid doesn't need to diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 0fb211845..ffa8eac12 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -67,6 +67,7 @@ int vacuum_freeze_min_age; int vacuum_freeze_table_age; int vacuum_multixact_freeze_min_age; int vacuum_multixact_freeze_table_age; +int vacuum_freeze_strategy_threshold; int vacuum_failsafe_age; int vacuum_multixact_failsafe_age; @@ -263,6 +264,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) params.multixact_freeze_table_age = -1; } + /* Determine freezing strategy later on using GUC or reloption */ + params.freeze_strategy_threshold = -1; + /* user-invoked vacuum is never "for wraparound" */ params.is_wraparound = false; @@ -943,7 +947,8 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params, multixact_freeze_min_age, freeze_table_age, multixact_freeze_table_age, - effective_multixact_freeze_max_age; + effective_multixact_freeze_max_age, + freeze_strategy_threshold; TransactionId nextXID, safeOldestXmin, aggressiveXIDCutoff; @@ -960,6 +965,7 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params, multixact_freeze_min_age = params->multixact_freeze_min_age; freeze_table_age = params->freeze_table_age; multixact_freeze_table_age = params->multixact_freeze_table_age; + freeze_strategy_threshold = params->freeze_strategy_threshold; /* * Acquire OldestXmin. @@ -1070,6 +1076,14 @@ vacuum_set_xid_limits(Relation rel, const VacuumParams *params, errhint("Close open transactions soon to avoid wraparound problems.\n" "You might also need to commit or roll back old prepared transactions, or drop stale replication slots."))); + /* + * Determine the freeze_strategy_threshold to use: as specified by the + * caller, or vacuum_freeze_strategy_threshold + */ + if (freeze_strategy_threshold < 0) + freeze_strategy_threshold = vacuum_freeze_strategy_threshold; + cutoffs->freeze_strategy_threshold = freeze_strategy_threshold; + /* * Assert that all cutoff invariants hold. * diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 601834d4b..72be67da0 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -151,6 +151,7 @@ static int default_freeze_min_age; static int default_freeze_table_age; static int default_multixact_freeze_min_age; static int default_multixact_freeze_table_age; +static int default_freeze_strategy_threshold; /* Memory context for long-lived data */ static MemoryContext AutovacMemCxt; @@ -2010,6 +2011,7 @@ do_autovacuum(void) default_freeze_table_age = 0; default_multixact_freeze_min_age = 0; default_multixact_freeze_table_age = 0; + default_freeze_strategy_threshold = 0; } else { @@ -2017,6 +2019,7 @@ do_autovacuum(void) default_freeze_table_age = vacuum_freeze_table_age; default_multixact_freeze_min_age = vacuum_multixact_freeze_min_age; default_multixact_freeze_table_age = vacuum_multixact_freeze_table_age; + default_freeze_strategy_threshold = vacuum_freeze_strategy_threshold; } ReleaseSysCache(tuple); @@ -2801,6 +2804,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, int freeze_table_age; int multixact_freeze_min_age; int multixact_freeze_table_age; + int freeze_strategy_threshold; int vac_cost_limit; double vac_cost_delay; int log_min_duration; @@ -2850,6 +2854,11 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, ? avopts->multixact_freeze_table_age : default_multixact_freeze_table_age; + freeze_strategy_threshold = (avopts && + avopts->freeze_strategy_threshold >= 0) + ? avopts->freeze_strategy_threshold + : default_freeze_strategy_threshold; + tab = palloc(sizeof(autovac_table)); tab->at_relid = relid; tab->at_sharedrel = classForm->relisshared; @@ -2872,6 +2881,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, tab->at_params.freeze_table_age = freeze_table_age; tab->at_params.multixact_freeze_min_age = multixact_freeze_min_age; tab->at_params.multixact_freeze_table_age = multixact_freeze_table_age; + tab->at_params.freeze_strategy_threshold = freeze_strategy_threshold; tab->at_params.is_wraparound = wraparound; tab->at_params.log_min_duration = log_min_duration; tab->at_vacuum_cost_limit = vac_cost_limit; diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 349dd6a53..d3c8ae87d 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -2503,6 +2503,17 @@ struct config_int ConfigureNamesInt[] = NULL, NULL, NULL }, + { + {"vacuum_freeze_strategy_threshold", PGC_USERSET, CLIENT_CONN_STATEMENT, + gettext_noop("Table size at which VACUUM freezes using eager strategy."), + NULL, + GUC_UNIT_BLOCKS + }, + &vacuum_freeze_strategy_threshold, + (UINT64CONST(4) * 1024 * 1024 * 1024) / BLCKSZ, 0, INT_MAX, + NULL, NULL, NULL + }, + { {"vacuum_defer_cleanup_age", PGC_SIGHUP, REPLICATION_PRIMARY, gettext_noop("Number of transactions by which VACUUM and HOT cleanup should be deferred, if any."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 868d21c35..a409e6281 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -693,6 +693,7 @@ #idle_in_transaction_session_timeout = 0 # in milliseconds, 0 is disabled #idle_session_timeout = 0 # in milliseconds, 0 is disabled #vacuum_freeze_table_age = 150000000 +#vacuum_freeze_strategy_threshold = 4GB #vacuum_freeze_min_age = 50000000 #vacuum_failsafe_age = 1600000000 #vacuum_multixact_freeze_table_age = 150000000 diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 24b1624ba..9c5861bd7 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -9145,6 +9145,21 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; + + vacuum_freeze_strategy_threshold (integer) + + vacuum_freeze_strategy_threshold configuration parameter + + + + + Specifies the cutoff size (in pages) that VACUUM + should use to decide whether to its eager freezing strategy. + The default is 4 gigabytes (4GB). + + + + vacuum_freeze_min_age (integer) @@ -9153,9 +9168,11 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; - Specifies the cutoff age (in transactions) that VACUUM - should use to decide whether to freeze row versions - while scanning a table. + Specifies the cutoff age (in transactions) that + VACUUM should use to decide whether to + trigger freezing of pages that have an older XID. When VACUUM + uses its eager freezing strategy, freezing a page can also be + triggered when the page contains only all-visible tuples. The default is 50 million transactions. Although users can set this value anywhere from zero to one billion, VACUUM will silently limit the effective value to half @@ -9232,10 +9249,10 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv; - Specifies the cutoff age (in multixacts) that VACUUM - should use to decide whether to replace multixact IDs with a newer - transaction ID or multixact ID while scanning a table. The default - is 5 million multixacts. + Specifies the cutoff age (in multixacts) that + VACUUM should use to decide whether to + trigger freezing of pages with an older multixact ID. The + default is 5 million multixacts. Although users can set this value anywhere from zero to one billion, VACUUM will silently limit the effective value to half the value of , diff --git a/doc/src/sgml/maintenance.sgml b/doc/src/sgml/maintenance.sgml index 759ea5ac9..554b3a75d 100644 --- a/doc/src/sgml/maintenance.sgml +++ b/doc/src/sgml/maintenance.sgml @@ -588,9 +588,9 @@ the relfrozenxid column of a table's pg_class row contains the oldest remaining unfrozen XID at the end of the most recent VACUUM that successfully - advanced relfrozenxid (typically the most recent - aggressive VACUUM). Similarly, the - datfrozenxid column of a database's + advanced relfrozenxid. All rows inserted by + transactions older than this cutoff XID are guaranteed to have been frozen. + Similarly, the datfrozenxid column of a database's pg_database row is a lower bound on the unfrozen XIDs appearing in that database — it is just the minimum of the per-table relfrozenxid values within the database. diff --git a/doc/src/sgml/ref/create_table.sgml b/doc/src/sgml/ref/create_table.sgml index c98223b2a..eabbf9e65 100644 --- a/doc/src/sgml/ref/create_table.sgml +++ b/doc/src/sgml/ref/create_table.sgml @@ -1682,6 +1682,20 @@ WITH ( MODULUS numeric_literal, REM + + autovacuum_freeze_strategy_threshold, toast.autovacuum_freeze_strategy_threshold (integer) + + autovacuum_freeze_strategy_threshold storage parameter + + + + + Per-table value for + parameter. + + + + autovacuum_freeze_min_age, toast.autovacuum_freeze_min_age (integer) -- 2.38.1