相关: 《Postgresql源码(18)PGPROC相关结构》 《Postgresql快照优化Globalvis新体系分析(性能大幅增强)》 《Improving Postgres Connection Scalability: Snapshots》
先看下这个优化的性能提升效果,在高连接数下会有明显提升:
typedef struct SnapshotData
{
TransactionId xmin; /* all XID < xmin are visible to me */
TransactionId xmax; /* all XID >= xmax are invisible to me */
TransactionId *xip;
uint32 xcnt; /* # of xact ids in xip[] */
...
优化之前PG是如何获取快照的?
procArray->pgprocnos
柔性数组中记录了排序过后的PGPROC数组的索引,参考这篇:《Postgresql源码(18)PGPROC相关结构》。procArray->pgprocnos
**找到所有存在的PGPROC、PGXACT结构,收集所有的xid。PGXACT->xmin
,用于在访问的过程中做一些vacuum的事情,回收元组。2011年已经发现GetSnapshotData存在瓶颈,当时做的优化是把PGPROC里面把快照需要的变量拆出来,放到PGXACT中,这样数据结构小很多,可以装到一个cpu cache line中。
从上述分析中可以看出,遍历所有连接的复杂度为O(#connection)
,快照计算成本随连接数线性增加。
理论上有两种提高扩展性的方法:
O(n)
。旧版本的快照获取步骤:
xmin = global_xmin = inferred_maximum_possible;
for (i = 0; i < #connections; i++)
{
int procno = shared_memory->connection_offsets[i];
PGXACT *pgxact = shared_memory->all_connections[procno];
// compute global xmin minimum
// 记录全局做小xmin
if (pgxact->xmin && pgxact->xmin < global_xmin)
global_xmin = pgxact->xmin;
// nothing to do if backend has transaction id assigned
// 未启动事务直接跳过
if (!pgxact->xid)
continue;
// the global xmin minimum also needs to include assigned transaction ids
// 全局最小xmin也要包含最小的事务ID
if (pxact->xid < global_xmin)
global_xmin = pgxact->xid;
// add the xid to the snapshot
// 活跃事务记录到快照的xip中
snapshot->xip[snapshot->xcnt++] = pgxact->xid;
// compute minimum xid in snapshot
// 记录最小的xid到快照中
if (pgxact->xid < xmin)
xmin = pgxact->xid;
}
snapshot->xmin = xmin;
// store snapshot xmin unless we already have built other snapshots
if (!MyPgXact->xmin)
MyPgXact->xmin = xmin;
RecentGlobalXminHorizon = global_xmin;
这里最重要的一点:
针对瓶颈点,作者做了三层优化。
问题主要来自于这行代码(这里的MyPgXact->xmin
的含义是:最小的运行中的xid,不包括lazy vacuum)
xmin = global_xmin = inferred_maximum_possible;
for (i = 0; i < #connections; i++)
{
...
...
if (!TransactionIdIsValid(MyPgXact->xmin))
MyPgXact->xmin = TransactionXmin = xmin; <---------------- 这里
...
}
因为这个值要记录运行中的最小xid,所以连接的事务提交、终止都需要更新这个值,在正常运行的系统中,由于xid是不断推进的,这个值的更新频率非常高。
在当前最常见的CPU微架构中,每个核心私有L1和L2缓存,每个CPU插槽上所有核共享L3缓存。
关于第二点:
代码作者优化掉了RecentGlobalXminHorizon(用于清理死元组),引入了两个没那么准确的新值:
在A、B中间的元组会引入昂贵的准确计算,避免在GetSnapshotData中计算引发上述问题。
PGXACT的存放位置是离散的,可能在allPgXact大数组中的任意几个位置,不连续。(参考《Postgresql源码(18)PGPROC相关结构》)在使用时,通过连续的pgprocnos数组中记录的index找到活跃的PGXACT。
GetSnapshotData
...
//优化前,通过pgprocnos拿到索引,通过索引在离散的allPgXact数组中拿到xid
numProcs = arrayP->numProcs;
for (index = 0; index < numProcs; index++)
{
int pgprocno = pgprocnos[index];
volatile PGXACT *pgxact = &allPgXact[pgprocno];
...
//优化后,在密集数组other_xids中拿xid
for (int pgxactoff = 0; pgxactoff < numProcs; pgxactoff++)
{
TransactionId xid = UINT32_ACCESS_ONCE(other_xids[pgxactoff]);
问题就是数据是离散的,将xids单独拿出来放到连续存储的密集数组中,可以显著提高命中率。
- 当要求重新计算快照时,我们检查计数器,如果没变可以直接复用上一个快照。
- 如果计数器变了,那么需要重新计算快照。