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