前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Postgresql快照优化Globalvis新体系分析(性能大幅增强)

Postgresql快照优化Globalvis新体系分析(性能大幅增强)

作者头像
mingjie
发布2022-08-03 18:27:05
7210
发布2022-08-03 18:27:05
举报

相关: 《Postgresql源码(18)PGPROC相关结构》 《Postgresql快照优化Globalvis新体系分析(性能大幅增强)》 《Improving Postgres Connection Scalability: Snapshots》

PG的新快照体系Globalvis分析

  • 本篇介绍快照优化作者Andres Freund的优化总结文章。
  • 本篇中有很多个人理解,不一定准确,推荐阅读:《Improving Postgres Connection Scalability: Snapshots》
  • 本篇文章作为新版本PG快照分析的第一篇,后面几篇新代码再做展开分析。

先看下这个优化的性能提升效果,在高连接数下会有明显提升:

一、概念回顾

1 MVCC

  • 传统锁机制无法做到读、写的并发操作,因为数据只有一个版本。引入mvcc后,数据存在多版本,可以做到读一个版本、写另一个版本,实现读写并发。
  • 如果实现这样机制,需要每个行版本记录xmin(可见开始位点visible since),xmax(可见结束位点visible until)。
  • 位点即时间戳,PG中使用自增正整数表示。

2 快照

  • 只有时间戳还是不够的,决定我当前能否看到一个元组,我还必须知道创建、删除元组的时间戳所代表的事务是否已经提交了。为了避免脏读,只有提交的事务才会被看做已生效。
  • PG使用快照来记录哪些事务正在运行中。
  • PG的SnapshotData结构中,xip记录了所有运行中的事务ID。
代码语言:javascript
复制
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[] */
...

3 快照获取

优化之前PG是如何获取快照的?

  • 每个PG进程都对应一个PGPROC结构、一个PGXACT结构(PG14把PGXACT合到PGPROC里面了)
  • PGPROC数组在初始化的时候就全部申请好了,再找PROC的时候为了避免每次都遍历整个数组,在procArray->pgprocnos柔性数组中记录了排序过后的PGPROC数组的索引,参考这篇:《Postgresql源码(18)PGPROC相关结构》
  • 在构建快照时,GetSnapshotData会遍历**procArray->pgprocnos**找到所有存在的PGPROC、PGXACT结构,收集所有的xid。
  • 在构建快照时,还有一些细节:
    • 为了方便,GetSnapshotData会计算全局最久PGXACT->xmin,用于在访问的过程中做一些vacuum的事情,回收元组。
    • 为了实现savepoint,一个backend需要记录多个事务ID。
    • 为了效率,vacuum忽略一些执行中的backend。
    • 备机上的快照计算完全不同(以前做过分析,文章写得很简陋,后面再补一篇)

4 历史优化

2011年已经发现GetSnapshotData存在瓶颈,当时做的优化是把PGPROC里面把快照需要的变量拆出来,放到PGXACT中,这样数据结构小很多,可以装到一个cpu cache line中。

二、识别当前的瓶颈点

从上述分析中可以看出,遍历所有连接的复杂度为O(#connection),快照计算成本随连接数线性增加。

理论上有两种提高扩展性的方法:

  1. 寻找复杂度更低的算法,避免O(n)
  2. 对每个连接做更少的操作,减少单次操作的时间。

旧版本的快照获取步骤:

代码语言:javascript
复制
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;

这里最重要的一点:

  • 循环体起始只需要计算活跃事务就好了,但是,这里还顺便计算了全局xmin。这不是快照的一部分,但可以方便的同时计算,只需要很小的代价(or so we thought…)。
  • 代码作者花了很长时间,试图去理解为什么遍历几千个元素,要话费如此昂贵的代价。

针对瓶颈点,作者做了三层优化。

1 瓶颈点&优化:ping pong

问题主要来自于这行代码(这里的MyPgXact->xmin的含义是:最小的运行中的xid,不包括lazy vacuum)

代码语言:javascript
复制
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缓存。

  • 第一点:假设有8核的CPU,8个连接在并发的构建8个快照,每个连接都需要读取他的七个PGXACT,然后修改自己的PGXACT;这时读取的其他七个PGXACT也在被不停的修改中,这样读取的PGXACT就会经常失效了。可能刚刚缓存好,别人把xmin修改了导致缓存的PGXACT失效,需要从内存重新拿上来。
  • 第二点:xmin其实是不需要访问的,需要的是计算出一个全局最小xid。但是不访问xmin也没什么用,因为xmin和xid在一个cache line上,xmin虽然不访问,但是会修改。 改了就会让整个cache line失效,导致xid的访问也很慢。

关于第二点:

代码作者优化掉了RecentGlobalXminHorizon(用于清理死元组),引入了两个没那么准确的新值:

  • A:确定比A小的都已经可以清理了
  • B:确定比B大的都无法清理

在A、B中间的元组会引入昂贵的准确计算,避免在GetSnapshotData中计算引发上述问题。

2 瓶颈点&优化:数据离散访问

PGXACT的存放位置是离散的,可能在allPgXact大数组中的任意几个位置,不连续。(参考《Postgresql源码(18)PGPROC相关结构》)在使用时,通过连续的pgprocnos数组中记录的index找到活跃的PGXACT。

代码语言:javascript
复制
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单独拿出来放到连续存储的密集数组中,可以显著提高命中率。

3 瓶颈点&优化:快照缓存

  • 经过上面两个优化(都在优化O(n)中的n),在连接数很高时计算一个快照仍然是一个昂贵的操作。
  • 经过上面优化后,快照中不在维护RecentGlobalXmin,这样就可以做一个更大的改进:如果我们可以确定快照没变,就不用重新计算了。以前这是不可行的,因为RecentGlobalXmin比快照本身的更新频率要高很多。
  • 快照唯一需要更新的时机是:之前运行的一个事务提交了。(快照最根本的功能是提供运行事务id列表,列表中最大的那个就是xmax了,事务提交后就相当于不在运行事务列表中了,在做可见性判断时如果不在运行事务列表中,就知道事务已经提交或回滚了,这就需要继续走标志位 或 clog判断了;但是如果事务ID在运行事务列表中,那就一定没提交,不需要做任何进一步判断)
  • 因此,作者设计了一个xactCompletion内存计数器,记录提交或终止的事务数量。
代码语言:txt
复制
- 当要求重新计算快照时,我们检查计数器,如果没变可以直接复用上一个快照。
- 如果计数器变了,那么需要重新计算快照。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PG的新快照体系Globalvis分析
    • 一、概念回顾
      • 1 MVCC
      • 2 快照
      • 3 快照获取
      • 4 历史优化
    • 二、识别当前的瓶颈点
      • 1 瓶颈点&优化:ping pong
      • 2 瓶颈点&优化:数据离散访问
      • 3 瓶颈点&优化:快照缓存
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档