从源码角度,分析PG事务号分配机制和防止事务号回卷

本文涉及的产品
PolarClaw,2核4GB
简介: 从源码角度,分析PG事务号分配机制和防止事务号回卷

缘起:

最近碰到客户遇到事务号回卷问题,导致业务停摆。相关报错如下:

ERROR: database is not accepting commands to avoid wraparound data loss in database "xxx"
HINT: Stop the postmaster and vacuum that database in single-user mode. You might also need to commit or roll back old prepared transactions, or drop stale replication slots.

之前虽然也碰到过类似问题,但没有真正深入了解,这次趁机会能够尽可能了解详细。

PG为什么要有事务号?

接触数据库的可能都听说过逻辑时钟,像Oracle的SCN(system change number),MySQL的lsn(log sequence number),包括PG的xid,其实都是为了比较事务的先后,实现MVCC(Multi-version Concurrency Control)中SI(snapshot isolation)。

PG的事务号是32位的无符号整形,定义如下,所以可用的事务号大概在42亿:

typedef uint32 TransactionId;

那么就要引申出下一个问题?用光了怎么办?

其实在PG内部,把事务号看做一个圆⭕️,循环的去使用事务号。这样就不会出现用光的情况了。

那么就要引申出下一个问题?既然是循环使用,怎么比较大小?怎么保证逻辑上是晚生成的事务号,但却比之前的事务号小?

提示:PG中有2类事务号,>=3的为正常事务号;0 1 2事务号有特殊含义,并且永远比正常事务号小
PG源码中事务号定义如下:
#define InvalidTransactionId    ((TransactionId) 0)  不可用的事务id 
#define BootstrapTransactionId    ((TransactionId) 1)  pg初始化cluster时用的事务id
#define FrozenTransactionId     ((TransactionId) 2)  frozen的事务id

PG使用了比较讨巧的方式,看下面比较事务号的代码:

/*
 * TransactionIdFollowsOrEquals --- is id1 logically >= id2?
 */
bool
TransactionIdFollowsOrEquals(TransactionId id1, TransactionId id2)
{
  int32   diff;
  if (!TransactionIdIsNormal(id1) || !TransactionIdIsNormal(id2))
    return (id1 >= id2); //如果有非正常事务号,永远是非正常事务号小
  diff = (int32) (id1 - id2); //如果
  return (diff >= 0);  
}

通过定义了frozen事务id来保证,所有足够久的事务都对所有事务可见。通过autovaccum 进程定期会扫描page中已经足够久行,把事务号标记成frozen事务号。(这里用“足够久”来解释,其实PG中有对应参数可以设置)。

如何保证并发获取

因为事务号是全局共享的,所以存储在共享内存中,在获取事务号之前,会去持有全局唯一独占锁XidGenLock的lwlock:LWLockAcquire(XidGenLock, LW_EXCLUSIVE)。

define XidGenLock (&MainLWLockArray[3].lock)

MainLWLockArray是一个lwlock的数组,XidGenLock是数组中的第三个元素。LWLock.state 是locker的状态。

*   lwlock.h
*   Lightweight lock manager
typedef struct LWLock
{
  uint16    tranche;    /* tranche ID */
  pg_atomic_uint32 state;   /* state of exclusive/nonexclusive lockers */
  proclist_head waiters;    /* list of waiting PGPROCs */
#ifdef LOCK_DEBUG
  pg_atomic_uint32 nwaiters;  /* number of waiters */
  struct PGPROC *owner;   /* last exclusive owner of the lock */
#endif
} LWLock;
* src/include/port/atomics/fallback.h
typedef struct pg_atomic_uint32
{
  /* Check pg_atomic_flag's definition above for an explanation */
#if defined(__hppa) || defined(__hppa__)  /* HP PA-RISC, GCC and HP compilers */
  int     sema[4];
#else
  int     sema;
#endif
  volatile uint32 value;
} pg_atomic_uint32;
proc是每个进程共享内存的结构
/*
 * src/include/storage/proclist_types.h
 * Header of a doubly-linked list of PGPROCs, identified by pgprocno.
 * An empty list is represented by head == tail == INVALID_PGPROCNO.
 */
typedef struct proclist_head
{
  int     head;     /* pgprocno of the head PGPROC */
  int     tail;     /* pgprocno of the tail PGPROC */
} proclist_head;

LWLockAcquire是获取轻量级锁的函数,如果锁不可用,则sleep到可用为止。如果能马上获取则返回true,返回flase则只能sleep。

