Postgresql源码(65)新快照体系Globalvis工作原理分析

2022-08-03 18:27:24 浏览数 (1)

相关: 《Postgresql源码(18)PGPROC相关结构》 《Postgresql源码(65)新快照体系Globalvis工作原理分析》 《Postgresql快照优化Globalvis新体系分析(性能大幅增强)》 《Improving Postgres Connection Scalability: Snapshots》 一些历史悠久的分析: 《Postgresql源码(28)获取快照GetSnapshotData流程分析和性能问题》

1 优化后的allProcs数组

先介绍下新版本PG删除了PGXACT结构后,事务ID是怎么存的:

  • 之前的PGPROC和PGXACT结构是一一对应的,每个后端一个PGPROC、一个PGXACT,事务ID记录在PGXACT中,问题就像上一篇提的,PGXACT不是紧凑的,遍历起来无法高效利用cache。
  • 新版本中,在PROC_HDR中原来的allPgXact不见了(指向PGXACT大数组),取而代之的是xids紧凑数组,注意!这个数组和pgprocnos索引是对应的。
  • 所以找一个PROC的xid,除了直接从PGPROC中读,还可以从PROC_HDR->xids读取,这个数组是紧凑的,遍历起来效率更高。

debug命令备忘(假设numProcs=3) p *procArray p allProcs[procArray->pgprocnos0] p allProcs[procArray->pgprocnos1] p allProcs[procArray->pgprocnos2] p ProcGlobal->xids0 p ProcGlobal->xids1 p ProcGlobal->xids2

2 GetSnapshotData第一部分——循环拿活跃事务

总结:

  • 遍历ProcGlobal->xids数组,拿到所有的xid(优化一:紧凑数组)。
  • 活跃的xid添加到快照的xip数组中。
  • 顺便算一个xmin(函数栈变量)记录最小的活跃事务id。
  • 读取PGXACT->xmin(优化二)
  • xmax在进入循环前就算好了,一定是latestCompletedXid 1。
  • 最后如果MyProc的xmin是无效的,更新一次PGPROC->xmin(还是要更新自己的xmin,这是必须的,无优化)
  • 整个过程包在ProcArrayLock大锁中。

代码流程:

代码语言:javascript复制
GetSnapshotData
  ...
  LWLockAcquire(ProcArrayLock, LW_SHARED)
  ...
  for (int pgxactoff = 0; pgxactoff < numProcs; pgxactoff  )
    TransactionId xid = UINT32_ACCESS_ONCE(other_xids[pgxactoff]);
    
    /* 如果xid>=xmax可以直接跳过,因为这个xid是在我拿快照后启动的,对我来说肯定不可见 */
    if (!NormalTransactionIdPrecedes(xid, xmax))
      continue;    
    
    /* 如果xid < xmin,xid也要包含在xmin中,xmin表示最小的活跃事务 */
    if (NormalTransactionIdPrecedes(xid, xmin))
      xmin = xid;
    
    /* 记录到快照中 */
    xip[count  ] = xid;
  
  /* 更新一次xmin */
  if (!TransactionIdIsValid(MyProc->xmin))
    MyProc->xmin = TransactionXmin = xmin;

  LWLockRelease(ProcArrayLock);

3 GetSnapshotData第二部分——维护GlobalVis

这部分要对比历史代码来看。

PG10

  • 【0】前面把快照已经计算完了。下面“顺便”计算下全局最小可清理位点:RecentGlobalXmin。
  • 【1】拿到最小事务ID。
  • 【2】最小可清理位点传给RecentGlobalXmin,如果配了vacuum_defer_cleanup_age,把最小可清理位点,再往前推,清理时会保留更多的事务,给高延迟的备机使用。
  • 【3】如果复制槽指定了某个位点,vacuum是不能清理的。
代码语言:javascript复制
GetSnapshotData
    //【0】前面把快照已经计算完了。下面“顺便”计算下全局最小可清理位点:RecentGlobalXmin
    
    //【1】拿到最小事务ID
	if (TransactionIdPrecedes(xmin, globalxmin))
		globalxmin = xmin;
    
	//【2】最小可清理位点传给RecentGlobalXmin,如果配了vacuum_defer_cleanup_age
	// 把最小可清理位点,再往前推,清理时会保留更多的事务,给高延迟的备机使用
	RecentGlobalXmin = globalxmin - vacuum_defer_cleanup_age;
	if (!TransactionIdIsNormal(RecentGlobalXmin))
		RecentGlobalXmin = FirstNormalTransactionId;

	//【3】如果复制槽指定了某个位点,vacuum是不能清理的;
	if (TransactionIdIsValid(replication_slot_xmin) &&
		NormalTransactionIdPrecedes(replication_slot_xmin, RecentGlobalXmin))
		RecentGlobalXmin = replication_slot_xmin;
    ...
    ...

PG14

