eygle.com   eygle.com
eygle.com eygle
eygle.com  
 

November 30, 2021

MacOS Monterey在腾讯会议声音不起作用coreaudiod重置

最近总是遇到腾讯会议启动,电脑的音频失效情况。经验值,以下重启 coreaudiod 的逻辑有效。

MacOS Monterey 的系统。

启动终端并输入以下命令:

 sudo killall coreaudiod

Return ,输入您的管理员密码,然后再次检查声音。 coreaudiod进程应该重新启动。

在极少数情况下,可能还存在听不到声音的问题。可以尝试以下命令:

 sudo launchctl start com.apple.audio.coreaudiod

launchctl命令启动守护进程并重新初始化coreaudiod进程。

在我的情况下,声音得到了恢复。

Posted by eygle at 9:10 AM | Permalink | System (106)

November 25, 2021

openGauss 分布式事务

前面我们简要介绍了单机事务和分布式事务的区别,也指出了在分布式情况下,可能存在特有的原子性和一致性问题。本文主要介绍在openGauss数据库中,如何保证分布式事务的原子性和强一致性。

1 分布式事务原子性和两阶段提交协议

为了保证分布式事务的原子性,防止出现如图10-2中所示的部分DN提交、部分DN回滚的"中间态"事务,openGauss采用两阶段提交(2PC)协议。

两阶段提交流程示意图.png

两阶段提交流程示意图

如上图所示,顾名思义,两阶段提交协议将事务的提交操作分为两个阶段:

  • 阶段一,准备阶段(prepare phase),在这个阶段,将所有提交操作所需要使用到的信息和资源全部写入磁盘,完成持久化;

  • 阶段二,提交阶段(commit prepared phase),根据之前准备好的提交信息和资源,执行提交或回滚操作。 两阶段提交协议之所以能够保证分布式事务原子性的关键在于:一旦准备阶段执行成功,那么提交需要的所有信息都完成持久化下盘,即使后续提交阶段某个DN发生执行错误,该DN可以再次从持久化的提交信息中尝试提交,直至提交成功。最终该分布式事务在所有DN上的状态一定是相同的,要么所有DN都提交,要么所有DN都回滚。因此,对外来说,该事务的状态变化是原子的。

下表总结了在openGauss分布式事务中的不同阶段,如果发生故障或执行失败,分布式事务的最终提交/回滚状态,读者可自行推演,本文不再赘述。

发生故障或执行失败时事务的最终状态

屏幕快照 2021-11-25 下午3.43.05.png

2 分布式事务一致性和全局事务管理

为了防止瞬时不一致现象,支持分布式事务的强一致性,我们需要全局范围内的事务号和快照,以保证全局MVCC和快照的一致性。在openGauss中,GTM负责提供和分发全局的事务号和快照。对于任何一个读事务,其都需要到GTM上获取全局快照;对于任何一个写事务,其都需要到GTM上获取全局事务号。

为了防止瞬时不一致现象,支持分布式事务的强一致性,我们需要全局范围内的事务号和快照,以保证全局MVCC和快照的一致性。在openGauss中,GTM负责提供和分发全局的事务号和快照。对于任何一个读事务,其都需要到GTM上获取全局快照;对于任何一个写事务,其都需要到GTM上获取全局事务号。

分布式事务一致性问题示意图.png

分布式事务一致性问题示意图

在上图中加入GTM,并考虑两阶段提交流程之后,分布式读-写并发事务的流程如下图所示。对于读事务来说,由于写事务在其从GTM获取的快照中,因此即使写事务在不同DN上的提交顺序和读事务的执行顺序不同,也不会造成不一致的可见性判断和不一致的读取结果。

读-写并发下全局事务号和快照的分发流程示意图.png

读-写并发下全局事务号和快照的分发流程示意图

细心的读者会发现,在上图的两阶段提交流程中,写事务T1在各个DN上完成准备阶段之后,首先第一步是到GTM上结束T1事务(将T1从全局快照中移除),然后第二步再到各个DN上进行提交阶段。在这种情况下,如果查询事务T2是在第一步和第二步之间在GTM上获取快照,并到各个DN上执行查询的话,那么T2事务读到的T1事务插入的记录v1和v2,它们xmin对应的XID1已经不在T2事务获取到的全局快照中,因此v1和v2的可见性判断会完全基于T1事务的提交状态。然而,此时XID1对应的T1事务在各个DN上可能还没有全部或部分完成提交阶段,那么就会出现各个DN上可见性不一致的情况。

为了防止上面这种问题出现,在openGauss中采用本地二阶段事务补偿机制。如下图所示,对于在DN上读取到的记录,如果其xmin或者xmax已经不在快照中,但是它们对应的写事务还在准备阶段,那么查询事务将会等到这些写事务在DN本地完成提交阶段之后,再进行可见性判断。考虑到通过两阶段提交协议,可以保证各个DN上事务最终的提交或回滚状态一定是一致的,因此在这种情况下各个DN上记录的可见性判断也一定是一致的。

读-写并发下本地两阶段事务补偿流程示意图.png

读-写并发下本地两阶段事务补偿流程示意图

3 小结

本文主要结合openGauss数据库的事务机制和实现原理,基于显式事务和隐式事务,介绍事务块状态机的变化,以及openGauss事务ACID特性的实现方式。尤其的,对于分布式场景下的事务原子性和一致性问题,介绍openGauss采取的多种解决技术方案,以保证数据库最终对外呈现的ACID不受分布式执行框架的影响。

Posted by enmotech at 2:52 PM | Permalink |

