Greenplum保证数据隔离的“秘密武器”:快照

数据库系统需要保证ACID特性,其中的I特指隔离性(Isolation),而多版本控制协议(MVCC)和快照(Snapshot)是实现隔离性的重要手段。 多版本控制协议相关内容在之前的相关文章中已经进行了介绍,本文中将会对Greenplum中快照相关的知识进行简要介绍,欢迎大家留言交流。

本文中涉及到的代码版本是Greenplum 6X的稳定分支(greenplum-db/gpdb at 6X_STABLE (github.com)),其他分支上(如master,5X等)的代码逻辑会有所差异,请读者留意。

01 快照基础

为了实现多版本控制,Greenplum中的堆数据元组(Heap Tuple)的元组头部分存储了这个元组的生命周期(通过事务id标识),它们分别是:

  • xmin,创建这个元组的事务id(txid)
  • xmax,删除这个元组的事务id

(准确说还有命令号cid和各种标记等等,本文中省略掉了对它们的介绍)

对于增删改操作,对它们的操作如下:

  • insert, set xmin=txid_current,set xmax=0
  • delete, set xmax=txid_current
  • update = delete + insert

而快照如其名字,是在一个特定的时刻对系统当前各个事务的运行状态的一个拍摄。其格式可以表示为: xmin : xmax : xip_list (,分割),见如下示例:

`demo=# begin;`
`BEGIN`
`demo=# select txid_current();`
` txid_current`
`--------------`
` 3911`
`(1 row)`

`demo=# select txid_current_snapshot();`
` txid_current_snapshot`
`-----------------------`
` 3911:3915:3912,3913`
`(1 row)`

一个快照表示了:

  • xmin,最早还在活跃的txid,所有txid < 它的事务都已经结束了(提交或者回滚)
  • xmax,下一个要分配的txid,所有txid >= 它的事务尚未开始(即它的操作对此快照不可见)
  • xip_list,xmin和xmax之间的活跃txid

因此对于之前例子中的快照 3911:3915:3912,3913,表示了:对于此快照,txid<3911的事务都已经结束,txid>=3915的事务尚未开始,而3912和3913这2个事务尚在运行中。

02 快照相关函数

在Greenplum代码中使用了SnapshotData结构体来表示:

`typedef struct SnapshotData`
`{`
`SnapshotSatisfiesFunc satisfies;  /* tuple test function */`

`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[] */`

`… // 暂时省略掉其他字段`
`}`

除了我们之前介绍的内容之外,还有一个 satisfies 字段,它是用于这个快照的 可见性判断函数 。 Greenplum中提供了多种不同的可见性判断函数,以供不同的场景所使用。 它们之间的区别简单来说在于观察的视角不同,代码都位于src/backend/utils/time/tqual.c,常见的可见性函数介绍如下表:

具体每个可见性判断函数都是由若干条规则组成的,细节较多,读者可以参考它们的代码实现和函数头上的规则简化注释。

可见性判断函数+快照数据(指xmin/xmax/xip)配合在一起使用,它们像一把”筛子”,对堆元组进行过滤,过滤出来的元组就是对于当前事务可见的元组。例如如下代码:

`heap_fetch()`
`{`
`/*`
` 宏的实现:调用satisfies指向的可见性函数`
` #define HeapTupleSatisfiesVisibility(rel, tuple, snapshot, buffer) \`
` ((*(snapshot)->satisfies) (rel, tuple, snapshot, buffer))`
`*/`
`valid = HeapTupleSatisfiesVisibility(relation, tuple, snapshot, buffer);`
`…`

`if (valid) return true; // 找到了可见的元组`

`…`
`return false;`
`}`

获取快照数据的函数有很多,它们都位于src/backend/utils/time/snapmgr.c中,例如:GetTransactionSnapshot(),GetLatestSnapshot(),GetCatalogSnapshot()等等。这些函数在一般情况下都会调用到GetSnapshotData函数,这个函数的主要操作就是遍历PGPROC数组,来生成调用时刻的快照数据,它的细节读者可以阅读代码或最后参考书目中的内容。

03 获得快照的时机

除了以上介绍的内容之外,快照还有一个要点: 快照的获取时机,即何时生成一个新的快照内容? 或应该复用哪个已经生成了的快照。

以我们熟悉的事务隔离级别为例(这个逻辑位于GetTransactionSnapshot函数中):

  • 读已提交(RC),对于事务中每条语句,都生成一个新的快照数据
  • 可重复读(RR),只有事务的第一条语句才生成快照数据,随后的语句只复用这个快照数据。