测试场景:

代码语言:javascript复制
事务一RC-----------4000440启动-------------------4000440结束------------------------------->
事务二RR----------------------4000450启动-----------------------------4000450结束---------->
事务三RC--------------------------------------------调试位置------------------------------->

调试位置:

代码语言:javascript复制
GetSnapshotData
    //【0】前面把快照已经计算完了。下面“顺便”计算下全局最小可清理位点,但是由于没有遍历PGPROC的xmin,只有自己遍历出来的xid,

	/* maintain state for GlobalVis* */
	{
		TransactionId def_vis_xid;
		TransactionId def_vis_xid_data;
		FullTransactionId def_vis_fxid;
		FullTransactionId def_vis_fxid_data;
		FullTransactionId oldestfxid;

        //【1】oldestfxid = 726
		oldestfxid = FullXidRelativeTo(latest_completed, oldestxid);
        
        //【2】def_vis_xid_data = 4000450
		def_vis_xid_data =
			TransactionIdRetreatedBy(xmin, vacuum_defer_cleanup_age);
        //【3】def_vis_xid_data = 4000450
		def_vis_xid_data =
			TransactionIdOlder(def_vis_xid_data, replication_slot_xmin);
        
        //【4】def_vis_xid = 4000450
		def_vis_xid = def_vis_xid_data;
		def_vis_xid = TransactionIdOlder(replication_slot_catalog_xmin, def_vis_xid);
		def_vis_fxid = FullXidRelativeTo(latest_completed, def_vis_xid);
		def_vis_fxid_data = FullXidRelativeTo(latest_completed, def_vis_xid_data);

        //【5】4000440 ----> 4000450
		GlobalVisSharedRels.definitely_needed =
			FullTransactionIdNewer(def_vis_fxid,
								   GlobalVisSharedRels.definitely_needed);
        ...
        //【6】4000440 ----> 4000440
		GlobalVisSharedRels.maybe_needed =
			FullTransactionIdNewer(GlobalVisSharedRels.maybe_needed,
								   oldestfxid);
        ...
	}

总结:

  • 【5】4000440 ----> 4000450:definitely_needed计算时实际上用的就是上面循环里面扫出来的最小的xid,在经过参数微调下(vacuumdelay和复制槽),definitely_needed的含义比较好理解,运行中的事务都是>=这个值的。
  • 【6】4000440 ----> 4000440:maybe_needed取值是取:newer(自己的值、最小的冻结事务ID),这个值的含义是:小于这个值的事务应该都不活跃了。
  • 不活跃 < maybe_needed < ... < definitely_needed <= 活跃,那maybe_needed和definitely_needed中间的值怎么知道有没有提交?

看下面函数:

代码语言:javascript复制
bool
GlobalVisTestIsRemovableFullXid(GlobalVisState *state,
								FullTransactionId fxid)
{
    // 比maybe_needed小的肯定已经不活跃了,可以清理!
	if (FullTransactionIdPrecedes(fxid, state->maybe_needed))
		return true;

    // 比definitely_needed大于或等于,肯定活跃的,不能清理!
	if (FullTransactionIdFollowsOrEquals(fxid, state->definitely_needed))
		return false;

    // 中间的怎么办?调用GlobalVisUpdate
	if (GlobalVisTestShouldUpdate(state))
	{
	    // 调用ComputeXidHorizons计算每一个PROC的xmin,然后更新到GlobalVis变量中。
		GlobalVisUpdate();

		Assert(FullTransactionIdPrecedes(fxid, state->definitely_needed));

		return FullTransactionIdPrecedes(fxid, state->maybe_needed);
	}
	else
		return false;
}
  • (粗略判断)在判断具体一个XID能不能清理时,如果比definitely_needed大(或等),或比maybe_needed小,可以直接返回。
  • (需要时精确判断)在判断maybe_needed和definitely_needed中间的xid时,要走优化前的老逻辑,遍历每个PGPROC的xmin,拿到全局最小的xmin,然后调用GlobalVisUpdateApply更新全局变量GlobalVisSharedRels.maybe_needed、GlobalVisSharedRels.definitely_needed的值,提供给后面函数使用。

4 (重要)优化后不在循环内读取PGXACT->xmin会有什么影响?

  1. 循环PGPROC时,看不到别的进程的xmin了,例如,一个RR事务A在拿快照的时候,最小事务ID:10还没提交,那么这个RR快照的xmin就是10。
  2. 优化前,A事务后面新事务B构造快照时,能通过A的PGPROC看到xmin=10,并更新自己的xmin=10。
  3. 优化后,A事务后面新事务B构造快照时,只能看到自己遍历时最小的xid,例如这时10已经提交了,B看到的是15,那么后构造的快照就会认为全局最小xmin=15,但是由于A的xmin是10,所以这时全局清理位点需要是10,不能是15。

0 人点赞