PostgreSQL 源码解读(252)- PG 14(Improving connection scalability)#4

PostgreSQL 14在MVCC上面有所增强,以保证在大批量客户端连接场景下性能不会出现显著下降(详细可参见参考资料中的链接)。本节介绍了提交记录“Don’t compute global horizons while building snapshots.”中pruneheap.c中的修改以及涉及的相关数据结构。

一、数据结构

PruneState
用于heap_page_prune和子函数的结构体


/* Working data for heap_page_prune and subroutines */
typedef struct
{
    Relation    rel;
    /* tuple visibility test, initialized for the relation */
    //元组可见性验证,为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().
     * 如果已计算并且OldSnapshotThresholdActive()返回为真,则通过函数TransactionIdLimitedForOldSnapshots设置阈值,
     * 第一次某个元组基于有限边界将要被删除时,old_snap_used设置为真,同时调用SetOldSnapshotThresholdTimestamp函数,
     * 详细参见heap_prune_satisfies_vacuum
     */
    TimestampTz old_snap_ts;//拍照时的时间点
    TransactionId old_snap_xmin;//拍照时的xmin
    bool        old_snap_used;//有元组因此而被删除
    TransactionId new_prune_xid;    /* page中新的清除标记值,new prune hint value for page */
    TransactionId latestRemovedXid; /* 该清除动作最后的xid,latest xid to be removed by this prune */
    int            nredirected;    /* 下面数组的元素个数,numbers of entries in arrays below */
    int            ndead;//
    int            nunused;
    /* arrays that accumulate indexes of items to be changed */
    OffsetNumber redirected[MaxHeapTuplesPerPage * 2];
    OffsetNumber nowdead[MaxHeapTuplesPerPage];
    OffsetNumber nowunused[MaxHeapTuplesPerPage];
    /* marked[i] is true if item i is entered in one of the above arrays */
    bool        marked[MaxHeapTuplesPerPage + 1];
} PruneState;

二、源码解读

/*
  diff:
  1.PruneState结构体增加rel、vistest等成员变量
  2.参数列表中存在OldestXmin的,均删除该参数
  3.函数体中存在OldestXmin、RecentGlobalXmin的,修改为GlobalVisXXX函数调用
  4.新增heap_prune_satisfies_vacuum函数
*/
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -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 */
 // 用于heap_page_prune和子例程的结构体
 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;
+
    /* 参见数据结构PruneState */
    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 */
 /* 
    heap_prune_chain函数参数删除了relation和OldestXmin 
    该函数用于清理指定的line pointer或者HOT链
 */
-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 */
- *
- * 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)
 /*
   heap_page_prune_opt函数用于清除page和清理page中的碎片
   该函数执行得很频繁,只要一有机会就会执行,在执行前调用者必须pin缓存且没有锁
   输入参数是relation和buffer
 */
 {
    Page        page = BufferGetPage(buffer);//获取相应的page
+   TransactionId prune_xid;//page上存储的事务id
+   GlobalVisState *vistest;//详见GlobalVisState结构体
+   TransactionId limited_xmin = InvalidTransactionId;//
+   TimestampTz limited_ts = 0;//
    Size        minfree;
-   TransactionId OldestXmin;//删除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))//非有效的事务id,退出
+       return;
+
+   /*
+    * Check whether prune_xid indicates that there may be dead rows that can
+    * be cleaned up.
     * 检查prune_xid是否存在可清除的数据行
     *
-    * 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.
+    *
+    * 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.
     * old_snapshot_threshold 是GUC参数,用于判断某个快照多长时间后too old,默认为-1(禁用该特性)
     */
-   if (IsCatalogRelation(relation) ||
-       RelationIsAccessibleInLogicalDecoding(relation))
-       OldestXmin = RecentGlobalXmin;
-   else
-       OldestXmin =
-           TransactionIdLimitedForOldSnapshots(RecentGlobalDataXmin,
-                                               relation);
+   vistest = GlobalVisTestFor(relation);//vistest中会有确实需要的xid和不再需要的xid
-   Assert(TransactionIdIsValid(OldestXmin));
+   if (!GlobalVisTestIsRemovableXid(vistest, prune_xid))//是否可被清除?返回false,表示不能清除
+   {
+       if (!OldSnapshotThresholdActive())//禁用快照过久特性,则返回
+           return;
-   /*
-    * Let's see if we really need pruning.
-    *
-    * Forget it if page is not hinted to contain something prunable that's
-    * older than OldestXmin.
-    */
-   if (!PageIsPrunable(page, OldestXmin))
-       return;
+       if (!TransactionIdLimitedForOldSnapshots(GlobalVisTestNonRemovableHorizon(vistest),
+                                                relation,
+                                                &limited_xmin, &limited_ts))
+           return;//快照过久也需要不限制事务id,返回
+
+       if (!TransactionIdPrecedes(prune_xid, limited_xmin))
+           return;//prune_xid在limited_xmin之后,返回
+   }
    /*
     * 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);//执行清理,输入参数删除OldestXmin,新增了vistest/limited_xmin/limited_ts
        }
        /* 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.
  */
  /*
    heap_page_prune函数用于清除page和清理page中的碎片
    vistest用于区分元组是DEAD还是RECENTLY_DEAD
    old_snap_xmin / old_snap_ts应由TransactionIdLimitedForOldSnapshots函数设置或者为InvalidTransactionId/0
    如report_stats为真,则发送heap-only元组给统计进程(pg_stats)
  */
 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;//PruneState结构体赋值
+   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 */
        //清理行指针line pointer
-       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.
  * 由于old_snapshot_threshold的存在,该过程比GlobalVisTestIsRemovableXid要复杂
  * 基于受限的范围,在确定要删除行时,我们希望增加由于过久快照而会触发错误的阈值
+ *
+ * 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.
  * 成本上面的考虑,我们希望在需要时才调用TransactionIdLimitedForOldSnapshots
  * 在pd_prune_xid过久的情况下,不会考虑在heap_hot_prune_opt中处理这事情
  * 但我们仍然希望可以清理对于prstate->vistest来说过新的行,但基于old_snapshot_threshold可被清除
  * 因此我们在需要时调用TransactionIdLimitedForOldSnapshots
+ */
+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);//调用HeapTupleSatisfiesVacuumHorizon
+
+   if (res != HEAPTUPLE_RECENTLY_DEAD)//只关注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;//dead_after在old_snap_xmin之前,则可清除
+       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.
     * 首先检查GlobalVisTestIsRemovableXid是否足以找到已死亡的行,
     * 否则如old_snapshot_threshold参数有效,则尝试使用更低的区间
+    */
+   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,//去掉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)//改为使用heap_prune_satisfies_vacuum
                == 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;

三、跟踪分析

N/A

四、参考资料

1. Improving connection scalability: GetSnapshotData()
2. snapshot scalability: Don’t compute global horizons while building snapshots.

请使用浏览器的分享功能分享到微信等