November 24, 2021

openGauss 数据库并发控制

文章来源于墨天轮:https://www.modb.pro/db/174555

当数据库中存在并发执行事务的情况下,要保证ACID特性,需要一些特殊的机制来支持。并发控制就是指这样的一种控制机制,能够保证并发事务同时访问同一个对象或数据下的ACID特性。

openGauss并发控制是十分高效的,其核心是MVCC和快照机制。如前文中所述,通过使用MVCC和快照,可以有效解决读写冲突,使得并发的读事务和写事务工作在同一条元组的不同版本上,彼此不会相互阻塞。对于并发的两个写事务,openGauss通过事务级别的锁机制(事务执行过程中持锁,事务提交时释放),来保证写事务的一致性和隔离性。

另一方面,对于底层数据的访问和修改,如物理页面和元组,为了保证读写操作的原子性,需要在每次的读、写操作期间加上共享锁或排他锁。当每次读、写操作完成之后,即可释放上述锁资源,无需等待事务提交,持锁窗口相对较短。

 

1 读-读并发控制

在绝大多数情况下,并发的读-读事务,是不会、也没有必要相互阻塞的。由于没有修改数据库,因此每个读事务使用自己的快照,就能保证查询结果的一致性和隔离性;同时,对于底层的页面和元组,只涉及读操作,只需要对它们加共享锁即可,不会发生锁等待的情况。

一个比较特殊的情况是执行SELECT FOR UPDATE查询。该查询会对所查到的每条记录在元组层面加排他锁,以防止在查询完成之后,查询结果集被后续其它写事务修改。该语句获取到的元组排他锁,在事务提交时才会释放。对于并发的SELECT FOR UPDATE事务,如果它们的查询结果集有交集,那么在交集中的元组上会发生锁冲突和锁等待。

 

2 读-写并发控制

如下图的例子所示,openGauss中对于读、写事务的并发控制基于MVCC和快照机制,彼此之间不会存在事务级的长时间阻塞。相比之下,采用两阶段锁协议(Two-Phase Locking Protocol,简称2PL协议)的并发控制(如IBM DB2数据库),由于读、写均在记录的同一个版本上操作,因此排在锁等待队列后面的事务至少要阻塞到持锁者事务提交之后才能继续执行。

读已提交和可重复读隔离级别在并发事务下的表现区别.png

读已提交和可重复读隔离级别在并发事务下的表现区别

另一方面,为了保证底层物理页面和元组的读、写原子性,在实际操作页面和元组时,需要暂时加上相应对象的共享锁或排他锁,在完成对象的读、写操作之后,就可以放锁。

对于所有可能的三种读-写并发场景,即查询-插入并发、查询-删除并发和查询-更新并发,在下面图1、图2和图3中分别给出了它们的并发控制示意图。

图1 查询-插入并发控制示意图.png

图1 查询-插入并发控制示意图

图2 查询-删除并发控制示意图.png

图2 查询-删除并发控制示意图

图3 查询-更新并发控制示意图.png

图3 查询-更新并发控制示意图

 

3 写-写并发控制

虽然通过MVCC,可以让并发的读-写事务工作在同一条记录的不同版本上(读老版本,写新版本),从而互不阻塞,但是对于并发的写-写事务,它们都必须工作在最新版本的元组上,因此如果并发的写-写事务涉及同一条记录的写操作,那么必然导致事务级的阻塞。

写-写并发的场景有以下6种:插入-插入并发、插入-删除并发、插入-更新并发、删除-删除并发、删除-更新并发、更新-更新并发。下面就插入-插入并发、删除-删除并发和更新-更新并发的控制流程做简要描述,另外三种并发场景下的控制流程供读者自行思考。

图4为插入-插入事务的并发控制流程图。对于每个插入事务,它们都会在表的物理页面中插入一条新元组,因此并不会在同一条元组上发生并发写冲突。然而,当表具有唯一索引时,为了避免违反唯一性约束,若并发插入-插入事务在唯一键上有冲突(即键值重复),后来的插入事务必须等待先来的插入事务提交以后,再根据先来插入事务的提交结果,才能进一步判断是否能够继续执行插入操作。如果先来插入事务提交了,那么后来插入事务必须回滚,以防止唯一键重复;如果先来插入事务回滚了,那么后来插入事务可以继续插入该键值的记录。

图4 插入-插入并发控制示意图.png

图4 插入-插入并发控制示意

图5为删除-删除事务的并发控制流程图。对于并发的删除-删除事务,它们都会尝试去修改同一条元组的xmax值。我们通过页面排他锁来控制该冲突。对于后加上锁的删除事务,它在再次标记元组xmax值之前,首先需要判断先来删除事务(即元组当前xmax事务号对应的事务)的提交结果。如果先来删除事务提交了,那么该元组对后来删除事务不可见,后来删除事务无元组需要删除;如果先来删除事务回滚了,那么该元组对后来删除事务依然可见,后来删除事务可以继续执行对该元组的删除操作。

图5 删除-删除并发控制示意图.png

图5 删除-删除并发控制示意图

图6为更新-更新事务的并发控制流程图。对于并发的更新-更新事务,与并发删除-删除事务类似,它们首先都会尝试去修改同一条元组的xmax值。我们通过页面排他锁来控制该冲突。对于后加上锁的更新事务,它在再次标记元组xmax值之前,首先需要判断先来更新事务(即元组当前xmax事务号对应的事务)的提交结果。如果先来更新事务提交了,那么该元组对后来更新事务不可见,此时,后来更新事务会去判断该元组更新后的值(先来更新事务插入)是否还符合后来更新事务的谓词条件(即删除范围),如果符合,那么后来的更新事务会在这条新的元组上进行更新操作,如果不符合,那么后来的更新事务无元组需要更新;如果先来更新事务回滚了,那么该元组对后来更新事务依然可见,后来更新事务可以继续在该元组上进行更新操作。

