PostgresSQL主备间快照共享探索
关于 PolarDB PostgreSQL 版
PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。
背景
本文主要介绍postgresql的事务快照和可见性判断原理,对主节点和备节点之间进行快照共享的方式进行探索,主备快照共享的机制可以实现主节点与备节点之间的逻辑数据一致性校验。
快照
PostgreSQL在数据库更新和删除时,新数据对象被直接插入到相关表页中。在读取对象时,PostgreSQL根据可见性检查规则,为每个事务选择合适的对象版本作为响应。在可见性检查时会使用到事务快照,事务快照是一个事务集合,存储着某个特定事务在某个特定时间点所看到的事务状态信息:哪些事务处于活跃状态。活跃状态的事务意味着事务正在进行中或还未开始,这些事务所修改或插入的元组对当前的事务快照来说都是不可见的。借此机制,可根据获取快照的策略和时间点,实现不同的事务隔离级别,例如可重复读(RR)、读已提交(RC)等等。
快照导入导出
使用pg_export_snapshot()函数,可将当前session1的快照以文件的形式导出到pg_snapshot文件夹,然后同一实例的另一个session2可将其快照导入,至此session1和session2共享同一快照,具有相同的可见性,可以看到一致的数据。这在做并发表数据导出时比较有帮助,多个session可以并发将db数据导出且不同表之间的数据是有逻辑一致性的。
如果在不同的数据库实例之间(例如主库和备库),快照能够实现共享,那么就可以凭借快照机制进行主库和备库之间的逻辑数据一致性了。因为主库和备库上的不同session若处在同一快照下,那么二者看到的数据是相同的。出于这一个动机,本文从快照和可见性判断的原理上分析了主库和备库之间能否实现共享快照进行了探索。
快照形式
使用内置函数查看快照
SELECT txid_current_snapshot();
txid_current_snapshot
---------------------
100:104:100,102
(1 row)
txid_current_snapshot的文本表示是xmin:xmax:xip_list
xmin: 最早仍然处于活跃事务的txid。早于xmin提交的事务所进行的修改对当前快照可见。
xmax: 第一个尚未分配的txid。大于等于xmax提交的事务所进行的修改对当前快照不可见。
xip_list: 获取快照是活跃事务的txid列表。活跃事务所进行的修改对当前快照不可见,即使此时某些活跃事务已经提交。
主库与备库导出的快照格式

快照文件中,主要包括以下信息
vxid、pid、dbid、隔离级别等基础信息
xmin、xmax
xip列表和sxp列表,分别为活跃事务列表与子事务活跃列表。注意,在备库快照中不区分xip和sxp,所有活跃事务xid均放入sxp列表中
sof表示子事务列表是否有溢出,当sof为1时,sxp数组强制写全空
rec表示是否为恢复期间打的快照,备库的事物快照该值默认为1
主库备库快照格式映射
图中的主库快照以及备库快照在逻辑上是同构的,可以设定一个从主库格式到备库快照格式的映射关系:将xip数组列表统一移动到sxp列表中,将rec置为1,重新赋值vxid,pid值,使的该快照在格式上符合备库快照格式,理论上能够被备库导入。
但进行快照导入时,还有较多需要考虑的case,可能会影响到备库快照的正确性,本文将从MVCC可见性判断的原理开始分析。
MVCC可见性判断

HeapTupleSatisfiesMVCC

此处逻辑整理参考了:https://cloud.tencent.com/developer/article/2000693
XidInMVCCSnapshot


快照获取&导出
事务内执行第一个sql时,会调用快照获取函数GetSnapshotData()获取快照信息,并将快照信息复写到ActiveSnapshot->as_snap变量中。
获取快照GetSnapshotData()

KnownAssignedXidsGetAndSetXmin()

主备事务id同步
主备之间的事务id通过wal日志的流复制机制进行同步:备库在共享内存中维护了一个名为KnownAssianedXids的数组,用来同步记录收到的事务或子事务id。
主节点在开启事务或子事务的时候,会通过 AssignTransactionId 函数给子事务生成xid,当开启的是子事务时,同时会在主节点的subtrans SLRU中维护subxid 与 parentxid的映射关系。当主库在事务内执行第一条insert操作时,会写入wal日志,假设该record xid为100,当备库接受这条wal日志进行redo时会调用RecordKownAssignedTransactionIds()函数将100这个事务id放入KnownAssignedXids列表中,此时备库就会知道此wal日志对应的事务id处于活跃状态。
主节点在AssignTransactionId时,会计数unreported subxid,如果当前事务为子事务,开启的subxid数量达到 PGPROC_MAX_CACHED_SUBXIDS 个时,就会把这些subxid信息放入WAL中,日志类型就是XLOG_XACT_ASSIGNMENT,内容就是topxid与subxids。
typedef struct xl_xact_assignment
{
TransactionId xtop; /* assigned XID's top-level XID */
int nsubxacts; /* number of subtransaction XIDs */
TransactionId xsub[FLEXIBLE_ARRAY_MEMBER]; * assigned subxids */
} xl_xact_assignment;
当主库的事务发生提交时,备库会回放该条cmmit日志,会将与之对应的xid移除。
子事务溢出:
当主库子某个session子事务溢出时(到达64),会向备库发送一条wal日志,备库会将64个子事务保存在pg_subtrans中,余量sub xid依然维护在KnownAssignedXids中。所以,当主库子事务产生溢出时,其打出的快照是不可以在备库中使用的,会导致备库快照子事务信息不完整而使的主库和备库对某张表的逻辑数据查询结果不一致。所以,当主库的快照存在子事务溢出情况时,该快照不能同步到备库,否则可能导致查询结果错乱。
总结:
备库的xid维护完全依据wal日志回放实现,则当前备库的xid状态一定是能找到主库的前某个时刻一一对应的状态。
推论,在主库某时刻打出的快照,若未产生子事务溢出情况,则一定可以在某个时刻的备库上打出逻辑上完全同构的快照。
快照导入
通过ImportSnapshot()函数将pg_snapshot目录下的指定快照进行校验,校验成功后将快照导入。

实例间快照同步
原生PG不支持在实例间共享快照,原因主要分为以下几点:1. 主库与备库之间的快照格式差异,2. vxid、pid等信息无法进行实例间共享,3. 缺乏同步的策略,造成快照导入不成功或者新session [xmin, xmax]内的数据被vacuum。
基于这些因素,若对主库打出的快照进行格式加工与验证,将整理后的快照文件传输到备库并在session中导入后,可实现主备在同一共享快照下的逻辑数据一致性校验。
同步流程
备库standby节点开启RR只读事务tx1,并查询vxid、sesssion进程的pid等信息,这些信息将用于快照的编辑
在主库primary节点开启RR只读事务tx2,调用pg_export_snapshot API导出快照
部署一个能同时连接主库和备库的Checker脚本,通过pg_read_file() API读取快照信息,将快照信息进行校验和重构后使用pg_file_write() API将快照写入standby节点
备库stnadby开启RR事务tx3,将快照信息导入,快照导入完毕后可将standby的另一事务tx1回滚
将快照用于数据校验,校验结束后tx2和tx3回滚