函数主调用栈:LWLockAcquire--> LWLockAttemptLock --> pg_atomic_compare_exchange_u32--> pg_atomic_compare_exchange_u32_impl(这个就是所谓的CAS原子操作:compare and swap)

static bool
LWLockAttemptLock(LWLock *lock, LWLockMode mode)
{
/* loop until we've determined whether we could acquire the lock or not */
  while (true)
  {
    uint32    desired_state;
    bool    lock_free;
    desired_state = old_state;
    if (mode == LW_EXCLUSIVE)
    {
      lock_free = (old_state & LW_LOCK_MASK) == 0;
      if (lock_free)
        desired_state += LW_VAL_EXCLUSIVE;
    }
    else
    {
      lock_free = (old_state & LW_VAL_EXCLUSIVE) == 0;
      if (lock_free)
        desired_state += LW_VAL_SHARED;
    }
    ...
  }
  ...
}

old_state用来保存locker的旧状态值。LW_LOCK_MASK =  00000001 11111111 11111111 11111111

(gdb)p old_state
$5 = 536870912
(gdb) p /x old_state
$6 = 0x20000000
(gdb) p /t old_state
$7 = 100000 00000000 00000000 00000000
 old_state & LW_LOCK_MASK =  0 所以 lock_free 是true
desired_state += LW_VAL_EXCLUSIVE;  (LW_VAL_EXCLUSIVE =1 00000000 00000000 00000000)
(gdb) p desired_state
$9 = 553648128
(gdb) p /x desired_state
$10 = 0x21000000
(gdb) p /t desired_state
$11 = 100001 00000000 00000000 00000000

可以看到其实__asm__用的就是内嵌汇编,核心就是cmpxchgl这个汇编指令。比较newval和*expected(也就是old_state) 是否相等,如果相等的话,就 setz就是取ZF(zero flag)的值,设置到ret。

if (pg_atomic_compare_exchange_u32(&lock->state,
                       &old_state, desired_state))
#define PG_HAVE_ATOMIC_COMPARE_EXCHANGE_U32
static inline bool
pg_atomic_compare_exchange_u32_impl(volatile pg_atomic_uint32 *ptr,
                  uint32 *expected, uint32 newval)
{
  char  ret;
  /*
   * Perform cmpxchg and use the zero flag which it implicitly sets when
   * equal to measure the success.
   */
  __asm__ __volatile__(
    " lock        \n"
    " cmpxchgl  %4,%5 \n"
    "   setz    %2    \n"
:   "=a" (*expected), "=m"(ptr->value), "=q" (ret)
:   "a" (*expected), "r" (newval), "m"(ptr->value)
:   "memory", "cc");
  return (bool) ret;
}
伪代码:
%rax = *expected;
if ( *expected ==ptr->value) {
    ptr->value = newval;
    ZF = 1;
} else {
    %rax = ptr->value
    ZF = 0;
}
*expected = %rax
ret = ZF;
return ret;
 
         

相关参数优化

与 vacuum freeze 相关的参数主要有三个:

1. vacuum_freeze_min_age

2. vacuum_freeze_table_age

3. autovacuum_freeze_max_age

vacuum_freeze_min_age

表示表中每个元组需要 freeze 的最小年龄。这里值得一提的是每次表被 freeze 之后,会更新 pg_class 中的 relfrozenxid 列为本次freeze 的 XID。 表年龄就是当前的最新的 XID 与 relfrozenxid 的差值,而元组年龄可以理解为每个元组的t_xmin 与 relfrozenxid 的差值。所以,这个参数也可以被简单理解为每个元组两次被 freeze 之间的 XID 差值的一个最小值。 增大该参数可以避免一些无用的 freeze 操作,减小该参数可以使得在表必须被强制清理之前保留更多的 XID 空间。该参数最大值为 20 亿,最小值为 2 亿。

普通的 vacuum 使用 visibility map 来快速定位哪些数据页需要被扫描,只会扫描那些脏页,其他的数据页即使其中元组对应的 xmin非常旧也不会被扫描。而在 freeze 的过程中,我们是需要对所有可见且未被 all-frozen 的数据页进行扫描,这个扫描过程PostgreSQL称为 aggressive vacuum。每次vacuum 都去扫描每个表所有符合条件的数据页显然是不现实的, 所以我们要选择合理的 aggressive vacuum 周期。 PostgreSQL 引入了参数 vacuum_freeze_table_age 来决定这个周期。