图6 更新-更新并发控制示意图.png

图6 更新-更新并发控制示意图

 

4 并发控制和隔离级别

在上文中介绍写-写并发控制的机制时,其实默认了使用读已提交的隔离级别。回顾图4、图5和图6,我们可以发现,当在某条元组上发生并发写-写冲突时,原本先来事务是在后来事务的快照中的,后来事务是不应该看到先来事务的提交结果的,但是为了解决上述冲突,后来事务会等待先来事务提交之后,再去校验先来事务对元组的操作结果。这种方式是符合读已提交隔离级别要求的,但是显然后来事务在等待之后,又刷新了自己的快照内容(将先来事务从快照中移除)。

基于上述原因,在MVCC和快照隔离的并发控制策略下,若使用可重复读的隔离级别,当发生上述写-写冲突时,后来事务不会再等待先来事务的提交结果,而是将直接报错回滚。这也是openGauss在可重复读隔离级别下,对于写-写冲突的处理模式。

进一步,如果要支持可串行化的隔离级别,对于使用MVCC和快照隔离的并发控制策略,需要解决写偏序(Write Skew)的异常现象,有兴趣的读者可以参考2008年SIGMOD最佳论文《Serializable Isolation for Snapshot Databases》。

 

5 对象属性的并发控制

在上面并发控制的介绍中,我们覆盖了DML和查询事务的并发控制机制。对于DDL语句,其虽然不涉及表数据元组的修改,但是其会修改表的结构(Schema),因此很多场景下不能和DML、查询并发执行。

图7 DDL-DML并发控制示意图.png

图7 DDL-DML并发控制示意图

以增加字段的DDL事务和插入事务并发执行为例,它们的并发执行流程如图7所示。首先,DDL事务会获取表级的排他锁,而DML事务在执行之前,需要获取表级的共享锁。DDL事务持锁之后,会执行新增字段操作。然后,DDL事务会给其它所有并发事务发送表结构失效消息,告诉其它并发事务,这个表的结构被修改了。最后,DDL事务释放表级排他锁,提交返回。

DDL事务放锁之后,DML事务可以获取到该表的共享锁。加锁之后,DML事务首先需要处理所有在等锁过程中可能收到的表结构失效消息,并加载新的表结构信息。然后,DML才可以执行增删改操作,并提交返回。

 

6 表级锁、轻量锁和死锁检测

在前面,已经向读者初步介绍了在事务并发控制中,需要有锁机制的参与。事实上,在openGauss中,主要有两种类型的锁:表级锁和轻量锁。

表级锁主要用于提供各种类型语句对于表的上层访问控制。根据访问控制的排他性级别,表级锁分为1级到8级锁。对于两个表级锁(同一张表)的持有者,如果他们持有的表级锁的级别之和大于等于8级,那么这两个持有者的表级锁会相互阻塞。

在典型的数据库操作中,查询语句需要获取1级锁,DML语句需要获取3级锁,因此这两个操作在表级层面不会相互阻塞(这得益于MVCC和快照机制)。相比之下,DDL语句通常需要获取8级锁,因此对同一张表的DDL操作会和查询语句、DML语句相互阻塞。以修改表结构类型的DDL语句为代表,如果允许在该DDL执行过程中同时插入多条数据,那么前后插入的数据的字段个数可能不一致,甚至相同字段的类型亦可能出现不一致。

另一方面,在创建一个表的索引过程中,一般不允许有并发的DML操作,否则可能会导致索引不正确,或者需要引入复杂的并发索引修正机制。在openGauss中,创建索引语句需要对目标表获取5级锁,该锁级别和DML的3级锁会相互阻塞。

在openGauss中,为表级锁的所有等待者维护了等待队列信息。基于该等待队列,openGauss对于表级锁提供了死锁检测。死锁检测的基本原理是尝试在所有表级锁的等待队列中寻找是否存在能够构成环形等待队列的情况,如果存在环形等待队列,那么就表示可能发生了死锁,需要让其中某个等待者回滚事务退出队列,从而打破该环形等待队列。

在openGauss中,第二种广泛使用的锁是轻量锁。轻量锁只有共享和排他两种级别,并且没有等待队列和死锁检测。一般轻量锁并不对数据库用户提供,仅供数据库开发人员使用,需要开发人员自己来保证并发情况下不会发生死锁的场景。在本章中曾经介绍过的页面锁即是一种轻量锁,表级锁也是基于轻量锁来实现的。

墨天轮,围绕数据人的学习成长提供一站式的全面服务,打造集新闻资讯、在线问答、活动直播、在线课程、文档阅览、资源下载、知识分享及在线运维为一体的统一平台,持续促进数据领域的知识传播和技术创新。

Posted by enmotech at 4:17 PM | Permalink |

November 22, 2021

openGauss 数据库事务概览

本文来源于墨天轮:https://www.modb.pro/db/171689

