关于Greenplum数据库中的并发控制

关于Greenplum数据库中的并发控制

Greenplum数据库使用了PostgreSQL的多版本并发控制(MVCC)模型来管理对于堆表的并发事务。

数据库管理系统中的并发控制允许在确保数据库的完整性的前提下,并发查询能够完成并且得到正确的结果。传统的数据库使用两阶段锁协议来阻止一个事务修改已经被另一个并发事务读取的数据并且阻止任何并发事务读取或者写入另一个事务已经更新的数据。协调事务所需的锁增加了数据库中的竞争,降低了总体事务吞吐量。

Greenplum数据库使用PostgreSQL的多版本并发控制(MVCC)模型来管理堆表的并发。通过MVCC,每一个查询都在它开始时的一个数据库快照上操作。在执行时,一个查询不能看到其他并发事务所作出的更改。这确保了一个查询看到的是数据库的一个一致的视图。读取行的查询不会被写入行的事务所阻塞。反过来,写入行的查询也不会被读取行的事务所阻塞。这使得Greenplum可以达到比使用锁来协调读写事务的传统数据库系统更高的并发度。

注意:追加优化表使用一种不同于MVCC的并发控制模型管理。它们是为了“一次写,多次读”的应用而设计,这些应用从不或者很少会进行行级更新。

快照

MVCC模型依赖于系统能够管理数据行的多个版本的能力。一个查询其实是在该查询开始时的数据库快照上操作。快照就是在一个语句或者事务开始时可见的行的集合。快照保证查询在其执行期间看到的是数据库的一个一致且合法的视图。

每一个事务都会被分配一个唯一的事务ID(XID),它是一个增量式的32位值。当一个新事务开始时,它被分配下一个XID。没有被包裹在一个事务中的一个SQL语句会被当做一个单语句事务,即会给它隐式地加上BEGINCOMMIT。这和一些数据库系统中的自动提交概念类似。

注意: Greenplum数据库只为涉及DDL或者DML操作的事务分配XID值,它们通常是唯一需要XID的事务。

当一个事务插入一行时,其XID会被保存在该行的 xmin系统列中。当一个事务删除一行时,其XID会被保存在xmax系统列中。更新一行被视为一次删除加上一次插入,因此XID会被保存在当前行的xmax中以及新插入行的xmin中。xminxmax列再加上事务完成状态就指定了一个事务的范围,行的这个版本对于其中的事务可见。一个事务可以看到所有小于xmin的事务的效果,这些事务确保已经被提交,但它无法看到任何大于等于xmax的事务的效果。

多语句事务还必须记录一个事务中哪个命令插入了一行(cmin)或者删除了一行(cmax),这样事务能够看到事务中先前的命令所作的更改。命令序列只在事务期间有意义,因此在一个事务开始时该序列被重置为0。

XID是数据库的一个性质。每一个Segment数据库都有其自己的XID序列,因此不能拿它和其他Segment数据库的XID进行比较。Master会使用一个集群范围的会话ID号来与Segment协调分布式事务,会话ID号被称为gp_session_id。Segment会会维护一个分布式事务ID到其本地XID的映射。Master用两阶段提交协议在所有Segment之间协调分布式事务。如果一个事务在任一一个Segment上失败,它将会在所有Segment上回滚。

用户可以用一个SELECT语句查看任意行的xminxmaxcmincmax列:
SELECT xmin, xmax, cmin, cmax, * FROM tablename;

因为用户是在Master上运行该SELECT命令,看到的XID都是分布式事务ID。如果用户能在一个Segment数据库上执行该命令,xminxmax值将是该Segment的本地XID。

事务ID回卷

MVCC模型使用事务ID(XID)来判断哪些行在一个查询或者事务开始时是可见的。XID是一个32位值,因此在该值溢出并且回卷到零之前,一个数据库理论上可以执行超过四十亿个事务。不过,Greenplum数据库使用模 232的计算方式来使用XID,这允许事务ID回卷,就像时钟会在十二点回卷一样。对于任何给定的XID,有大约二十亿个过去的XID和二十亿个未来的XID。直到一行的一个版本存在大约二十亿个事务之前,这一套机制都有效,当这种情况发生时那个版本就会突然变成一个新行。为了阻止这种情况的发生,Greenplum有一个被称为 FrozenXID的特殊XID,当把它和任何其他常规XID比较时它都是较老的哪一个。如果行中的xmin位于那二十亿个事务之中,就必须被替换为FrozenXID,这也是VACUUM命令执行的功能之一。