vacuum_freeze_table_age

表示表的年龄大于该值时,会进行 aggressivevacuum,即扫描表中可见且未被 all-frozen 的数据页。该参数最大值为 20 亿,最小值为 1.5 亿。 如果该值为 0,则每次扫描表都进行 aggressive vacuum。

直到这里,我们可以看出:

当表的年龄超过 vacuum_freeze_table_age 则会 aggressive vacuum。当元组的年龄超过 vacuum_freeze_min_age 后可以进行 freeze。为了保证整个数据库的最老最新事务差不能超过 20 亿的原则,两次 aggressivevacuum 之间的新老事务差不能超过 20 亿,即两次 aggressive vacuum 之间表的年龄增长( vacuum_freeze_table_age)不能超过 20 亿减去vacuum_freeze_min_age(只有元组年龄超过 vacuum_freeze_min_age 才会被freeze)。 但是看上面的参数,很明显不能绝对保证这个约束,为了解决这个问题, PostgreSQL 引入了 autovacuum_freeze_max_age 参数。22autovacuum_freeze_max_age 表示如果当前最新的 XID 减去元组的t_xmin 大于等于 autovacuum_freeze_max_age,则元组对应的表会强制进行autovacuum,即使 PostgreSQL 已经关闭了 autovacuum。该参数最小值为 2亿, 最大值为 20 亿。

也就是说, 在经过 autovacuum_freeze_max_age - vacuum_freeze_min_age 的XID 增长之后,这个表肯定会被强制地进行 一次 freeze。因为autovacuum_freeze_max_age 最大值为 20 亿, 所以说在两次 freeze 之间, XID 的增长肯定不会超过 20 亿,这就保证了上文中整个数据库的最老最新事务差不能超过 20 亿的原则。

值得一提的是, vacuum_freeze_table_age 设置的值如果比autovacuum_freeze_max_age 要高,则每次 vacuum_freeze_table_age 生效地时候, autovacuum_freeze_max_age 已经生效,起不到过滤减少数据页扫描的

作用。所以默认的规则, vacuum_freeze_table_age 要设置的比autovacuum_freeze_max_age 小。 但是也不能太小,太小的话会造成频繁的aggressive vacuum。

另外我们通过分析源码可知, vacuum_freeze_table_age 在最后应用时,会去取min(vacuum_freeze_table_age,0.95 autovacuum_freeze_max_age)。所以官方文档推荐 vacuum_freeze_table_age=0.95 autovacuum_freeze_max_age。

23freeze 操作会消耗大量的 IO,对于不经常更新的表,可以合理地增大autovacuum_freeze_max_age 和 vacuum_freeze_min_age 的差值。

但是如果设置 autovacuum_freeze_max_age 和 vacuum_freeze_table_age 过大, 因为需要存储更多的事务提交信息,会造成 pg_xact 和 pg_commit 目录占用更多的空间。例如,我们把 autovacuum_freeze_max_age 设置为最大值

20 亿, pg_xact 大约占 500MB, pg_commit_ts 大约是 20GB(一个事务的提交状态占 2 位) 。 如果是对存储比较敏感的用户,也要考虑这点影响。

而减小 vacuum_freeze_min_age 则会造成 vacuum 做很多无用的工作, 因为当数据库 freeze 了符合条件的 row 后,这个 row 很可能接着会被改变。 理想的状态就是,当该行不会被改变,才去 freeze 这行。