事务是数据库为用户提供的最核心、最具吸引力的功能之一。简单地说,事务是用户定义的一系列数据库操作(如查询、插入、修改或删除等)的集合,数据库从内部保证了该操作集合(作为一个整体)的原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),统称事务的ACID特性。其中:

  • A:原子性是指一个事务中的所有操作要么全部执行成功,要么全部执行失败。一个事务执行以后,数据库只可能处于上述两种状态之一。即使数据库在这些操作执行过程中发生故障,数据库也不会出现只有部分操作执行成功的状态。

  • C:一致性是指一个事务的执行会导致数据从一个一致的状态转移到另一个一致的状态,事务的执行不会违反一致性约束、触发器等定义的规则。

  • I:隔离性是指在一个事务的执行过程中,所看到的数据库状态受并发事务的影响程度。根据该影响程度的轻重,一般将事务的隔离级别分为读未提交、读已提交、可重复读和可串行化四个级别(受并发事务影响由重到轻)。

  • D:持久性是指一旦一个事务的提交以后,那么即使数据库发生故障重启,该事务的执行结果不会丢失,仍然对后续事务可见。

本文主要结合openGauss数据库的事务机制和实现原理,来阐述在openGauss是如何保证事务的ACID特性的。

openGauss是一个分布式的数据库。同样的,openGauss数据库的事务机制也是一个从单机到分布式的双层构架。

openGauss集群事务组件构成示意图.png

openGauss集群事务组件构成示意图
.

如图所示,在openGauss集群中,事务的执行和管理主要涉及GTM、CN和DN三种组件,其中:

  • GTM,全称Global Transaction Manager,即全局事务管理器,负责全局事务号的分发,事务提交时间戳的分发以及全局事务运行状态的登记。对于采用多版本并发控制(Multi-Version Concurrency Control,简称MVCC)的事务模型(以openGauss和Oracle为例),GTM本质上可以简化为一个递增序列号(或时间戳)生成器,其为集群的所有事务进行了全局的统一排序,以确定快照(Snapshot)内容和由此决定的事务可见性。在本章10.3节openGauss数据库并发控制中,将进一步详述GTM的作用。

  • CN,全称Coordinator Node,即协调者实例,负责管理和推进一个具体事务的执行流程,维护和推进事务执行的事务块状态机。

  • DN,全称Data Node,即数据实例,负责一个具体事务在某一个数据分片内的所有读写操作。  

1. 显式事务和隐式事务

显式事务是指,用户在所执行的一条或多条SQL语句的前后,显式添加了开启事务START TRANSACTION语句和提交事务COMMIT语句。

隐式事务是指,用户在所执行的一条或多条SQL语句的前后,没有显式添加开启事务和提交事务的语句。在这种情况下,每一条SQL语句在开始执行时,openGauss内部都会为其开启一个事务,并且在该语句执行完成之后,自动提交该事务。

以一条SELECT语句和一条INSERT语句为例,简要描述显式事务和隐式事务在openGauss集群中的主要执行流程。

显式事务的SQL语句如下(假设表t只包含一个整数类型字段a,且为分布列):

START TRANSACTION; SELECT * FROM t; INSERT INTO t(a) VALUES (100); COMMIT;

1)START TRANSACTION

该SQL语句只在CN上执行,CN显式开启一个事务,并将CN本地事务块状态机从空闲状态置为进行中状态,然后返回客户端,等待下一条SQL命令。

2)SELECT * FROM t

该SQL语句首先在CN上执行,由于openGauss分片采用一致性哈希算法,因此对于不带分布列上谓词条件的查询语句,CN需要将该SQL语句发送到所有DN实例上执行。对于每一个分片对应的DN实例,由于采用了显式事务,CN会先发送一条START TRANSACTION命令给该DN,让该DN显式开启事务(DN上的事务块状态机从空闲状态变为进行中状态),然后CN将SELECT语句发送给该DN。此后,CN在收到所有DN的查询结果之后,返回客户端,等待下一条SQL命令。

3)INSERT INTO t(a) VALUES (100)

该SQL语句首先在CN上执行,由于a为表t的分布列,因此CN可以根据被插入记录中a的具体取值,来决定应该由哪个数据分片对应的DN实例来执行实际的插入操作(这里假设该分片为DN1)。由于采用了显式事务,CN先发送一条START TRANSACTION命令给DN1,由于经过第(2)步DN1的事务块状态机已经处于进行中状态,因此对于该语句DN1并不会执行什么实际的操作,然后,CN将具体的INSERT语句发送给DN1,并等待DN1执行插入成功之后,返回客户端,等待下一条SQL命令。

4)COMMIT

该SQL语句首先在CN上执行,CN进入提交事务阶段后,将COMMIT语句发送给所有参与第(2)步和第(3)步的DN,让这些DN结束该事务,并将DN本地的事务块状态机从进行中状态置为空闲状态。CN在收到所有DN的事务提交结果之后,再将CN本地的事务块状态机从进行中状态置为空闲状态。然后,CN返回客户端,该事务执行完成。

上述操作的隐式事务语句如下(假设表t只包含一个整数类型字段a,且为分布列):

SELECT * FROM t; INSERT INTO t(a) VALUES (1); 1)SELECT * FROM t

该SQL语句首先在CN上执行,CN隐式开启一个事务,将CN本地的事务块状态机从空闲状态置为开启状态(注意不同于显式事务中的进行中状态)。然后,CN需要将该语句发送到所有DN实例上执行。对于每一个分片对应的DN实例,由于采用了隐式事务且该语句为只读查询,CN直接将SELECT语句发送给该DN。

DN收到该SELECT语句之后,亦采用隐式事务:第一步,隐式开启事务,将DN本地的事务块状态机从空闲状态置为开启状态;第二步,执行该查询语句,将查询结果返回给CN;第三步,隐式提交事务,将DN本地的事务块状态机从开启状态置为空闲状态。

