1
0
mirror of https://github.com/postgres/postgres.git synced 2025-08-31 17:02:12 +03:00

snapshot scalability: Don't compute global horizons while building snapshots.

To make GetSnapshotData() more scalable, it cannot not look at at each proc's
xmin: While snapshot contents do not need to change whenever a read-only
transaction commits or a snapshot is released, a proc's xmin is modified in
those cases. The frequency of xmin modifications leads to, particularly on
higher core count systems, many cache misses inside GetSnapshotData(), despite
the data underlying a snapshot not changing. That is the most
significant source of GetSnapshotData() scaling poorly on larger systems.

Without accessing xmins, GetSnapshotData() cannot calculate accurate horizons /
thresholds as it has so far. But we don't really have to: The horizons don't
actually change that much between GetSnapshotData() calls. Nor are the horizons
actually used every time a snapshot is built.

The trick this commit introduces is to delay computation of accurate horizons
until there use and using horizon boundaries to determine whether accurate
horizons need to be computed.

The use of RecentGlobal[Data]Xmin to decide whether a row version could be
removed has been replaces with new GlobalVisTest* functions.  These use two
thresholds to determine whether a row can be pruned:
1) definitely_needed, indicating that rows deleted by XIDs >= definitely_needed
   are definitely still visible.
2) maybe_needed, indicating that rows deleted by XIDs < maybe_needed can
   definitely be removed
GetSnapshotData() updates definitely_needed to be the xmin of the computed
snapshot.

When testing whether a row can be removed (with GlobalVisTestIsRemovableXid())
and the tested XID falls in between the two (i.e. XID >= maybe_needed && XID <
definitely_needed) the boundaries can be recomputed to be more accurate. As it
is not cheap to compute accurate boundaries, we limit the number of times that
happens in short succession.  As the boundaries used by
GlobalVisTestIsRemovableXid() are never reset (with maybe_needed updated by
GetSnapshotData()), it is likely that further test can benefit from an earlier
computation of accurate horizons.

To avoid regressing performance when old_snapshot_threshold is set (as that
requires an accurate horizon to be computed), heap_page_prune_opt() doesn't
unconditionally call TransactionIdLimitedForOldSnapshots() anymore. Both the
computation of the limited horizon, and the triggering of errors (with
SetOldSnapshotThresholdTimestamp()) is now only done when necessary to remove
tuples.

This commit just removes the accesses to PGXACT->xmin from
GetSnapshotData(), but other members of PGXACT residing in the same
cache line are accessed. Therefore this in itself does not result in a
significant improvement. Subsequent commits will take advantage of the
fact that GetSnapshotData() now does not need to access xmins anymore.

Note: This contains a workaround in heap_page_prune_opt() to keep the
snapshot_too_old tests working. While that workaround is ugly, the tests
currently are not meaningful, and it seems best to address them separately.

Author: Andres Freund <andres@anarazel.de>
Reviewed-By: Robert Haas <robertmhaas@gmail.com>
Reviewed-By: Thomas Munro <thomas.munro@gmail.com>
Reviewed-By: David Rowley <dgrowleyml@gmail.com>
Discussion: https://postgr.es/m/20200301083601.ews6hz5dduc3w2se@alap3.anarazel.de
This commit is contained in:
Andres Freund
2020-08-12 16:03:49 -07:00
parent 1f42d35a1d
commit dc7420c2c9
38 changed files with 1466 additions and 570 deletions

View File

@@ -793,3 +793,29 @@ ginvacuumcleanup(IndexVacuumInfo *info, IndexBulkDeleteResult *stats)
return stats;
}
/*
* Return whether Page can safely be recycled.
*/
bool
GinPageIsRecyclable(Page page)
{
TransactionId delete_xid;
if (PageIsNew(page))
return true;
if (!GinPageIsDeleted(page))
return false;
delete_xid = GinPageGetDeleteXid(page);
if (!TransactionIdIsValid(delete_xid))
return true;
/*
* If no backend still could view delete_xid as in running, all scans
* concurrent with ginDeletePage() must have finished.
*/
return GlobalVisCheckRemovableXid(NULL, delete_xid);
}

View File

@@ -891,15 +891,13 @@ gistPageRecyclable(Page page)
* As long as that can happen, we must keep the deleted page around as
* a tombstone.
*
* Compare the deletion XID with RecentGlobalXmin. If deleteXid <
* RecentGlobalXmin, then no scan that's still in progress could have
* For that check if the deletion XID could still be visible to
* anyone. If not, then no scan that's still in progress could have
* seen its downlink, and we can recycle it.
*/
FullTransactionId deletexid_full = GistPageGetDeleteXid(page);
FullTransactionId recentxmin_full = GetFullRecentGlobalXmin();
if (FullTransactionIdPrecedes(deletexid_full, recentxmin_full))
return true;
return GlobalVisIsRemovableFullXid(NULL, deletexid_full);
}
return false;
}

View File