相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
SQL 缓存 运维
PostgreSQL 事务号回卷分析
## XID 定义 xid 是个啥东西?xid 就是 PostgreSQL 里面的事务号,每个事物都会分配一个 xid。PostgreSQL 数据中每个元组头部都会保存着 插入 或者 删除 这条元组的事务号,即 xid,然后内核通过这个 xid 进行元组的可见性判断。简单理解,比如有两个事务,xid1=200,xid2=201,那么 xid1 中只能看到 t_xmin 200 的元组。 ```c
|
6月前
|
存储 Cloud Native 关系型数据库
PolarDB-PG IMCI实战解析:深度融合DuckDB,复杂查询性能最高百倍级提升
阿里云PolarDB PostgreSQL版创新融合DuckDB向量化引擎,推出IMCI列存索引,实现HTAP一体化。支持实时交易与复杂分析并行,查询性能提升60-100倍,兼容PG生态,秒级数据同步,助力企业高效挖掘数据价值。
846 0
|
JSON 关系型数据库 PostgreSQL
PostgreSQL 9种索引的原理和应用场景
PostgreSQL 支持九种主要索引类型,包括 B-Tree、Hash、GiST、SP-GiST、GIN、BRIN、Bitmap、Partial 和 Unique 索引。每种索引适用于不同场景,如 B-Tree 适合范围查询和排序,Hash 仅用于等值查询,GiST 支持全文搜索和几何数据查询,GIN 适用于多值列和 JSON 数据,BRIN 适合非常大的表,Bitmap 适用于低基数列,Partial 只对部分数据创建索引,Unique 确保列值唯一。
1398 15
|
9月前
|
存储 设计模式 IDE
从基础到高级的 Java 学习资料全面汇总
本文汇总了Java学习的全面资料,涵盖Java基础、面向对象编程、核心知识、高级特性及常用框架,如Spring和Hibernate。内容包括技术详解、代码实例及学习资源推荐,助力从入门到精通Java编程,适合各阶段学习者参考。
642 0
|
SQL Oracle 关系型数据库
【YashanDB 知识库】崖山有哪些内存参数,Share Pool 各个参数之间有什么关系
在使用YashanDB时,用户常对内存参数配置有疑问,尤其是23.2及以上版本中,如SQL_POOL_SIZE+DICTIONARY_CACHE_SIZE超100报错,影响跑批性能。主要内存参数包括SHARE_POOL_SIZE、SQL_POOL_SIZE、DICTIONARY_CACHE_SIZE等,需合理配置以优化性能。SHARE POOL内含多个POOL,可动态调整。具体配置方法及观察使用情况的方式详见官网文档。
|
JSON JavaScript 前端开发
深入浅出Node.js:从零开始构建RESTful API
在数字化时代的浪潮中,后端开发作为连接用户与数据的桥梁,扮演着至关重要的角色。本文将引导您步入Node.js的奇妙世界,通过实践操作,掌握如何使用这一强大的JavaScript运行时环境构建高效、可扩展的RESTful API。我们将一同探索Express框架的使用,学习如何设计API端点,处理数据请求,并实现身份验证机制,最终部署我们的成果到云服务器上。无论您是初学者还是有一定基础的开发者,这篇文章都将为您打开一扇通往后端开发深层知识的大门。
392 12
|
运维 监控 关系型数据库
【一文搞懂PGSQL】7. PostgreSQL + repmgr + witness 高可用架构
该文档介绍了如何构建基于PostgreSQL的高可用架构,利用repmgr进行集群管理和故障转移,并引入witness节点增强网络故障检测能力。repmgr是一款轻量级的开源工具,支持一键部署、自动故障转移及分布式节点管理。文档详细描述了环境搭建步骤,包括配置postgresql参数、安装与配置repmgr、注册集群节点以及配置witness节点等。此外,还提供了故障手动与自动切换的方法及常用命令,确保集群稳定运行。
|
机器学习/深度学习 人工智能 算法
转载:【AI系统】AI系统概述与设计目标
本文介绍了AI系统全栈架构,涵盖AI系统的设计目标、组成和生态,强调了系统性能、用户体验、稳定性及开源社区运营的重要性。文章详细解析了AI系统的基本概念、定义及其设计挑战,旨在为读者构建AI系统知识体系,助力AI技术的全面发展。通过对比传统软件和云计算架构,阐述了AI系统在连接硬件与应用间的独特作用,探讨了高效编程语言、开发框架和工具链的设计,以及AI任务的系统级支持,包括计算能力的扩展、自动编译优化、云原生自动分布式化等方面,为应对AI技术的新挑战提供了全面视角。
|
监控 持续交付 数据安全/隐私保护
Python进行微服务架构的监控
【6月更文挑战第16天】
Python进行微服务架构的监控
|
SQL 关系型数据库 C语言
PostgreSQL SQL扩展 ---- C语言函数(三)
可以用C(或者与C兼容,比如C++)语言编写用户自定义函数(User-defined functions)。这些函数被编译到动态可加载目标文件(也称为共享库)中并被守护进程加载到服务中。“C语言函数”与“内部函数”的区别就在于动态加载这个特性,二者的实际编码约定本质上是相同的(因此,标准的内部函数库为用户自定义C语言函数提供了丰富的示例代码)

热门文章

最新文章

下一篇
开通oss服务