CN在收到所有DN的查询结果之后,返回客户端,并隐式提交事务,将CN本地的事务块状态机从开启状态置为空闲状态。

2)INSERT INTO t(a) VALUES (1)

该SQL语句首先在CN上执行,CN隐式开启一个事务,将CN本地的事务块状态机从空闲状态置为开启状态。然后,CN需要将该INSERT语句发送到目的分片的DN实例上执行(这里假设该分片为DN1)。

虽然该语句采用了隐式事务,但是由于该语句为写操作,因此在DN1上会采取显式事务:CN会先发送一条START TRANSACTION命令给DN1,让DN1显式开启事务(DN1上的事务块状态机从空闲状态变为进行中状态),然后CN将INSERT语句发送给DN1,DN1执行完成后,返回执行结果给CN。

CN收到执行结果之后,进入提交事务阶段。先发送COMMIT语句到DN1。DN1收到COMMIT语句后,进行显式提交,将DN1本地的事务块状态机从进行中状态置为空闲状态。CN在收到DN1的事务提交结果之后,本地再进行隐式提交事务,将CN本地的事务块状态机从开启状态置为空闲状态,返回客户端,该事务执行完成。

综上,对于CN来说,使用显式事务还是隐式事务,完全取决于用户输入的SQL语句;对于DN来说,只有当SQL为隐式只读事务时,才会使用隐式事务,当SQL为显式事务或者隐式写事务时,都会使用显式事务。  

2. 单机事务和分布式事务

在openGauss这样的分布式集群中,单机事务(亦称单分片事务)是指一个事务中所有的操作都发生在同一个分片(即DN实例)上,分布式事务是指一个事务中有两个或以上的分片参与了该事务的执行。

对于单机事务,其写操作的原子性和读操作的一致性由该DN自身的事务机制就能保证;对于分布式事务,不同分片之间写操作的原子性和不同分片之间读操作的一致性,需要额外的机制来保障。下面结合SQL语句简要介绍下分布式事务的原子性和一致性要求,具体的原理机制在10.4节中说明。

首先,考虑涉及多分片的写操作事务,以如下事务T1为例(假设表t只包含一个整数类型字段a,且为分布列):

START TRANSACTION; INSERT INTO t(a) VALUES (v1); INSERT INTO t(a) VALUES (v2); COMMIT; 上面事务T1的两条INSERT语句均为只涉及一个分片的写(插入)事务,如果v1和v2分布在同一个分片内,那么该事务为单机事务,如果v1和v2分布在两个不同的分片内,那么该事务为分布式事务。

对于只涉及一个DN分片的单机事务,其对于数据库的修改和影响全部发生在同一个分片内,因此该分片的事务提交结果即是该事务在整个集群的提交结果,该分片事务提交的原子性就能够保证整个事务的原子性。在事务T1示例中,如果v1和v2全分布在DN1上,那么在DN1上,如果事务提交,那么这两条记录就全部插入成功;如果DN1上事务回滚,那么这两条记录的插入就全部失败。

对于分布式事务,为了保证事务在整个集群范围内的原子性,必须保证所有参与写操作的分片要么全部提交,那么全部回滚,不能出现部分分片提交,部分分片回滚的"中间态"。如下图所示,如果v1插入到DN1上,且DN1提交成功,同时,v2插入到DN2上,且DN2最终回滚,那么最终该事务只有一部分操作成功,破坏了事务的原子性要求。为了避免这种情况的发生,openGauss采用两阶段提交(Two Phase Commit,简称2PC)协议,来保证分布式事务的原子性,在10.4.1节中会对两阶段提交相关内容进行更详细的介绍。

分布式事务原子性问题示意图.png

分布式事务原子性问题示意图

其次,考虑涉及多分片的读操作事务T2,以如下SQL语句为例(假设表t只包含一个整数类型字段a,且为分布列):

START TRANSACTION; SELECT * FROM t where a = v1 or a = v2; COMMIT; 上面查询事务T2中,如果v1和v2分布在同一个分片内,那么该事务为单机事务,如果v1和v2分布在两个不同的分片内,那么该事务为分布式事务。

对于单机事务,其查询的数据完全来自于同一个分片内,因此该分片事务的可见性和一致性就能够保证整个事务的一致性。

在事务T1和T2示例中,考虑T1和T2并发执行的场景(假设T1提交成功),如果v1和v2全分布在DN1上,那么,在DN1上,如果T1对T2可见,那么T2就能查询到所有的两条记录,如果T1对T2不可见,那么T2不会查询到两条记录中的任何一条。

对于分布式事务,其查询的数据来自不同的分片,单个分片的可见性和一致性无法完全保证整个事务的一致性,不同分片之间事务提交的先后顺序和可见性判断会导致查询结果存在某种"不确定性"。

仍考虑T1和T2并发执行的场景(假设T1提交成功)。如下图所示,如果v1和v2分别分布在DN1和DN2上,若在DN1上,T1事务提交先于T2的查询执行,且对于T2可见,而在DN2上,T2的查询执行先于T1事务提交(或T1事务提交先于T2查询执行,但对T2不可见),那么T2最终只会查询到v1这一条记录。对于以银行为代表的传统数据库用户来说,这种现象破坏了事务作为一个整体的一致性要求。在分布式事务中,亦称为强一致性要求。

 

分布式事务一致性问题示意图.png

 

分布式事务一致性问题示意图
 

