提高Postgres连接可伸缩性:快照

2020-10-26 08:15:12

如果你想了解更多关于Microsoft Azure上的CATUS,请阅读这篇关于Azure Database for PostgreSQL上的Hyperscale(CATUS)的文章。

跳过导航我最近分析了连接可伸缩性的限制,以了解改进Postgres处理大量连接的最有效方法,以及为什么这很重要。我的结论是,最紧迫的问题是快照可伸缩性。

这篇文章详细介绍了我最近对Postgres14(将于2021年第三季度发布)所做的改进,显著降低了已确定的快照可伸缩性瓶颈。

由于实现细节的解释相当长,我认为如果我从工作的结果开始,而不是从技术细节开始(我知道,我在作弊;),对你们中的一些人来说会更有趣。

对于所有这些基准,我在合并所有连接可伸缩性更改之前和之后对Postgres开发树进行了比较。还穿插了一些其他更改,但没有一项更改可能会对性能产生重大影响。

这些结果1显示了在我将在这篇文章中讨论的快照可伸缩性更改之后的显著改进,即使在连接计数非常高的情况下,也几乎没有证据表明可伸缩性问题。大约100个连接开始的下降-对于前/后更改运行-似乎是由操作系统任务调度引起的,而不是Postgres直接造成的。

接下来,重复我在上一篇文章中使用的基准,即分析连接可伸缩性以确定快照可伸缩性是主要瓶颈(再次在我的工作站2上执行)。

这些结果(3,4)显示了Postgres固定版本和非固定版本在可伸缩性方面的极大差异。这比上面的结果更重要,因为选择基准测试是为了突出快照可伸缩性问题。

提交模式)或整个事务(例如,在可重复读取模式下),以确定由其他事务创建的哪些行应该可见,哪些不可见。

Tyecif struct SnapshotData(类型定义函数结构快照数据)。

