From 0afaf310255a068d3c1ca9d2ce6f00118cbff890 Mon Sep 17 00:00:00 2001 From: Peter Geoghegan Date: Fri, 25 Nov 2022 11:23:20 -0800 Subject: [PATCH v5 1/2] Add autovacuum trigger instrumentation. Add new instrumentation that lists a triggering condition in the server log whenever an autovacuum is logged. This reports "table age" as the triggering criteria when antiwraparound autovacuum runs (the XID age trigger case and the MXID age trigger case are represented separately). Other cases are reported as autovacuum trigger when the tuple insert thresholds or the dead tuple thresholds were crossed. Author: Peter Geoghegan Reviewed-By: Andres Freund Reviewed-By: Jeff Davis Discussion: https://postgr.es/m/CAH2-Wz=S-R_2rO49Hm94Nuvhu9_twRGbTm6uwDRmRu-Sqn_t3w@mail.gmail.com --- src/include/commands/vacuum.h | 19 +++- src/backend/access/heap/vacuumlazy.c | 5 ++ src/backend/commands/vacuum.c | 31 ++++++- src/backend/postmaster/autovacuum.c | 124 ++++++++++++++++++--------- 4 files changed, 137 insertions(+), 42 deletions(-) diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h index 689dbb770..13f70a1f6 100644 --- a/src/include/commands/vacuum.h +++ b/src/include/commands/vacuum.h @@ -191,6 +191,21 @@ typedef struct VacAttrStats #define VACOPT_SKIP_DATABASE_STATS 0x100 /* skip vac_update_datfrozenxid() */ #define VACOPT_ONLY_DATABASE_STATS 0x200 /* only vac_update_datfrozenxid() */ +/* + * Values used by autovacuum.c to tell vacuumlazy.c about the specific + * threshold type that triggered an autovacuum worker. + * + * AUTOVACUUM_NONE is used when VACUUM isn't running in an autovacuum worker. + */ +typedef enum AutoVacType +{ + AUTOVACUUM_NONE = 0, + AUTOVACUUM_TABLE_XID_AGE, + AUTOVACUUM_TABLE_MXID_AGE, + AUTOVACUUM_DEAD_TUPLES, + AUTOVACUUM_INSERTED_TUPLES, +} AutoVacType; + /* * Values used by index_cleanup and truncate params. * @@ -222,7 +237,8 @@ typedef struct VacuumParams * use default */ int multixact_freeze_table_age; /* multixact age at which to scan * whole table */ - bool is_wraparound; /* force a for-wraparound vacuum */ + bool is_wraparound; /* antiwraparound autovacuum? */ + AutoVacType trigger; /* autovacuum trigger condition, if any */ int log_min_duration; /* minimum execution threshold in ms at * which autovacuum is logged, -1 to use * default */ @@ -313,6 +329,7 @@ extern void vacuum(List *relations, VacuumParams *params, extern void vac_open_indexes(Relation relation, LOCKMODE lockmode, int *nindexes, Relation **Irel); extern void vac_close_indexes(int nindexes, Relation *Irel, LOCKMODE lockmode); +extern const char *vac_autovacuum_trigger_msg(AutoVacType trigger); extern double vac_estimate_reltuples(Relation relation, BlockNumber total_pages, BlockNumber scanned_pages, diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 8f14cf85f..8a64dce61 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -639,6 +639,8 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, * implies aggressive. Produce distinct output for the corner * case all the same, just in case. */ + Assert(params->trigger == AUTOVACUUM_TABLE_XID_AGE || + params->trigger == AUTOVACUUM_TABLE_MXID_AGE); if (vacrel->aggressive) msgfmt = _("automatic aggressive vacuum to prevent wraparound of table \"%s.%s.%s\": index scans: %d\n"); else @@ -656,6 +658,9 @@ heap_vacuum_rel(Relation rel, VacuumParams *params, vacrel->relnamespace, vacrel->relname, vacrel->num_index_scans); + if (!verbose) + appendStringInfo(&buf, _("triggered by: %s\n"), + vac_autovacuum_trigger_msg(params->trigger)); appendStringInfo(&buf, _("pages: %u removed, %u remain, %u scanned (%.2f%% of total)\n"), vacrel->removed_pages, new_rel_pages, diff --git a/src/backend/commands/vacuum.c b/src/backend/commands/vacuum.c index 7b1a4b127..18278acb5 100644 --- a/src/backend/commands/vacuum.c +++ b/src/backend/commands/vacuum.c @@ -273,8 +273,9 @@ ExecVacuum(ParseState *pstate, VacuumStmt *vacstmt, bool isTopLevel) params.multixact_freeze_table_age = -1; } - /* user-invoked vacuum is never "for wraparound" */ + /* user-invoked vacuum never uses these autovacuum-only flags */ params.is_wraparound = false; + params.trigger = AUTOVACUUM_NONE; /* user-invoked vacuum uses VACOPT_VERBOSE instead of log_min_duration */ params.log_min_duration = -1; @@ -1874,7 +1875,11 @@ vacuum_rel(Oid relid, RangeVar *relation, VacuumParams *params, bool skip_privs) LWLockAcquire(ProcArrayLock, LW_EXCLUSIVE); MyProc->statusFlags |= PROC_IN_VACUUM; if (params->is_wraparound) + { + Assert(params->trigger == AUTOVACUUM_TABLE_XID_AGE || + params->trigger == AUTOVACUUM_TABLE_MXID_AGE); MyProc->statusFlags |= PROC_VACUUM_FOR_WRAPAROUND; + } ProcGlobal->statusFlags[MyProc->pgxactoff] = MyProc->statusFlags; LWLockRelease(ProcArrayLock); } @@ -2176,6 +2181,30 @@ vac_close_indexes(int nindexes, Relation *Irel, LOCKMODE lockmode) pfree(Irel); } +/* + * Return translatable string describing autovacuum trigger threshold type + */ +const char * +vac_autovacuum_trigger_msg(AutoVacType trigger) +{ + switch (trigger) + { + case AUTOVACUUM_NONE: + return _("none"); + case AUTOVACUUM_TABLE_XID_AGE: + return _("table XID age"); + case AUTOVACUUM_TABLE_MXID_AGE: + return _("table MXID age"); + case AUTOVACUUM_DEAD_TUPLES: + return _("dead tuples"); + case AUTOVACUUM_INSERTED_TUPLES: + return _("inserted tuples"); + } + + Assert(false); /* cannot be reached */ + return NULL; +} + /* * vacuum_delay_point --- check for interrupts and cost-based delay. * diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index f5ea381c5..90f9df992 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -327,15 +327,17 @@ static void FreeWorkerInfo(int code, Datum arg); static autovac_table *table_recheck_autovac(Oid relid, HTAB *table_toast_map, TupleDesc pg_class_desc, int effective_multixact_freeze_max_age); -static void recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts, - Form_pg_class classForm, - int effective_multixact_freeze_max_age, - bool *dovacuum, bool *doanalyze, bool *wraparound); -static void relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, - Form_pg_class classForm, - PgStat_StatTabEntry *tabentry, - int effective_multixact_freeze_max_age, - bool *dovacuum, bool *doanalyze, bool *wraparound); +static AutoVacType recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts, + Form_pg_class classForm, + int effective_multixact_freeze_max_age, + bool *dovacuum, bool *doanalyze, + bool *wraparound); +static AutoVacType relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, + Form_pg_class classForm, + PgStat_StatTabEntry *tabentry, + int effective_multixact_freeze_max_age, + bool *dovacuum, bool *doanalyze, + bool *wraparound); static void autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy); @@ -2767,6 +2769,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, autovac_table *tab = NULL; bool wraparound; AutoVacOpts *avopts; + AutoVacType trigger; /* fetch the relation's relcache entry */ classTup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(relid)); @@ -2790,9 +2793,10 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, avopts = &hentry->ar_reloptions; } - recheck_relation_needs_vacanalyze(relid, avopts, classForm, - effective_multixact_freeze_max_age, - &dovacuum, &doanalyze, &wraparound); + trigger = recheck_relation_needs_vacanalyze(relid, avopts, classForm, + effective_multixact_freeze_max_age, + &dovacuum, &doanalyze, + &wraparound); /* OK, it needs something done */ if (doanalyze || dovacuum) @@ -2878,6 +2882,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, 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.is_wraparound = wraparound; + tab->at_params.trigger = trigger; tab->at_params.log_min_duration = log_min_duration; tab->at_vacuum_cost_limit = vac_cost_limit; tab->at_vacuum_cost_delay = vac_cost_delay; @@ -2906,7 +2911,7 @@ table_recheck_autovac(Oid relid, HTAB *table_toast_map, * Fetch the pgstat of a relation and recheck whether a relation * needs to be vacuumed or analyzed. */ -static void +static AutoVacType recheck_relation_needs_vacanalyze(Oid relid, AutoVacOpts *avopts, Form_pg_class classForm, @@ -2916,25 +2921,28 @@ recheck_relation_needs_vacanalyze(Oid relid, bool *wraparound) { PgStat_StatTabEntry *tabentry; + AutoVacType trigger; /* fetch the pgstat table entry */ tabentry = pgstat_fetch_stat_tabentry_ext(classForm->relisshared, relid); - relation_needs_vacanalyze(relid, avopts, classForm, tabentry, - effective_multixact_freeze_max_age, - dovacuum, doanalyze, wraparound); + trigger = relation_needs_vacanalyze(relid, avopts, classForm, tabentry, + effective_multixact_freeze_max_age, + dovacuum, doanalyze, wraparound); /* ignore ANALYZE for toast tables */ if (classForm->relkind == RELKIND_TOASTVALUE) *doanalyze = false; + + return trigger; } /* * relation_needs_vacanalyze * - * Check whether a relation needs to be vacuumed or analyzed; return each into - * "dovacuum" and "doanalyze", respectively. Also return whether the vacuum is + * Check whether a relation needs to be vacuumed or analyzed; set each using + * "dovacuum" and "doanalyze", respectively. Also indicate if the vacuum is * being forced because of Xid or multixact wraparound. * * relopts is a pointer to the AutoVacOpts options (either for itself in the @@ -2966,8 +2974,17 @@ recheck_relation_needs_vacanalyze(Oid relid, * autovacuum_vacuum_threshold GUC variable. Similarly, a vac_scale_factor * value < 0 is substituted with the value of * autovacuum_vacuum_scale_factor GUC variable. Ditto for analyze. + * + * Return value is the condition that triggered autovacuum to run VACUUM + * (useful only when *dovacuum is set). There can only be exactly one + * triggering condition, even when multiple thresholds happened to be crossed + * at the same time. We prefer to return "table XID age" in the event of such + * a conflict, after which we prefer "table MXID age" as the criteria, then + * "dead tuples", with "inserted tuples" placed last. These predecence rules + * are largely arbitrary. We must at least ensure that all antiwraparound + * autovacuums are advertised as triggered by table XID/MXID age criteria. */ -static void +static AutoVacType relation_needs_vacanalyze(Oid relid, AutoVacOpts *relopts, Form_pg_class classForm, @@ -2978,7 +2995,10 @@ relation_needs_vacanalyze(Oid relid, bool *doanalyze, bool *wraparound) { - bool force_vacuum; + TransactionId relfrozenxid = classForm->relfrozenxid; + MultiXactId relminmxid = classForm->relminmxid; + AutoVacType trigger = AUTOVACUUM_NONE; + bool tableagevac; bool av_enabled; float4 reltuples; /* pg_class.reltuples */ @@ -3055,27 +3075,33 @@ relation_needs_vacanalyze(Oid relid, xidForceLimit = recentXid - freeze_max_age; if (xidForceLimit < FirstNormalTransactionId) xidForceLimit -= FirstNormalTransactionId; - force_vacuum = (TransactionIdIsNormal(classForm->relfrozenxid) && - TransactionIdPrecedes(classForm->relfrozenxid, - xidForceLimit)); - if (!force_vacuum) - { - multiForceLimit = recentMulti - multixact_freeze_max_age; - if (multiForceLimit < FirstMultiXactId) - multiForceLimit -= FirstMultiXactId; - force_vacuum = MultiXactIdIsValid(classForm->relminmxid) && - MultiXactIdPrecedes(classForm->relminmxid, multiForceLimit); - } - *wraparound = force_vacuum; + multiForceLimit = recentMulti - multixact_freeze_max_age; + if (multiForceLimit < FirstMultiXactId) + multiForceLimit -= FirstMultiXactId; - /* User disabled it in pg_class.reloptions? (But ignore if at risk) */ - if (!av_enabled && !force_vacuum) + tableagevac = true; + *wraparound = false; + /* See header comments about trigger precedence */ + if (TransactionIdIsNormal(relfrozenxid) && + TransactionIdPrecedes(relfrozenxid, xidForceLimit)) + trigger = AUTOVACUUM_TABLE_XID_AGE; + else if (MultiXactIdIsValid(relminmxid) && + MultiXactIdPrecedes(relminmxid, multiForceLimit)) + trigger = AUTOVACUUM_TABLE_MXID_AGE; + else + tableagevac = false; + + /* User disabled non-table-age autovacuums in pg_class.reloptions? */ + if (!av_enabled && !tableagevac) { *doanalyze = false; *dovacuum = false; - return; + return AUTOVACUUM_NONE; } + /* A table age autovacuum always gets antiwraparound protections */ + *wraparound = tableagevac; + /* * If we found stats for the table, and autovacuum is currently enabled, * make a threshold-based decision whether to vacuum and/or analyze. If @@ -3085,6 +3111,9 @@ relation_needs_vacanalyze(Oid relid, */ if (PointerIsValid(tabentry) && AutoVacuumingActive()) { + bool deadtupvac, + inserttupvac; + reltuples = classForm->reltuples; vactuples = tabentry->dead_tuples; instuples = tabentry->ins_since_vacuum; @@ -3112,9 +3141,19 @@ relation_needs_vacanalyze(Oid relid, NameStr(classForm->relname), vactuples, vacthresh, anltuples, anlthresh); + deadtupvac = (vactuples > vacthresh); + inserttupvac = (vac_ins_base_thresh >= 0 && instuples > vacinsthresh); + /* See header comments about trigger precedence */ + if (!tableagevac) + { + if (deadtupvac) + trigger = AUTOVACUUM_DEAD_TUPLES; + else if (inserttupvac) + trigger = AUTOVACUUM_INSERTED_TUPLES; + } + /* Determine if this table needs vacuum or analyze. */ - *dovacuum = force_vacuum || (vactuples > vacthresh) || - (vac_ins_base_thresh >= 0 && instuples > vacinsthresh); + *dovacuum = (tableagevac || deadtupvac || inserttupvac); *doanalyze = (anltuples > anlthresh); } else @@ -3124,13 +3163,17 @@ relation_needs_vacanalyze(Oid relid, * for anti-wrap purposes. If it's not acted upon, there's no need to * vacuum it. */ - *dovacuum = force_vacuum; + *dovacuum = tableagevac; *doanalyze = false; } /* ANALYZE refuses to work with pg_statistic */ if (relid == StatisticRelationId) *doanalyze = false; + + Assert((trigger != AUTOVACUUM_NONE) == *dovacuum); + + return trigger; } /* @@ -3169,14 +3212,15 @@ autovacuum_do_vac_analyze(autovac_table *tab, BufferAccessStrategy bstrategy) static void autovac_report_activity(autovac_table *tab) { -#define MAX_AUTOVAC_ACTIV_LEN (NAMEDATALEN * 2 + 56) +#define MAX_AUTOVAC_ACTIV_LEN (NAMEDATALEN * 2 + 100) char activity[MAX_AUTOVAC_ACTIV_LEN]; int len; /* Report the command and possible options */ if (tab->at_params.options & VACOPT_VACUUM) snprintf(activity, MAX_AUTOVAC_ACTIV_LEN, - "autovacuum: VACUUM%s", + "autovacuum for %s: VACUUM%s", + vac_autovacuum_trigger_msg(tab->at_params.trigger), tab->at_params.options & VACOPT_ANALYZE ? " ANALYZE" : ""); else snprintf(activity, MAX_AUTOVAC_ACTIV_LEN, -- 2.39.0