另一方面,如果T1先完成提交,并等待足够长的时间以后(保证所有分片均完成T1的提交,并保证提交结果对T2可见),再执行T2,那么T2将可以看到T1插入的所有两条记录。在分布式事务中,这种一致性表现被称为最终一致性。与传统数据库用户不同,在互联网等新兴业务中,最终一致性是被广泛接受的。

openGauss通过全局一致性的时间戳(快照)技术和本地两阶段事务补偿技术,提供分布式强一致事务的能力,同时,对于追求性能的新兴数据库业务,也支持可选的最终一致性事务的能力。

Posted by enmotech at 10:25 AM | Permalink |

November 19, 2021

openGauss 数据库内存引擎

本文来源于墨天轮:https://www.modb.pro/db/170018

内存引擎作为在openGauss中与传统基于磁盘的行存储、列存储并存的一种高性能存储引擎,基于全内存态数据存储,为openGauss提供了高吞吐的实时数据处理分析能力以及极低的事务处理时延,在不同业务负载场景下可以达到其他引擎事务处理能力的3~10倍不等。

内存引擎之所以有较强的事务处理能力,并不单是因为基于内存而非磁盘带来的性能提升,而更多是因为其全面地利用了内存中可以实现的无锁化的数据及索引结构、高效的数据管控、基于NUMA架构的内存管控、优化的数据处理算法以及事务管理机制。

值得一提的是,虽然是全内存态存储,但是并不代表着内存引擎中的处理数据会因为系统故障而丢失;相反的,内存引擎有着与openGauss原有机制相兼容的并行持久化、checkpoint(检查点)能力,使得内存引擎有着与其他存储引擎相同的容灾能力以及主备副本带来的高可靠能力。

内存引擎总的架构如图所示。

内存引擎总体架构图.png

内存引擎总体架构图

可以看到,内存引擎通过原有的Foreign Data Wrapper(外部数据封装器)扩展能力与openGauss的优化执行流程相交互,通过事务机制的回调以及与openGauss相兼容的WAL机制,保证了与其他存储引擎在这一体系架构内的共存,保证了整体对外的一致表现;同时通过维护内部的内存管理结构、无锁化索引、乐观事务机制来为系统提供极致的事务吞吐能力。

以下将逐步展开讲解相关关键技术点与设计。

 

1. 内存引擎的兼容性设计

由于数据形态的不同以及底层事务机制的差别,此处如何与一个以段页式为基础的系统对接是内存引擎存在于openGauss中的重点问题之一。

此处openGauss原有的FDW(Foreign Data Wrapper)机制为内存引擎提供了一个很好的对接接口,优化器可以通过FDW来获取内存引擎内部的元信息,内存引擎的内存计算处理机制可以直接通过FDW的执行器接口算子实现直接调起、并通过相同的结构将结果以符合执行器预期的方式(比如Scan(扫描)操作的pipelining(流水线))将结果反馈回执行器进行进一步处理后(如排序、Group by(分组))返回给客户端应用。

与此同时内存引擎自身的Error Handling(错误处理机制),也可以通过与FDW的交互,提交给上次系统,以此同步触发上层逻辑的相应错误处理(如回滚事务、线程退出等)。

内存引擎借助FDW的方式接近无缝的工作在整个系统架构下,与以磁盘为基础的行列存储引擎实现共存。

在内存引擎中Create Table(创建表)的实际操作流程如图所示。

内存引擎操作流程图

可以看到FDW充当了一个整体交互API的作用。实现中同时扩展了FDW的机制,使得其具有更完备的交互功能,包括:

(1) 支持DDL接口;

(2) 完整的事务生命周期对接;

(3) 支持checkpoint(检查点);

(4) 支持持久化WAL;

(5) 支持故障恢复(Redo);

(6) 支持Vacuum(垃圾清理回收)。

借由FDW机制,内存引擎可以作为一个与原有openGauss代码框架异构的存储引擎存在于整个体系中。

  2. 内存引擎索引

内存引擎的索引结构以及整体的数据组织都是基于Masstree实现的。主体如图所示。

内存引擎主体结构.png

内存引擎主体结构

Primary Index(主键索引)在内存引擎的一个表中是必须存在的要素,因此要求表在组织时尽量存在primary index;如果不存在,内存引擎也会额外生成surrogate key(代理键)来用于生成Primary index。Primary Index指向各个代表各个行记录的Sentinel(行指针),由Sentinel来对行记录数据进行内存地址的记录以及引用。Secondary Index(二级索引)索引后指向一对键值,键值的value(值)部分为到对应数据Sentinel的指针。

Masstree作为Concurrent B+ tree(并行B+树),集成了大量B+树的优化策略,并在此基础上做了进一步的改良和优化。其大致实现如图所示。

Masstree实现方式

Masstree实现比于传统的B-tree,Masstree实际上是一个类似于诸多B+-树以trie(前缀树)的组织形式堆叠的Radix tree(基数树)模式,以Key(键)的前缀作为索引,每k个字节形成一层B+-树结构,在每层中处理Key(键)中这k个字节对应所需的insert/lookup/update/delete流程。下图为k=8时情况。

k等于8时的Masstree.png

k等于8时的Masstree

注:图来自于"Cache craftiness for fast multicore key-value storage" , Eddie Kohler et. al.

Masstree中的读操作使用了类OCC(Optimistic Concurrency Control,乐观并发控制)的实现,而所有的update(更新)锁仅为本地锁。在树的结构上,每层的interior node(内部节点)和leaf node(叶子节点)都会带有版本,因此可以借助version validation(版本检查)来避免fine-grained lock(细粒度锁)的使用。