{。

…。

TransactionId xmin;/*我可以看到所有xid<;xmin*/。

TransactionId xMax;/*我看不到所有xid>;=xmax*/。

…。

/*

*对于普通MVCC快照,它包含中的所有Xact ID。

*进度,除非快照是在恢复期间拍摄的,在这种情况下。

*它是空的。…。

*注意:xip[]中的所有ID都满足xmin<;=xip[i]<;xmax。

*/。

TransactionId*xip;

Uint32 xcnt;/*xip[]*/中的xact ID数。

Xip数组包含拍摄快照时正在运行的所有事务ID(Postgres使用这些ID而不是普通时间戳)。当遇到具有特定xmin的行版本时,如果该事务在拍摄快照时仍在运行,则该版本将不可见,反之,如果xmin是在那时已经完成的事务,则该版本可能是可见的。相反,如果拍摄快照时正在运行的关联事务被拍摄,则具有xmax的行版本仍然可见,否则不可见。

要了解性能问题和改进,有必要了解快照以前是如何构建的。其核心例程是GetSnapshotData(),毫不奇怪,它就是我们在前面的概要文件中看到的函数。

到Postgres的每个连接都有一个关联的struct PGPROC,到目前为止,还有一个struct PGXACT条目。这些结构在服务器上根据MAX_CONNECTIONS(和MAX_PREPARED_XACTS、MAX_AUTOVUAL_WORKS、…)进行预分配。。

类型定义结构ProcArrayStruct。

{。

Int numProcs;/*有效进程条目数*/。

…。

/*索引到allPgXact[],具有PROCARRAY_MAXPROCS条目*/

Int pgprocnos[Flexible_array_ember];

…。

)ProcArrayStruct;

结构PGPROC。

{。

}。

…。

/*。

*在PostgreSQL 9.2之前,以下字段存储为。

*PGPROC。然而,基准测试显示,包装这些特殊的。

*将成员尽可能紧密地放入单独的阵列可加快GetSnapshotData的速度。

*在具有多个CPU内核的系统上,通过减少

*需要提取的高速缓存线。因此,在添加之前请仔细考虑。

*这里有任何其他东西。

*/。

类型定义结构PGXACT。

{。

TransactionId xid;/*当前正在执行的顶级事务的id。

*如果正在运行且xid,则由此进程执行。

*已赋值;否则为InvalidTransactionId*/

TransactionId xmin;/*最小化运行xid,就像我们。

*开始我们的实践,不包括懒惰吸尘器:

*Vacuum不得删除由删除的元组。

*xid>;=xmin!*/。

Uint8 vacuumFlags;/*真空相关标志,见上*/。

布尔溢出;

Uint8nxid;

)PGXACT;

为了避免需要遍历所有PGPROC/PGXACT条目,ProcArrayStruct->;pgprocnos是->;maxProc已建立连接的排序数组。每个数组条目都是PGPROC/PGXACT的索引。

为了构建快照,GetSnapshotData()迭代pgprocnos中的所有maxProc条目,为所有具有指定事务ID的连接收集PGXACT->;xid。

有几个方面使得这比我描述的简单循环稍微复杂一些:

因为在某种程度上很方便,所以GetSnapshotData()还计算全局最旧的PGXACT->;xmin。这是最重要的,用于在访问时删除死的元组。

要实现保存点,一个后端可以分配多个事务ID。其中一定数量的文件作为PGPROC的一部分存储。

出于效率目的,在构建快照时会忽略一些后端,例如执行Vacuum的后端。

在2011年,GetSnapshotData()被视为瓶颈。此时,构建快照的所有相关数据都存储在PGPROC中。这会导致性能问题,主要是因为每个已建立的连接都要访问多个高速缓存线。

通过将最重要的字段拆分到一个新的数据结构PGXACT中,对此进行了改进。这极大地减少了构建快照所需访问的高速缓存线的总数。此外,对PGXACT的访问顺序进行了改进,使其按内存顺序递增(以前由建立和断开连接的顺序决定)。

不难看出,上述方法(即迭代包含所有已建立连接的数组)的复杂度为O(#Connections),即快照计算成本随连接数量线性增加。

这里有两种提高可伸缩性的基本方法:第一,找到一种能够提高复杂度的算法,这样每个额外的连接都不会线性地增加快照计算成本。其次,为每个连接执行较少的工作,希望能大大减少所需的总时间,以便即使在高连接计数时,总时间仍然很小,不会有太大影响(即减少常量因子)。

Postgres社区多年来一直致力于改善GetSnapshotData()算法复杂性的一种方法是基于提交序列号的快照(也称为基于CSN的快照)。不幸的是,事实证明,实施CSN快照是一个非常大的项目,有许多悬而未决的重要问题需要解决。当我在寻找可以在更短的时间内完成的改进时,我放弃了追求这种方法,以及其他类似的根本性改变。

早在2015年,我之前就曾试图通过缓存快照来解决这个问题,但事实证明这也不容易(至少现在还不是…)。。

因此,我选择首先将重点放在提高每个额外连接所增加的成本上。迭代几千个元素的数组并取消对相当小的内容的引用显然不是免费的,但与作为查询处理一部分完成的其他工作相比,它应该不会像上一篇文章中的CPU配置文件中那样突出:

Xmin=global_xmin=推断的最大可能;

对于(i=0;i<;#连接;i++)。

{。

Int procno=共享内存->;连接偏移量[i];

PGXACT*pgxact=共享内存->;所有连接[procno];

//计算全局最小xmin。

IF(pgxact->;xmin&;&;pgxact->;xmin<;global_xmin)

Global_xmin=pgxact->;xmin;

//如果后端分配了事务id,则不做任何事情。

如果(!Pgxact->;xid)。

继续;

//全局最小xmin还需要包含分配的事务ID。

IF(pxact->;xid<;global_xmin)

Global_xmin=pgxact->;xid;

//将xid添加到快照中。

快照->;xip[快照->;xcnt++]=pgxact->;xid;

//计算快照中的最小XID。

IF(pgxact->;xid<;xmin)。

Xmin=pgxact->;xid;

}。

快照->;xmin=xmin;

//存储快照xmin,除非我们已经构建了其他快照。

如果(!MyPgXact->;xmin)。

MyPgXact->;xmin=xmin;

RecentGlobalXminHorizon=global_xmin;

关于这一点的一个重要观察是,主循环不仅计算快照内容,还计算“全局xmin水平”。它实际上不是快照的一部分,但是可以方便地同时计算,只需少量的附加成本。或者我们是这么认为的,…。

我花了很多时间,断断续续地试图理解为什么迭代数组的几千个元素,即使考虑到间接性,在某些工作负载中也会如此昂贵。

主要问题原来是MyPgXact->;xmin=xmin;。每当计算快照(除非已存在另一个快照)、提交/中止事务时(1,2),都会设置连接的xmin。

在当前最常见的多核CPU微体系结构上,每个CPU核心都有自己的专用L1和L2缓存,并且CPU插槽中的所有内核都共享一个L3缓存。

活动后端不断更新MyPgXact->;xmin。简而言之,这反过来要求数据处于核心-本地缓存中(处于独占/修改状态)。相反,在构建快照时,后端访问所有其他连接的PGXACT->;{xid,xmin}。为了掩饰一些细节,反过来要求包含PGXACT的缓存线不能在另一个内核的私有缓存中。迎头相撞警报。

与访问L3中的共享和未修改的数据(显然,在本地L1/L2中更是如此)相比,将高速缓存线的修改内容从另一核中的高速缓存传送到本地高速缓存或共享L3高速缓存具有相当高的等待时间成本。

关键在于,要构建快照,实际上不需要访问->;xmin。它只需要计算RecentGlobalXminHorizon。然而,仅仅删除读访问本身并不能显著改善这种情况:因为构建快照确实需要访问->;xid,而->;xmin修改->;xmin会导致->;xid访问速度变慢。

GetSnapshotData()还重新计算RecentGlobalXminHorizon的原因是我们将其用于清理死表和索引项(请参阅何时可以/应该修剪或整理碎片?以及即时删除索引元组)。水平用作阈值,低于该阈值,任何连接都不能访问旧的元组版本。如果版本早于水平行版本,则可以安全地删除指向它们的索引项。

在经历了相当长一段时间的尝试之后,关键的观察结果让我避免了昂贵的重新计算,那就是我们在大多数情况下并不一定需要一个准确的值。

在大多数工作负载中,大多数访问都是对活动元组的访问,当遇到非活动元组版本时,它们要么非常旧,要么非常新。只要稍加小心,我们就可以懒惰地维护一个更复杂的阈值:一个值确定所有比它更老的东西肯定是死的,第二个值确定它以上的东西肯定是太新而无法清理。

当遇到介于这些阈值之间的元组时,我们计算对当前事务有效的精确值。如果我们必须在每个短事务中重新计算阈值,这将比预先计算GetSnapshotData()中的精确值更昂贵-但构建这样的工作负载非常困难。

实现此新方法的主要承诺是dc7420c2c92快照可伸缩性:在构建快照时不要计算全局视野。

提交之后,我们不再访问GetSnapshotData()中的->;xmin。为了避免缓存线乒乓,我们可以将其移出GetSnapshotData()使用的数据。仅此一项就大大提高了可伸缩性。

提交时,1f51c17c68d快照可伸缩性:将PGXACT->;xmin移回PGPROC。包括一些粗略的数字:

对于高度并发、快照获取繁重的工作负载,仅此更改即可。

可以显著提高可伸缩性。例如,较小的2上的普通pgbench

插座机只读pgbench提高1.07倍,只读pgbench提高1.22倍。

批量提交查询时,100个批量提交查询,100个批量提交查询2.85x。

';选择';;。后几个数字显然不会出现在。

真实世界,但对快照计算进行微观基准测试。

可伸缩性(以前大约80%的时间花在GetSnapshotData()上)。

上面我展示了一些用于快照计算的简化伪代码(实代码)。伪代码的开头:

Xmin=global_xmin=推断的最大可能;

对于(i=0;i<;#连接;i++)

{。

Int procno=共享内存->;连接偏移量[i];

PGXACT*pgxact=共享内存->;所有连接[procno];

显示对PGXACT的访问必须通过间接方式。这种间接性允许只查看已建立连接的PGXACT,而不必查看非活动连接的连接插槽。

我们可以使PGXACT的内容密集,而不必经历间接的过程。这使得连接建立/断开稍微慢了一点,现在不仅必须确保CONNECTION_OFFSES数组是密集的,而且还要确保PGXACT的内容是密集的。

第二个相关的观察结果是,当->;xid无效时(之前需要访问->;xmin),其余的PGXACT成员都不需要访问。在许多工作负载中,大多数事务不写入,并且在大多数写入繁重的工作负载中,大多数事务不使用保存点。

因此,最好不要使整个PGXACT阵列密集,而是将其成员拆分为单独的密集阵列。包含所有已建立连接的XID的数组几乎总是需要访问6。但是,只有当连接具有分配的XID时,才需要访问其他成员。

通过将单独的数组用于XID,可以提高CPU缓存命中率,因为大多数情况下不会访问其他字段。此外,由于其他字段更改的频率较低,因此将它们分开允许它们在高速缓存域之间以未经修改的状态共享(提高访问速度/减少总线流量)。

这产生了相当大的好处,正如其中一条提交消息中所评论的那样:

在较大的双插槽机器上,此提交和前面的两个提交结果。

在只读pgbench中性能提高约1.07倍。对于读取量大的情况。

没有行级争用的混合读/写工作负载,我看到大约是1.1倍。

即使进行了上述所有更改,计算具有大量连接的快照仍然不便宜。虽然这些更改大大提高了常量因子,但必须遍历可能只有几千个元素的数组仍然不便宜。

现在,GetSnapshotData()不再需要维护RecentGlobalXmin,这是对表的一个巨大改进:如果我们可以确定快照没有更改,则可以避免重新计算快照。以前,这是不可行的,因为RecentGlobalXmin比快照内容本身更改得更频繁。

快照仅在先前运行的事务已提交时才需要更改(因此其影响是可见的):因为所有大于或等于->;xmax的事务都被视为正在运行,并且在计算快照之后启动的所有事务都保证被分配一个大于->;xmax的事务ID,所以我们不需要关心新启动的事务。

因此,可以使用已完成(即已提交或已中止)事务数量的简单内存中计数器来使快照无效。完成计数器存储在快照中,当要求重新计算快照内容时,我们只需要检查快照的snapXactCompletionCount是否与当前内存中的值ShmemVariableCache->;xactCompletionCount相同。如果是,则可以重用快照的内容,否则需要从头开始构建快照。

此更改在Postgres Commit 623a9ba79bb:快照可伸缩性:使用Xact完成计数器缓存快照中实现。

在较小的双插槽计算机上,这又增加了约1.03倍,在较大的计算机上。

机器效果大致加倍(较早的补丁版本经过测试。

不过)。

正如最后一句话所暗示的,目前我们测试持有锁的缓存能力。这很可能是可能的。

.