@@ -387,11 +387,11 @@ gistRedoPageReuse(XLogReaderState *record)
* PAGE_REUSE records exist to provide a conflict point when we reuse
* pages in the index via the FSM. That's all they do though.
*
* latestRemovedXid was the page's deleteXid. The deleteXid <
* RecentGlobalXmin test in gistPageRecyclable() conceptually mirrors the
* pgxact->xmin > limitXmin test in GetConflictingVirtualXIDs().
* Consequently, one XID value achieves the same exclusion effect on
* primary and standby.
* latestRemovedXid was the page's deleteXid. The
* GlobalVisIsRemovableFullXid(deleteXid) test in gistPageRecyclable()
* conceptually mirrors the pgxact->xmin > limitXmin test in
* GetConflictingVirtualXIDs(). Consequently, one XID value achieves the
* same exclusion effect on primary and standby.
*/
if (InHotStandby)
{

View File

@@ -1517,6 +1517,7 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
bool at_chain_start;
bool valid;
bool skip;
GlobalVisState *vistest = NULL;
/* If this is not the first call, previous call returned a (live!) tuple */
if (all_dead)
@@ -1527,7 +1528,8 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
at_chain_start = first_call;
skip = !first_call;
Assert(TransactionIdIsValid(RecentGlobalXmin));
/* XXX: we should assert that a snapshot is pushed or registered */
Assert(TransactionIdIsValid(RecentXmin));
Assert(BufferGetBlockNumber(buffer) == blkno);
/* Scan through possible multiple members of HOT-chain */
@@ -1616,9 +1618,14 @@ heap_hot_search_buffer(ItemPointer tid, Relation relation, Buffer buffer,
* Note: if you change the criterion here for what is "dead", fix the
* planner's get_actual_variable_range() function to match.
*/
if (all_dead && *all_dead &&
!HeapTupleIsSurelyDead(heapTuple, RecentGlobalXmin))
*all_dead = false;
if (all_dead && *all_dead)
{
if (!vistest)
vistest = GlobalVisTestFor(relation);
if (!HeapTupleIsSurelyDead(heapTuple, vistest))
*all_dead = false;
}
/*
* Check to see if HOT chain continues past this tuple; if so fetch

View File

@@ -1203,7 +1203,7 @@ heapam_index_build_range_scan(Relation heapRelation,
/* okay to ignore lazy VACUUMs here */
if (!IsBootstrapProcessingMode() && !indexInfo->ii_Concurrent)
OldestXmin = GetOldestXmin(heapRelation, PROCARRAY_FLAGS_VACUUM);
OldestXmin = GetOldestNonRemovableTransactionId(heapRelation);
if (!scan)
{
@@ -1244,6 +1244,17 @@ heapam_index_build_range_scan(Relation heapRelation,
hscan = (HeapScanDesc) scan;
/*
* Must have called GetOldestNonRemovableTransactionId() if using
* SnapshotAny. Shouldn't have for an MVCC snapshot. (It's especially
* worth checking this for parallel builds, since ambuild routines that
* support parallel builds must work these details out for themselves.)
*/
Assert(snapshot == SnapshotAny || IsMVCCSnapshot(snapshot));
Assert(snapshot == SnapshotAny ? TransactionIdIsValid(OldestXmin) :
!TransactionIdIsValid(OldestXmin));
Assert(snapshot == SnapshotAny || !anyvisible);
/* Publish number of blocks to scan */
if (progress)
{
@@ -1263,17 +1274,6 @@ heapam_index_build_range_scan(Relation heapRelation,
nblocks);
}
/*
* Must call GetOldestXmin() with SnapshotAny. Should never call
* GetOldestXmin() with MVCC snapshot. (It's especially worth checking
* this for parallel builds, since ambuild routines that support parallel
* builds must work these details out for themselves.)
*/
Assert(snapshot == SnapshotAny || IsMVCCSnapshot(snapshot));
Assert(snapshot == SnapshotAny ? TransactionIdIsValid(OldestXmin) :
!TransactionIdIsValid(OldestXmin));
Assert(snapshot == SnapshotAny || !anyvisible);
/* set our scan endpoints */
if (!allow_sync)
heap_setscanlimits(scan, start_blockno, numblocks);

View File

@@ -1154,19 +1154,56 @@ HeapTupleSatisfiesMVCC(HeapTuple htup, Snapshot snapshot,
* we mainly want to know is if a tuple is potentially visible to *any*
* running transaction. If so, it can't be removed yet by VACUUM.
*
* OldestXmin is a cutoff XID (obtained from GetOldestXmin()). Tuples
* deleted by XIDs >= OldestXmin are deemed "recently dead"; they might
* still be visible to some open transaction, so we can't remove them,
* even if we see that the deleting transaction has committed.
* OldestXmin is a cutoff XID (obtained from
* GetOldestNonRemovableTransactionId()). Tuples deleted by XIDs >=
* OldestXmin are deemed "recently dead"; they might still be visible to some
* open transaction, so we can't remove them, even if we see that the deleting
* transaction has committed.
*/
HTSV_Result
HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin,
Buffer buffer)
{
TransactionId dead_after = InvalidTransactionId;
HTSV_Result res;
res = HeapTupleSatisfiesVacuumHorizon(htup, buffer, &dead_after);
if (res == HEAPTUPLE_RECENTLY_DEAD)
{
Assert(TransactionIdIsValid(dead_after));
if (TransactionIdPrecedes(dead_after, OldestXmin))
res = HEAPTUPLE_DEAD;
}
else
Assert(!TransactionIdIsValid(dead_after));
return res;
}
/*
* Work horse for HeapTupleSatisfiesVacuum and similar routines.
*
* In contrast to HeapTupleSatisfiesVacuum this routine, when encountering a
* tuple that could still be visible to some backend, stores the xid that
* needs to be compared with the horizon in *dead_after, and returns
* HEAPTUPLE_RECENTLY_DEAD. The caller then can perform the comparison with
* the horizon. This is e.g. useful when comparing with different horizons.
*
* Note: HEAPTUPLE_DEAD can still be returned here, e.g. if the inserting
* transaction aborted.
*/
HTSV_Result
HeapTupleSatisfiesVacuumHorizon(HeapTuple htup, Buffer buffer, TransactionId *dead_after)
{
HeapTupleHeader tuple = htup->t_data;
Assert(ItemPointerIsValid(&htup->t_self));
Assert(htup->t_tableOid != InvalidOid);
Assert(dead_after != NULL);
*dead_after = InvalidTransactionId;
/*
* Has inserting transaction committed?
@@ -1323,17 +1360,15 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin,
else if (TransactionIdDidCommit(xmax))
{
/*
* The multixact might still be running due to lockers. If the
* updater is below the xid horizon, we have to return DEAD
* regardless -- otherwise we could end up with a tuple where the
* updater has to be removed due to the horizon, but is not pruned
* away. It's not a problem to prune that tuple, because any
* remaining lockers will also be present in newer tuple versions.
* The multixact might still be running due to lockers. Need to
* allow for pruning if below the xid horizon regardless --
* otherwise we could end up with a tuple where the updater has to
* be removed due to the horizon, but is not pruned away. It's
* not a problem to prune that tuple, because any remaining
* lockers will also be present in newer tuple versions.
*/
if (!TransactionIdPrecedes(xmax, OldestXmin))
return HEAPTUPLE_RECENTLY_DEAD;
return HEAPTUPLE_DEAD;
*dead_after = xmax;
return HEAPTUPLE_RECENTLY_DEAD;
}
else if (!MultiXactIdIsRunning(HeapTupleHeaderGetRawXmax(tuple), false))
{
@@ -1372,14 +1407,11 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin,
}
/*
* Deleter committed, but perhaps it was recent enough that some open
* transactions could still see the tuple.
* Deleter committed, allow caller to check if it was recent enough that
* some open transactions could still see the tuple.
*/
if (!TransactionIdPrecedes(HeapTupleHeaderGetRawXmax(tuple), OldestXmin))
return HEAPTUPLE_RECENTLY_DEAD;
/* Otherwise, it's dead and removable */
return HEAPTUPLE_DEAD;
*dead_after = HeapTupleHeaderGetRawXmax(tuple);
return HEAPTUPLE_RECENTLY_DEAD;
}
@@ -1393,14 +1425,28 @@ HeapTupleSatisfiesVacuum(HeapTuple htup, TransactionId OldestXmin,
*
* This is an interface to HeapTupleSatisfiesVacuum that's callable via
* HeapTupleSatisfiesSnapshot, so it can be used through a Snapshot.
* snapshot->xmin must have been set up with the xmin horizon to use.
* snapshot->vistest must have been set up with the horizon to use.
*/
static bool
HeapTupleSatisfiesNonVacuumable(HeapTuple htup, Snapshot snapshot,
Buffer buffer)
{
return HeapTupleSatisfiesVacuum(htup, snapshot->xmin, buffer)
!= HEAPTUPLE_DEAD;
TransactionId dead_after = InvalidTransactionId;
HTSV_Result res;
res = HeapTupleSatisfiesVacuumHorizon(htup, buffer, &dead_after);
if (res == HEAPTUPLE_RECENTLY_DEAD)
{
Assert(TransactionIdIsValid(dead_after));
if (GlobalVisTestIsRemovableXid(snapshot->vistest, dead_after))
res = HEAPTUPLE_DEAD;
}
else
Assert(!TransactionIdIsValid(dead_after));
return res != HEAPTUPLE_DEAD;
}
@@ -1418,7 +1464,7 @@ HeapTupleSatisfiesNonVacuumable(HeapTuple htup, Snapshot snapshot,
* if the tuple is removable.
*/
bool
HeapTupleIsSurelyDead(HeapTuple htup, TransactionId OldestXmin)
HeapTupleIsSurelyDead(HeapTuple htup, GlobalVisState *vistest)
{
HeapTupleHeader tuple = htup->t_data;
@@ -1459,7 +1505,8 @@ HeapTupleIsSurelyDead(HeapTuple htup, TransactionId OldestXmin)
return false;
/* Deleter committed, so tuple is dead if the XID is old enough. */
return TransactionIdPrecedes(HeapTupleHeaderGetRawXmax(tuple), OldestXmin);
return GlobalVisTestIsRemovableXid(vistest,
HeapTupleHeaderGetRawXmax(tuple));
}
/*

View File

@@ -23,12 +23,30 @@
#include "miscadmin.h"
#include "pgstat.h"
#include "storage/bufmgr.h"
#include "utils/snapmgr.h"
#include "utils/rel.h"
#include "utils/snapmgr.h"
/* Working data for heap_page_prune and subroutines */
typedef struct
{
Relation rel;
/* tuple visibility test, initialized for the relation */
GlobalVisState *vistest;
/*
* Thresholds set by TransactionIdLimitedForOldSnapshots() if they have
* been computed (done on demand, and only if
* OldSnapshotThresholdActive()). The first time a tuple is about to be
* removed based on the limited horizon, old_snap_used is set to true, and
* SetOldSnapshotThresholdTimestamp() is called. See
* heap_prune_satisfies_vacuum().
*/
TimestampTz old_snap_ts;
TransactionId old_snap_xmin;
bool old_snap_used;
TransactionId new_prune_xid; /* new prune hint value for page */
TransactionId latestRemovedXid; /* latest xid to be removed by this prune */
int nredirected; /* numbers of entries in arrays below */
@@ -43,9 +61,8 @@ typedef struct
} PruneState;
/* Local functions */
static int heap_prune_chain(Relation relation, Buffer buffer,
static int heap_prune_chain(Buffer buffer,
OffsetNumber rootoffnum,
TransactionId OldestXmin,
PruneState *prstate);
static void heap_prune_record_prunable(PruneState *prstate, TransactionId xid);
static void heap_prune_record_redirect(PruneState *prstate,
@@ -65,16 +82,16 @@ static void heap_prune_record_unused(PruneState *prstate, OffsetNumber offnum);
* if there's not any use in pruning.
*
* Caller must have pin on the buffer, and must *not* have a lock on it.
*
* OldestXmin is the cutoff XID used to distinguish whether tuples are DEAD
* or RECENTLY_DEAD (see HeapTupleSatisfiesVacuum).
*/
void
heap_page_prune_opt(Relation relation, Buffer buffer)
{
Page page = BufferGetPage(buffer);
TransactionId prune_xid;
GlobalVisState *vistest;
TransactionId limited_xmin = InvalidTransactionId;
TimestampTz limited_ts = 0;
Size minfree;
TransactionId OldestXmin;
/*
* We can't write WAL in recovery mode, so there's no point trying to
@@ -85,37 +102,55 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
return;
/*
* Use the appropriate xmin horizon for this relation. If it's a proper
* catalog relation or a user defined, additional, catalog relation, we
* need to use the horizon that includes slots, otherwise the data-only
* horizon can be used. Note that the toast relation of user defined
* relations are *not* considered catalog relations.
* XXX: Magic to keep old_snapshot_threshold tests appear "working". They
* currently are broken, and discussion of what to do about them is
* ongoing. See
* https://www.postgresql.org/message-id/20200403001235.e6jfdll3gh2ygbuc%40alap3.anarazel.de
*/
if (old_snapshot_threshold == 0)
SnapshotTooOldMagicForTest();
/*
* First check whether there's any chance there's something to prune,
* determining the appropriate horizon is a waste if there's no prune_xid
* (i.e. no updates/deletes left potentially dead tuples around).
*/
prune_xid = ((PageHeader) page)->pd_prune_xid;
if (!TransactionIdIsValid(prune_xid))
return;
/*
* Check whether prune_xid indicates that there may be dead rows that can
* be cleaned up.
*
* It is OK to apply the old snapshot limit before acquiring the cleanup
* It is OK to check the old snapshot limit before acquiring the cleanup
* lock because the worst that can happen is that we are not quite as
* aggressive about the cleanup (by however many transaction IDs are
* consumed between this point and acquiring the lock). This allows us to
* save significant overhead in the case where the page is found not to be
* prunable.
*/
if (IsCatalogRelation(relation) ||
RelationIsAccessibleInLogicalDecoding(relation))
OldestXmin = RecentGlobalXmin;
else
OldestXmin =
TransactionIdLimitedForOldSnapshots(RecentGlobalDataXmin,
relation);
Assert(TransactionIdIsValid(OldestXmin));
/*
* Let's see if we really need pruning.
*
* Forget it if page is not hinted to contain something prunable that's
* older than OldestXmin.
* Even if old_snapshot_threshold is set, we first check whether the page
* can be pruned without. Both because
* TransactionIdLimitedForOldSnapshots() is not cheap, and because not
* unnecessarily relying on old_snapshot_threshold avoids causing
* conflicts.
*/
if (!PageIsPrunable(page, OldestXmin))
return;
vistest = GlobalVisTestFor(relation);
if (!GlobalVisTestIsRemovableXid(vistest, prune_xid))
{
if (!OldSnapshotThresholdActive())
return;
if (!TransactionIdLimitedForOldSnapshots(GlobalVisTestNonRemovableHorizon(vistest),
relation,
&limited_xmin, &limited_ts))
return;
if (!TransactionIdPrecedes(prune_xid, limited_xmin))
return;
}
/*
* We prune when a previous UPDATE failed to find enough space on the page
@@ -151,7 +186,9 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
* needed */
/* OK to prune */
(void) heap_page_prune(relation, buffer, OldestXmin, true, &ignore);
(void) heap_page_prune(relation, buffer, vistest,
limited_xmin, limited_ts,
true, &ignore);
}
/* And release buffer lock */
@@ -165,8 +202,11 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
*
* Caller must have pin and buffer cleanup lock on the page.
*
* OldestXmin is the cutoff XID used to distinguish whether tuples are DEAD
* or RECENTLY_DEAD (see HeapTupleSatisfiesVacuum).
* vistest is used to distinguish whether tuples are DEAD or RECENTLY_DEAD
* (see heap_prune_satisfies_vacuum and
* HeapTupleSatisfiesVacuum). old_snap_xmin / old_snap_ts need to
* either have been set by TransactionIdLimitedForOldSnapshots, or
* InvalidTransactionId/0 respectively.
*
* If report_stats is true then we send the number of reclaimed heap-only
* tuples to pgstats. (This must be false during vacuum, since vacuum will
@@ -177,7 +217,10 @@ heap_page_prune_opt(Relation relation, Buffer buffer)
* latestRemovedXid.
*/
int
heap_page_prune(Relation relation, Buffer buffer, TransactionId OldestXmin,
heap_page_prune(Relation relation, Buffer buffer,
GlobalVisState *vistest,
TransactionId old_snap_xmin,
TimestampTz old_snap_ts,
bool report_stats, TransactionId *latestRemovedXid)
{
int ndeleted = 0;
@@ -198,6 +241,11 @@ heap_page_prune(Relation relation, Buffer buffer, TransactionId OldestXmin,
* initialize the rest of our working state.
*/
prstate.new_prune_xid = InvalidTransactionId;
prstate.rel = relation;
prstate.vistest = vistest;
prstate.old_snap_xmin = old_snap_xmin;
prstate.old_snap_ts = old_snap_ts;
prstate.old_snap_used = false;
prstate.latestRemovedXid = *latestRemovedXid;
prstate.nredirected = prstate.ndead = prstate.nunused = 0;
memset(prstate.marked, 0, sizeof(prstate.marked));
@@ -220,9 +268,7 @@ heap_page_prune(Relation relation, Buffer buffer, TransactionId OldestXmin,
continue;
/* Process this item or chain of items */
ndeleted += heap_prune_chain(relation, buffer, offnum,
OldestXmin,
&prstate);
ndeleted += heap_prune_chain(buffer, offnum, &prstate);
}
/* Any error while applying the changes is critical */
@@ -323,6 +369,85 @@ heap_page_prune(Relation relation, Buffer buffer, TransactionId OldestXmin,
}
/*
* Perform visiblity checks for heap pruning.
*
* This is more complicated than just using GlobalVisTestIsRemovableXid()
* because of old_snapshot_threshold. We only want to increase the threshold
* that triggers errors for old snapshots when we actually decide to remove a
* row based on the limited horizon.
*
* Due to its cost we also only want to call
* TransactionIdLimitedForOldSnapshots() if necessary, i.e. we might not have
* done so in heap_hot_prune_opt() if pd_prune_xid was old enough. But we
* still want to be able to remove rows that are too new to be removed
* according to prstate->vistest, but that can be removed based on
* old_snapshot_threshold. So we call TransactionIdLimitedForOldSnapshots() on
* demand in here, if appropriate.
*/
static HTSV_Result
heap_prune_satisfies_vacuum(PruneState *prstate, HeapTuple tup, Buffer buffer)
{
HTSV_Result res;
TransactionId dead_after;
res = HeapTupleSatisfiesVacuumHorizon(tup, buffer, &dead_after);
if (res != HEAPTUPLE_RECENTLY_DEAD)
return res;
/*
* If we are already relying on the limited xmin, there is no need to
* delay doing so anymore.
*/
if (prstate->old_snap_used)
{
Assert(TransactionIdIsValid(prstate->old_snap_xmin));
if (TransactionIdPrecedes(dead_after, prstate->old_snap_xmin))
res = HEAPTUPLE_DEAD;
return res;
}
/*
* First check if GlobalVisTestIsRemovableXid() is sufficient to find the
* row dead. If not, and old_snapshot_threshold is enabled, try to use the
* lowered horizon.
*/
if (GlobalVisTestIsRemovableXid(prstate->vistest, dead_after))
res = HEAPTUPLE_DEAD;
else if (OldSnapshotThresholdActive())
{
/* haven't determined limited horizon yet, requests */
if (!TransactionIdIsValid(prstate->old_snap_xmin))
{
TransactionId horizon =
GlobalVisTestNonRemovableHorizon(prstate->vistest);
TransactionIdLimitedForOldSnapshots(horizon, prstate->rel,
&prstate->old_snap_xmin,
&prstate->old_snap_ts);
}
if (TransactionIdIsValid(prstate->old_snap_xmin) &&
TransactionIdPrecedes(dead_after, prstate->old_snap_xmin))
{
/*
* About to remove row based on snapshot_too_old. Need to raise
* the threshold so problematic accesses would error.
*/
Assert(!prstate->old_snap_used);
SetOldSnapshotThresholdTimestamp(prstate->old_snap_ts,
prstate->old_snap_xmin);
prstate->old_snap_used = true;
res = HEAPTUPLE_DEAD;
}
}
return res;
}
/*
* Prune specified line pointer or a HOT chain originating at line pointer.
*
@@ -349,9 +474,7 @@ heap_page_prune(Relation relation, Buffer buffer, TransactionId OldestXmin,
* Returns the number of tuples (to be) deleted from the page.
*/
static int
heap_prune_chain(Relation relation, Buffer buffer, OffsetNumber rootoffnum,
TransactionId OldestXmin,
PruneState *prstate)
heap_prune_chain(Buffer buffer, OffsetNumber rootoffnum, PruneState *prstate)
{
int ndeleted = 0;
Page dp = (Page) BufferGetPage(buffer);
@@ -366,7 +489,7 @@ heap_prune_chain(Relation relation, Buffer buffer, OffsetNumber rootoffnum,
i;
HeapTupleData tup;
tup.t_tableOid = RelationGetRelid(relation);
tup.t_tableOid = RelationGetRelid(prstate->rel);
rootlp = PageGetItemId(dp, rootoffnum);
@@ -401,7 +524,7 @@ heap_prune_chain(Relation relation, Buffer buffer, OffsetNumber rootoffnum,
* either here or while following a chain below. Whichever path
* gets there first will mark the tuple unused.
*/
if (HeapTupleSatisfiesVacuum(&tup, OldestXmin, buffer)
if (heap_prune_satisfies_vacuum(prstate, &tup, buffer)
== HEAPTUPLE_DEAD && !HeapTupleHeaderIsHotUpdated(htup))
{
heap_prune_record_unused(prstate, rootoffnum);
@@ -485,7 +608,7 @@ heap_prune_chain(Relation relation, Buffer buffer, OffsetNumber rootoffnum,
*/
tupdead = recent_dead = false;
switch (HeapTupleSatisfiesVacuum(&tup, OldestXmin, buffer))
switch (heap_prune_satisfies_vacuum(prstate, &tup, buffer))
{
case HEAPTUPLE_DEAD:
tupdead = true;

View File

@@ -788,6 +788,7 @@ lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats,
PROGRESS_VACUUM_MAX_DEAD_TUPLES
};
int64 initprog_val[3];
GlobalVisState *vistest;
pg_rusage_init(&ru0);
@@ -816,6 +817,8 @@ lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats,
vacrelstats->nonempty_pages = 0;
vacrelstats->latestRemovedXid = InvalidTransactionId;
vistest = GlobalVisTestFor(onerel);
/*
* Initialize state for a parallel vacuum. As of now, only one worker can
* be used for an index, so we invoke parallelism only if there are at
@@ -1239,7 +1242,8 @@ lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats,
*
* We count tuples removed by the pruning step as removed by VACUUM.
*/
tups_vacuumed += heap_page_prune(onerel, buf, OldestXmin, false,
tups_vacuumed += heap_page_prune(onerel, buf, vistest, false,
InvalidTransactionId, 0,
&vacrelstats->latestRemovedXid);
/*
@@ -1596,14 +1600,16 @@ lazy_scan_heap(Relation onerel, VacuumParams *params, LVRelStats *vacrelstats,
}
/*
* It's possible for the value returned by GetOldestXmin() to move
* backwards, so it's not wrong for us to see tuples that appear to
* not be visible to everyone yet, while PD_ALL_VISIBLE is already
* set. The real safe xmin value never moves backwards, but
* GetOldestXmin() is conservative and sometimes returns a value
* that's unnecessarily small, so if we see that contradiction it just
* means that the tuples that we think are not visible to everyone yet
* actually are, and the PD_ALL_VISIBLE flag is correct.
* It's possible for the value returned by
* GetOldestNonRemovableTransactionId() to move backwards, so it's not
* wrong for us to see tuples that appear to not be visible to
* everyone yet, while PD_ALL_VISIBLE is already set. The real safe
* xmin value never moves backwards, but
* GetOldestNonRemovableTransactionId() is conservative and sometimes
* returns a value that's unnecessarily small, so if we see that
* contradiction it just means that the tuples that we think are not
* visible to everyone yet actually are, and the PD_ALL_VISIBLE flag
* is correct.
*
* There should never be dead tuples on a page with PD_ALL_VISIBLE
* set, however.

View File

@@ -519,7 +519,8 @@ index_getnext_tid(IndexScanDesc scan, ScanDirection direction)
SCAN_CHECKS;
CHECK_SCAN_PROCEDURE(amgettuple);
Assert(TransactionIdIsValid(RecentGlobalXmin));
/* XXX: we should assert that a snapshot is pushed or registered */
Assert(TransactionIdIsValid(RecentXmin));
/*
* The AM's amgettuple proc finds the next index entry matching the scan

View File

@@ -342,9 +342,9 @@ snapshots and registered snapshots as of the deletion are gone; which is
overly strong, but is simple to implement within Postgres. When marked
dead, a deleted page is labeled with the next-transaction counter value.
VACUUM can reclaim the page for re-use when this transaction number is
older than RecentGlobalXmin. As collateral damage, this implementation
also waits for running XIDs with no snapshots and for snapshots taken
until the next transaction to allocate an XID commits.
guaranteed to be "visible to everyone". As collateral damage, this
implementation also waits for running XIDs with no snapshots and for
snapshots taken until the next transaction to allocate an XID commits.
Reclaiming a page doesn't actually change its state on disk --- we simply
record it in the shared-memory free space map, from which it will be
@@ -411,8 +411,8 @@ page and also the correct place to hold the current value. We can avoid
the cost of walking down the tree in such common cases.
The optimization works on the assumption that there can only be one
non-ignorable leaf rightmost page, and so even a RecentGlobalXmin style
interlock isn't required. We cannot fail to detect that our hint was
non-ignorable leaf rightmost page, and so not even a visible-to-everyone
style interlock required. We cannot fail to detect that our hint was
invalidated, because there can only be one such page in the B-Tree at
any time. It's possible that the page will be deleted and recycled
without a backend's cached page also being detected as invalidated, but

View File

@@ -1097,7 +1097,7 @@ _bt_page_recyclable(Page page)
*/
opaque = (BTPageOpaque) PageGetSpecialPointer(page);
if (P_ISDELETED(opaque) &&
TransactionIdPrecedes(opaque->btpo.xact, RecentGlobalXmin))
GlobalVisCheckRemovableXid(NULL, opaque->btpo.xact))
return true;
return false;
}
@@ -2318,7 +2318,7 @@ _bt_unlink_halfdead_page(Relation rel, Buffer leafbuf, BlockNumber scanblkno,
* updated links to the target, ReadNewTransactionId() suffices as an
* upper bound. Any scan having retained a now-stale link is advertising
* in its PGXACT an xmin less than or equal to the value we read here. It
* will continue to do so, holding back RecentGlobalXmin, for the duration
* will continue to do so, holding back the xmin horizon, for the duration
* of that scan.
*/
page = BufferGetPage(buf);

View File

@@ -808,6 +808,12 @@ _bt_vacuum_needs_cleanup(IndexVacuumInfo *info)
metapg = BufferGetPage(metabuf);
metad = BTPageGetMeta(metapg);
/*
* XXX: If IndexVacuumInfo contained the heap relation, we could be more
* aggressive about vacuuming non catalog relations by passing the table
* to GlobalVisCheckRemovableXid().
*/
if (metad->btm_version < BTREE_NOVAC_VERSION)
{
/*
@@ -817,13 +823,12 @@ _bt_vacuum_needs_cleanup(IndexVacuumInfo *info)
result = true;
}
else if (TransactionIdIsValid(metad->btm_oldest_btpo_xact) &&
TransactionIdPrecedes(metad->btm_oldest_btpo_xact,
RecentGlobalXmin))
GlobalVisCheckRemovableXid(NULL, metad->btm_oldest_btpo_xact))
{
/*
* If any oldest btpo.xact from a previously deleted page in the index
* is older than RecentGlobalXmin, then at least one deleted page can
* be recycled -- don't skip cleanup.
* is visible to everyone, then at least one deleted page can be
* recycled -- don't skip cleanup.
*/
result = true;
}
@@ -1276,14 +1281,13 @@ backtrack:
* own conflict now.)
*
* Backends with snapshots acquired after a VACUUM starts but
* before it finishes could have a RecentGlobalXmin with a
* later xid than the VACUUM's OldestXmin cutoff. These
* backends might happen to opportunistically mark some index
* tuples LP_DEAD before we reach them, even though they may
* be after our cutoff. We don't try to kill these "extra"
* index tuples in _bt_delitems_vacuum(). This keep things
* simple, and allows us to always avoid generating our own
* conflicts.
* before it finishes could have visibility cutoff with a
* later xid than VACUUM's OldestXmin cutoff. These backends
* might happen to opportunistically mark some index tuples
* LP_DEAD before we reach them, even though they may be after
* our cutoff. We don't try to kill these "extra" index
* tuples in _bt_delitems_vacuum(). This keep things simple,
* and allows us to always avoid generating our own conflicts.
*/
Assert(!BTreeTupleIsPivot(itup));
if (!BTreeTupleIsPosting(itup))

View File

@@ -948,11 +948,11 @@ btree_xlog_reuse_page(XLogReaderState *record)
* Btree reuse_page records exist to provide a conflict point when we
* reuse pages in the index via the FSM. That's all they do though.
*
* latestRemovedXid was the page's btpo.xact. The btpo.xact <
* RecentGlobalXmin test in _bt_page_recyclable() conceptually mirrors the
* pgxact->xmin > limitXmin test in GetConflictingVirtualXIDs().
* Consequently, one XID value achieves the same exclusion effect on
* primary and standby.
* latestRemovedXid was the page's btpo.xact. The
* GlobalVisCheckRemovableXid test in _bt_page_recyclable() conceptually
* mirrors the pgxact->xmin > limitXmin test in
* GetConflictingVirtualXIDs(). Consequently, one XID value achieves the
* same exclusion effect on primary and standby.
*/
if (InHotStandby)
{

View File

@@ -501,10 +501,14 @@ vacuumRedirectAndPlaceholder(Relation index, Buffer buffer)
OffsetNumber itemToPlaceholder[MaxIndexTuplesPerPage];
OffsetNumber itemnos[MaxIndexTuplesPerPage];
spgxlogVacuumRedirect xlrec;
GlobalVisState *vistest;
xlrec.nToPlaceholder = 0;
xlrec.newestRedirectXid = InvalidTransactionId;
/* XXX: providing heap relation would allow more pruning */
vistest = GlobalVisTestFor(NULL);
START_CRIT_SECTION();
/*
@@ -521,7 +525,7 @@ vacuumRedirectAndPlaceholder(Relation index, Buffer buffer)
dt = (SpGistDeadTuple) PageGetItem(page, PageGetItemId(page, i));
if (dt->tupstate == SPGIST_REDIRECT &&
TransactionIdPrecedes(dt->xid, RecentGlobalXmin))
GlobalVisTestIsRemovableXid(vistest, dt->xid))
{
dt->tupstate = SPGIST_PLACEHOLDER;
Assert(opaque->nRedirection > 0);

View File

@@ -281,7 +281,7 @@ present or the overflow flag is set.) If a backend released XidGenLock
before storing its XID into MyPgXact, then it would be possible for another
backend to allocate and commit a later XID, causing latestCompletedXid to
pass the first backend's XID, before that value became visible in the
ProcArray. That would break GetOldestXmin, as discussed below.
ProcArray. That would break ComputeXidHorizons, as discussed below.
We allow GetNewTransactionId to store the XID into MyPgXact->xid (or the
subxid array) without taking ProcArrayLock. This was once necessary to
@@ -293,42 +293,50 @@ once, rather than assume they can read it multiple times and get the same
answer each time. (Use volatile-qualified pointers when doing this, to
ensure that the C compiler does exactly what you tell it to.)
Another important activity that uses the shared ProcArray is GetOldestXmin,
which must determine a lower bound for the oldest xmin of any active MVCC
snapshot, system-wide. Each individual backend advertises the smallest
xmin of its own snapshots in MyPgXact->xmin, or zero if it currently has no
live snapshots (eg, if it's between transactions or hasn't yet set a
snapshot for a new transaction). GetOldestXmin takes the MIN() of the
valid xmin fields. It does this with only shared lock on ProcArrayLock,
which means there is a potential race condition against other backends
doing GetSnapshotData concurrently: we must be certain that a concurrent
backend that is about to set its xmin does not compute an xmin less than
what GetOldestXmin returns. We ensure that by including all the active
XIDs into the MIN() calculation, along with the valid xmins. The rule that
transactions can't exit without taking exclusive ProcArrayLock ensures that
concurrent holders of shared ProcArrayLock will compute the same minimum of
currently-active XIDs: no xact, in particular not the oldest, can exit
while we hold shared ProcArrayLock. So GetOldestXmin's view of the minimum
active XID will be the same as that of any concurrent GetSnapshotData, and
so it can't produce an overestimate. If there is no active transaction at
all, GetOldestXmin returns latestCompletedXid + 1, which is a lower bound
for the xmin that might be computed by concurrent or later GetSnapshotData
calls. (We know that no XID less than this could be about to appear in
the ProcArray, because of the XidGenLock interlock discussed above.)
Another important activity that uses the shared ProcArray is
ComputeXidHorizons, which must determine a lower bound for the oldest xmin
of any active MVCC snapshot, system-wide. Each individual backend
advertises the smallest xmin of its own snapshots in MyPgXact->xmin, or zero
if it currently has no live snapshots (eg, if it's between transactions or
hasn't yet set a snapshot for a new transaction). ComputeXidHorizons takes
the MIN() of the valid xmin fields. It does this with only shared lock on
ProcArrayLock, which means there is a potential race condition against other
backends doing GetSnapshotData concurrently: we must be certain that a
concurrent backend that is about to set its xmin does not compute an xmin
less than what ComputeXidHorizons determines. We ensure that by including
all the active XIDs into the MIN() calculation, along with the valid xmins.
The rule that transactions can't exit without taking exclusive ProcArrayLock
ensures that concurrent holders of shared ProcArrayLock will compute the
same minimum of currently-active XIDs: no xact, in particular not the
oldest, can exit while we hold shared ProcArrayLock. So
ComputeXidHorizons's view of the minimum active XID will be the same as that
of any concurrent GetSnapshotData, and so it can't produce an overestimate.
If there is no active transaction at all, ComputeXidHorizons uses
latestCompletedXid + 1, which is a lower bound for the xmin that might
be computed by concurrent or later GetSnapshotData calls. (We know that no
XID less than this could be about to appear in the ProcArray, because of the
XidGenLock interlock discussed above.)
GetSnapshotData also performs an oldest-xmin calculation (which had better
match GetOldestXmin's) and stores that into RecentGlobalXmin, which is used
for some tuple age cutoff checks where a fresh call of GetOldestXmin seems
too expensive. Note that while it is certain that two concurrent
executions of GetSnapshotData will compute the same xmin for their own
snapshots, as argued above, it is not certain that they will arrive at the
same estimate of RecentGlobalXmin. This is because we allow XID-less
transactions to clear their MyPgXact->xmin asynchronously (without taking
ProcArrayLock), so one execution might see what had been the oldest xmin,
and another not. This is OK since RecentGlobalXmin need only be a valid
lower bound. As noted above, we are already assuming that fetch/store
of the xid fields is atomic, so assuming it for xmin as well is no extra
risk.
As GetSnapshotData is performance critical, it does not perform an accurate
oldest-xmin calculation (it used to, until v13). The contents of a snapshot
only depend on the xids of other backends, not their xmin. As backend's xmin
changes much more often than its xid, having GetSnapshotData look at xmins
can lead to a lot of unnecessary cacheline ping-pong. Instead
GetSnapshotData updates approximate thresholds (one that guarantees that all
deleted rows older than it can be removed, another determining that deleted
rows newer than it can not be removed). GlobalVisTest* uses those threshold
to make invisibility decision, falling back to ComputeXidHorizons if
necessary.
Note that while it is certain that two concurrent executions of
GetSnapshotData will compute the same xmin for their own snapshots, there is
no such guarantee for the horizons computed by ComputeXidHorizons. This is
because we allow XID-less transactions to clear their MyPgXact->xmin
asynchronously (without taking ProcArrayLock), so one execution might see
what had been the oldest xmin, and another not. This is OK since the
thresholds need only be a valid lower bound. As noted above, we are already
assuming that fetch/store of the xid fields is atomic, so assuming it for
xmin as well is no extra risk.
pg_xact and pg_subtrans

View File

@@ -9096,7 +9096,7 @@ CreateCheckPoint(int flags)
* StartupSUBTRANS hasn't been called yet.
*/
if (!RecoveryInProgress())
TruncateSUBTRANS(GetOldestXmin(NULL, PROCARRAY_FLAGS_DEFAULT));
TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
/* Real work is done, but log and update stats before releasing lock. */
LogCheckpointEnd(false);
@@ -9456,7 +9456,7 @@ CreateRestartPoint(int flags)
* this because StartupSUBTRANS hasn't been called yet.
*/
if (EnableHotStandby)
TruncateSUBTRANS(GetOldestXmin(NULL, PROCARRAY_FLAGS_DEFAULT));
TruncateSUBTRANS(GetOldestTransactionIdConsideredRunning());
/* Real work is done, but log and update before releasing lock. */
LogCheckpointEnd(true);