但是如上只是比较粗略的获取时机,对于某个特定操作(比如utility函数,vaccum等)的快照获取细节还需要到代码中进行检查(例如和各种锁的配合)。

Greenplum中处理query的入口是QD进行的exec_simple_query(),相关的快照获取时机见下:

`exec_simple_query()`
`{`
`…`
`/*`
` * Set up a snapshot if parse analysis/planning will need one.`
` */`
`// 某些类型query在planner阶段也需要获取快照`
`if (analyze_requires_snapshot(parsetree))`
`{`
`PushActiveSnapshot(GetTransactionSnapshot());`
`snapshot_set = true;`
`}`
`…`
`/* Done with the snapshot used for parsing/planning */`
`if (snapshot_set)`
`PopActiveSnapshot();`
`…`

`// 在PortalStart中调用GetTransactionSnapshot(),为executor的执行做准备`
`PortalStart(portal, NULL, 0, InvalidSnapshot, NULL);`
`…`

`// 在PortalRun中通过CdbDispatchXXX dispatch这个snaphost(一般是activesnapshot)到QE上`
`(void) PortalRun(portal, ...);`
`…`

`}`

`QE进程的执行入口是exec_mpp_query(),它直接使用QD dispatch过来的快照(DtxContextInfo结构体)中:`

`PostgresMain()`
`{`
`…`

`case 'M': /* MPP dispatched stmt from QD */`
`…`

`// 获取QD生成并dispatch过来的(分布式)快照`
`serializedDtxContextInfo = pq_getmsgbytes(&input_message,serializedDtxContextInfolen);`
`…`

`// 已经存储在QEDtxContextInfo全局变量中,可以在exec_mpp_query中使用`
`exec_mpp_query();`
`…`

`}`

通过如上示例中的注释,读者可以发现QE上的快照并不是QE进程本地的快照,而是引入了分布式快照的 内容 ,请看下节中的介绍。

04 分布式快照

让我们回过头来再看SnapshotData结构体,除了之前介绍的xmin等字段之外,最后还有一个distribSnapshotWithLocalMapping字段:

`typedef struct SnapshotData`
`{`
`… (xmin, xmax, xip ...)`

`/*`
` * GP: Global information about which transactions are visible for a`
` * distributed transaction, with cached local xids`
` */`
`DistributedSnapshotWithLocalMapping  distribSnapshotWithLocalMapping; // 结构见下`

`}`

`typedef struct DistributedSnapshotWithLocalMapping`
`{`
`DistributedSnapshot ds; /* DistributedSnapshot结构简略如下:`
`{`
`DistributedSnapshotId distribSnapshotId;`
`DistributedTransactionId xmin;`
`DistributedTransactionId xmax;`
`int32    count;`
`/* Array of distributed transactions in progress. */`
`DistributedTransactionId        *inProgressXidArray;`
` } */`
`…`

`int32 currentLocalXidsCount;`
`int32 maxLocalXidsCount;`
`TransactionId *inProgressMappedLocalXids;`
`} DistributedSnapshotWithLocalMapping;`

即SnapshotData的大部分字段都是内地快照内容,然后通过最后的distribSnapshotWithLocalMapping 字段来绑定到它对应的分布式快照(在Master上生成)上,而分布式快照的数据结构是DistributedSnapshot结构体。

DistributedSnapshot的结构和本地快照非常相似:

  • distribSnapshotId表示分布式事务ID(Dtxid)
  • xmin,xmax和SnapshotData中的作用一致,只不过作用于分布式事务ID上
  • inProgressXidArray对应SnapshotData中的xip,表示运行中的分布式事务ID列表

通过分布式事务信息就可以保证多个Segment上的判断可见性是一致的(即由Master为分布式事务定序),另外还要注意并不是任何时候都需要通过分布式事务信息来判断可见性:例如QE上新创建的堆元组尚未提交时就需要通过本地快照信息来判断它的可见性。

所以大家在查看可见性判断函数(如HeapTupleSatisfiesMVCC)的代码时,请留意其中涉及到分布式事务信息的逻辑(如XidInMVCCSnapshot函数)。

05 总结

本文对Greenplum中快照进行简要的介绍。 网上一般涉及到事务讲解时,对MVCC(如元组头中的xmin/xmax)相关介绍较多,而对快照介绍的相对较少。 读者可以结合本文,对Greeplum代码中涉及到各类快照的使用场景(如catalog访问,index,vacuum等等)进行扩展学习和调试,这样能对快照有更深刻的理解。

06 参考资料

关注微信公众号

VMware 中国研发中心