至少在每二十亿个事务时清理数据库可以阻止XID回卷。Greenplum数据库会监控事务ID并且在需要一次VACUUM操作时做出告警。

当不再可用的事务ID达到可观的比例并且事务ID回卷还没发生时,将会发出一个警告:
WARNING: database "database_name" must be vacuumed within number_of_transactions transactions

当该警告被发出时,就需要一次VACUUM操作。如果没有执行所需的VACUUM操作,当Greenplum数据库达到事务ID回卷发生之前的一个限制点时,它将会停止创建新事务来避免可能的数据丢失并且发出这样的错误:

FATAL: database is not accepting commands to avoid wraparound data loss in database "database_name"

关于从这种错误恢复的过程请见从一次事务ID限制错误中恢复

服务器配置参数xid_warn_limitxid_stop_limit控制何时显示这些警告和错误。 xid_warn_limit参数指定在xid_stop_limit之前多少个事务ID时发出警告。 xid_stop_limit参数指定在回卷发生之前多少个事务ID时发出错误并且不允许创建新的事务。

事务隔离模式

SQL标准描述了数据库事务并发运行时可能发生的三种现象:
  • 脏读 – 一个事务可能读到来自另一个并发事务的未提交数据。
  • 不可重复读 – 在一个事务中两次读取同一行得到不同的结果,因为另一个并发事务在这个事务开始后提交了更改。
  • 幻读 – 在同一个事务中两次执行同一个查询可能返回不同的行集合,因为另一个并发事务增加了行。

SQL标准定义了数据库系统必须支持的四种事务隔离模式:

表 1. 事务隔离模式
级别 脏读 不可重复读 幻读
读未提交 可能 可能 可能
读已提交 不可能 可能 可能
可重复读 不可能 不可能 可能
可序列化 不可能 不可能 不可能

Greenplum数据库的SQL命令允许用户请求READ UNCOMMITTEDREAD COMMITTED或者SERIALIZABLE。Greenplum数据库把READ UNCOMMITTED视作与READ COMMITTED相同。请求REPEATABLE READ会产生一个错误,请使用SERIALIZABLE代替。默认的隔离模式是READ COMMITTED

READ COMMITTEDSERIALIZABLE之间的区别在于,在READ COMMITTED模式中,一个事务中的每个语句只能看到在该语句开始前提交的行,而在SERIALIZABLE模式中,一个事务中的所有语句都只能看到在该事务开始前提交的行。

The READ COMMITTED隔离模式允许比SERIALIZABLE模式更好的并发性和更好的性能。它允许不可重复读,也就是在一个事务中两次检索同一行的值可以不同,因为另一个并发事务在该事务开始后提交了修改。 READ COMMITTED模式也允许幻读,即在同一个事务中两次执行同一个查询可能返回不同的行集合。

SERIALIZABLE隔离模式能同时防止不可重复读以及幻读,但是其代价是并发度和性能。每一个并发事务都有一个取自其执行开始处的一致的数据库视图。一个尝试修改已被另一事务修改过的数据的并发事务将被回滚。如果一个应用执行的事务处于SERIALIZABLE模式中,它必须准备好处理事务由于序列化错误而失败的问题。如果应用不是非用SERIALIZABLE隔离模式不可,最好使用READ COMMITTED模式。

SQL标准定义并发可序列化事务产生的数据库状态和它们顺序执行时产生的数据库状态相同。MVCC快照隔离模型在没有使用昂贵的锁定方法的前提下,能阻止脏读、不可重复读和幻读。但是在Greenplum数据库中的某些SERIALIZABLE事务之间可能会出现其他相互作用,导致它们无法真正地可序列化。这些异常往往都是由于Greenplum数据库没有执行谓词逻辑锁导致,这意味着一个事务中的写操作可以影响另一个并发事务中先前的读操作的结果。