Masstree除了lockless(无锁化)之外,最大的亮点是cache line(缓存块)的高效利用。Lockless本身一定程度避免了lookup/insert/update操作互相invalidate共享cache line(失效共享缓存块)的情况。而基于prefix(前缀)的分层,辅以合适的每层中B+-树fanout(扇出)的设置,可以最大程度的利用CPU prefetch(预取)的结果(尤其是在树的深度遍历过程中),减少了与DRAM交互带来的额外时延。

Prefetch(预取)在Masstree的设计中显得尤为关键,尤其是在Masstree从tree root(树根节点)向leaf node(叶子节点)遍历、也就是树的下降过程中。此过程中的执行时延大部分由于内存交互的时延组成,因此prefetch(预取)可以有效地提高masstree traverse(遍历)操作的执行效率以及cache line(缓存块)的使用效率(命中)。

 

3. 内存引擎的并发控制

内存引擎的并发控制机制采用OCC,在操作数据冲突少的场景下,并发性能很好。

内存引擎的事务周期以及并发管控组件结构,如图所示。

内存引擎的事务周期以及并发管控组件结构.png

内存引擎的事务周期以及并发管控组件结构

这里需要解释一下,内存引擎的数据组织为什么整体是一个接近无锁化的设计。

除去以上提到的Masstree本身的无锁化机制外,内存引擎的流程机制也进一步最小化了并发冲突的存在。

每个工作线程会将事务处理过程中所有需要读取的记录,复制一份至本地内存,保存在read-set(读数据集)中,并在事务全程基于这些本地数据进行相应计算。相应的运算结果保存在工作线程本地的write set(写数据集)中。直至事务运行完毕,工作线程会进入尝试提交流程,对read set(读数据集)和write set进行validate(检查验证)操作并在允许的情况下对write set中数据对应的全局版本进行更新。

这样的流程,会把事务流程中对于全局版本的影响,缩小到validation的过程,而在事务进行其他任何操作的过程中都不会影响到其他的并发事务。并且,在仅有的validation(检查验证)过程中,所需要的也并不是传统意义上的锁,而仅是记录头部信息中的代表锁的数位(lock bit)。相应的这些考虑,都是为了最小化并发中可能出现的资源争抢以及冲突,并更有效地使用CPU缓存。

同时read set(读数据集)和write set(写数据集)的存在,可以良好地支持各个隔离级别,不同隔离级别可以通过在validation(检查验证)阶段对read set(读数据集)和write set(写数据集)进行不同的审查机制来获得。通过检查两个set(数据集)中行记录在全局版本中对应的lock bit(锁定位)以及行头中的TID结构,可以判断自己的读、写与其他事务的冲突情况,进而判断自己在不同隔离级别下是否可以commit(提交)、或是需要abort(终止)。同时由于Masstree中Trie node中存在版本记录,Masstree的结构性改动(insert/delete,插入/删除)会更改相关Trie node(节点)上面的版本号。因此维护一个Range query(范围查询)涉及的node set(节点集),并在validation(检查验证)阶段对其进行对比校验,可以比较容易地在事务提交阶段检查此Range query所涉及的子集是否有过变化,从而能够检测到Phantom(幻读)的存在,是一个时间复杂度很低的操作。

 

4. 内存引擎的内存管控

由于内存引擎的数据是全内存态的,因此可以按照记录来组织数据,不需要遵从页面的数据组织形式,从而从数据操作的冲突粒度这一点上有着很大优势。摆脱了段页式的限制,不再需要共享缓存区进行缓存以及与磁盘间的交互淘汰,设计上不需要考虑IO以及磁盘性能的优化(比如索引B+ 树的高度以及HDD(Hard Disk Driv,磁盘)对应的随机读写问题),数据读取和运算就可以进行大量的优化和并发改良。

由于是全内存的数据形态,内存资源的管控就显得尤为重要,内存分配机制及实现会很大程度上影响到内存引擎的计算吞吐能力。内存引擎的内存管理主要分为3层,如图所示。

内存引擎的内存管理示意图.png

内存引擎的内存管理示意图

下面分别对3层设计进行介绍:

(1) 第一层为内存引擎自身,包含了临时的内存使用以及长期的内存使用(数据存储)。

(2) 第二层为对象的内存池,主要负责为第一层对象如表、索引、行记录、Key值、以及Sentinel(行指针)提供内存。该层从底层索取大块内存,再进行细粒度的分配。

(3) 第三层为资源管理层,主要负责与操作系统之间的交互,以及实际的内存申请。为降低内存申请的调用开销,交互单位一般在2 MB左右。此层同时也有内存预取和预占用的功能。

第三层实际上是非常重要的,主要因为:

(1) 内存预取可以非常有效的降低内存分配开销,提高吞吐量。

(2) 与NUMA库进行交互的性能成本非常高,如果直接放在交互层会对性能产生很大影响。

内存引擎对短期与长期的内存使用针对NUMA结构适配的角度也是不同的。短期使用,一般为事务或session(会话)本身,那么此时一般需要在处理该session的CPU核对应的NUMA节点上获取本地内存,使得transaction(交易)本身的内存使用有着较小的开销;而长期的内存使用,如表、索引、记录的存储,则需要NUMA概念中interleaved内存,并且要尽量平均分配在各个NUMA节点上,来防止单个NUMA节点内存消耗过多带来的性能下降。

短期的内存使用,也就是NUMA角度的本地内存,也有一个很重要的特性,就是这部分内存仅供本事务自身使用(比如复制的读取数据以及做出的更新数据),因此也就避免了这部分内存上的并发管控。

 

5. 内存引擎的持久化

内存引擎基于同步的WAL机制以及checkpoint(检查点)来保证数据的持久化,并且此处通过兼容openGauss的WAL机制(即Transaction log,事务日志),在数据持久化的同时,也可以保证数据能够在主备节点之间进行同步,从而提供RPO=0的高可靠以及较小RTO的高可用能力。

内存引擎的持久化机制如图所示。

内存引擎的持久化机制.png

内存引擎的持久化机制

可以看到,openGauss的Xlog模块被内存引擎对应的manager(管理器)所调用,持久化日志通过WAL的writer线程(刷新磁盘线程)写至磁盘,同时被walsender(事务日志发送线程)调起发往备机,并在备机walreceiver(事务日志接收线程)处接收、落盘与恢复。

内存引擎的Checkpoint也是根据openGauss自身的checkpointer机制被调起。

openGauss中的checkpoint机制是通过在做checkpoint时进行shared_buffer(共享缓冲区)中脏页的刷盘,以及一条特殊checkpoint日志来实现的。内存引擎由于是全内存存储,没有脏页的概念,因此实现了基于CALC的Checkpoint机制。

这里主要涉及一个部分多版本(partial multi-versioning)的概念:当一个checkpoint指令被下发,使用两个版本来追踪一个记录:活跃(live)版本,也就是该记录的最新版本;稳定(stable)版本,也就是在checkpoint被下发、形成虚拟一致性点时此记录对应的版本。在一致性点之前提交的事务需要更新活跃(live)和稳定(stable)两个版本,而在一致性点之后的事务仅更新活跃(live)版本本保持stable版本不变。在无checkpoint状态的时候,实际上稳定(stable)版本是空的,代表着stable与live版本在此时实际是相同的值;仅有在checkpoint过程中,在一致性点后有事务对记录进行更新,此时才会需要根据双版本来保证checkpoint与其他正常事务流程的并行运作。

CALC(Checkpointing Asynchronously using Logical Consistency,逻辑一致性异步检查点)的实现有5个阶段:

(1) rest(休息)阶段:这个阶段内,没有checkpoint(检查点)的流程,每个记录仅存储live版本。

(2) prepare(准备)阶段:整个系统触发checkpoint后,会马上进入这个阶段。在这个阶段中事务对读写的更改,也会更新live版本;但是在更新前,如果stable版本不存在,那么在更新live版本前,live版本的数据会被存入stable版本。在此事务的更新结束,在放锁前,会进行检查:如果此时系统仍然处于prepare阶段,那么刚刚生成的stable版本可以被移除;反之,如果整个系统已经脱离prepare阶段进入下一阶段,那么stable版本就会被保留下来。

(3) resolve(解析)阶段:在进入prepare阶段前发生的所有事务都已提交或回滚后,系统就会进入resolve阶段,进入这个阶段也就代表着一个虚拟一致性点已经产生,在此阶段前提交的事务相关的改动都会被反映到此次checkpoint中。

(4) capture(捕获)阶段:在prepare阶段所有事务都结束后,系统就会进入capture阶段。此时后台线程会开始将checkpoint对应的版本(如果没有stable版本的记录即则为live版本)写入磁盘,并删除stable版本。

(5) complete(完成)阶段:在checkpoint写入过程结束后,并且capture阶段中进行的所有事务都结束后,系统进入complete阶段,系统事务的写操作的表现会恢复和rest阶段相同的默认状态。

CALC有着以下优点:

(1) 低内存消耗:每个记录至多在checkpoint时形成两份数据。在checkpoint进行中如果该记录stable版本和live版本相同,或在没有checkpoint的情况下,内存中只会有数据自身的物理存储。

(2) 较低的实现代价:相对其他内存库checkpoint机制,对整个系统的影响较小。

(3) 使用虚拟一致性点:不需要阻断整个数据库的业务以及处理流程来达到一份物理一致性点,而是通过部分多版本来达到一个虚拟一致性点。

  6. 小结

openGauss整个系统设计是可插拔、自组装的, openGauss通过支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。其中面向OLTP不同的时延要求,需要的存储引擎技术是不同的。例如在银行的风控场景里,对时延的要求是非常苛刻,传统的行存引擎的时延很难满足业务要求。openGauss除了支持传统行存引擎外还支持内存引擎。在OLAP联机数据分析处理上openGauss提供了列存储引擎,有极高的压缩比和计算效率。另外一个事务里可以同时包含三种引擎的DML操作,且可以保证ACID。

Posted by enmotech at 10:23 AM | Permalink |

近期发表

  • openGauss 数据库列存储引擎 - November 18, 2021
  • 墨天轮国产数据库沙龙 | 胡彦军:华为GaussDB迁移工具解密 - November 17, 2021
  • 国产数据库沙龙 | 张晓庆:GoldenDB分布式数据库自动安装与备份恢复 - November 17, 2021
  • openGauss 数据库存储概览 - November 16, 2021
  • 循序渐进 openGauss :初始化参数的设置、查询和修改 - November 15, 2021
  • openGauss 高级特性介绍 - November 15, 2021
  • openGauss 数据库执行器概述 - November 11, 2021
  • openGauss SQL解析 - November 10, 2021
  • openGauss 查询优化 - November 9, 2021
  • openGauss 论文: DBMind A Self-Driving Platform in openGauss - November 8, 2021


  • CopyRight © 2004 ~ 2012 eygle.com, All rights reserved.