并发运行的事务应该接受检查以发现没有被禁止并发更新同一数据所阻止的相互作用。发现的问题可以通过使用显式表锁定或者要求冲突的事务更新一个表示该冲突的虚假行来阻止。

SQL语句SET TRANSACTION ISOLATION LEVEL为当前事务设置隔离模式。模式必须在任何SELECTINSERTDELETEUPDATE或者 COPY语句之前设置:
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
...
COMMIT;
隔离模式也可以作为BEGIN语句的一部分指定:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

一个会话的默认事务隔离模式可以通过设置default_transaction_isolation配置属性来修改。

从表中移除死亡的行

更新或者删除一行会在表中留下该行的一个过期版本。当一个过期的行不在被任何活跃事务引用时,它可以被移除从而腾出其所占用的空间进行重用。VACUUM命令会标记过期行所使用的空间可以被重用。

当表中的过期行累积后,为了容纳新的行就必须扩展磁盘文件。这样执行查询所需的磁盘I/O就会增加,从而性能受到影响。这种情况被称为膨胀,并且应该通过定期清理表来解决。

VACUUM命令(不带FULL)可以与其他查询并行运行。它会标记之前被过期行所占用的空间为空闲可用。如果剩余的空闲空间数量可观,它会把该页面加到该表的空闲空间映射中。当Greenplum数据库之后需要空间分配给新行时,它首先会参考该表的空闲空间映射以寻找有可用空间的页面。如果没有找到这样的页面,它会为该文件追加新的页面。

VACUUM(不带FULL)不会合并页面或者减小表在磁盘上的尺寸。它回收的空间只是放在空闲空间映射中表示可用。为了阻止磁盘文件大小增长,重要的是足够频繁地运行VACUUM。运行VACUUM的频率取决于表中更新和删除(插入只会增加新行)的频率。重度更新的表可能每天需要运行几次VACUUM来确保通过空闲空间映射能找到可用的空闲空间。在运行了一个更新或者删除大量行的事务之后运行VACUUM也非常重要。

VACUUM FULL命令会把表重写为没有过期行,并且将表减小到其最小尺寸。表中的每一页都会被检查,其中的可见行被移动到前面还没有完全填满的页面中。空页面会被丢弃。该表会被一直锁住直到VACUUM FULL完成。相对于常规的VACUUM命令来说,它是一种非常昂贵的操作,可以用定期的清理来避免或者推迟这种操作。最好是在一个维护期来运行VACUUM FULLVACUUM FULL的一种替代方案是用一个CREATE TABLE AS语句重新创建该表并且删除掉旧表。

空闲空间映射位于共享内存中,它跟踪着所有表和索引的空闲空间。每一个表或者索引使用大约60字节的内存,每一个具有空闲空间的页面会使用6个字节。配置空闲空间映射尺寸的两个系统配置参数是:

max_fsm_pages
设置可以加入到共享空闲空间映射的磁盘页最大数量。每一个页槽需要消耗共享内存的6个字节。默认值是200000。这个参数必须被设置为max_fsm_relations值的至少16倍。
max_fsm_relations

设置共享内存中空闲空间映射所跟踪的关系的最大数量。这个参数应该被设置为一个大于表数量 + 索引数量 + 系统表数量总和的值。默认值为1000。在每个实例上每一个关系会消耗大约60字节的内存。这个参数值宜高不宜低。

如果空闲空间映射容量不足,一些具有可用空间的磁盘页将无法被加到其中,那么这些空间将无法被重用直至下一次VACUUM命令运行。这将会导致文件尺寸增长。

用户可以运行VACUUM VERBOSE tablename来得到一份Segment上已移除的死亡行数量、受影响页面数以及有可用空闲空间页面数的报告。

查询pg_class系统表可以找出一个表在所有Segment上使用了多少页面。注意首先对该表执行ANALYZE确保得到的是准确的数据。
SELECT relname, relpages, reltuples FROM pg_class WHERE relname='tablename';

另一个有用的工具是gp_toolkit方案中的gp_bloat_diag视图,它通过比较一个表使用的实际页数和预期的页数来确定表膨胀。更多有关gp_bloat_diag的内容请见Greenplum数据库参考指南中的“gp_toolkit管理方案”。