其他分享
首页 > 其他分享> > OceanBase分布式事务剖析

OceanBase分布式事务剖析

作者:互联网

在正文开始之前,首先对OceanBase整体架构和存储引擎做一个简单介绍,以帮助更好地理解OceanBase的事务引擎

1 整体架构

OceanBase(以下简称OB)是一个分布式关系数据库系统,是典型的shared-nothing架构。其架构如下图1所示:
947e1359e4d8057f0c31128231b0a93d_1167x692.jpg@900-0-90-f.jpg

图1 OceanBase整体架构

OceanBase中几个关键概念的解释如下:

2 存储引擎

OB采用LSM-Tree来实现存储引擎,用户的所有写数据被缓存到Memory Table(以下简称memtable)。当memtable中缓存的数据量达到一定阈值时,会发起对memtable的dump操作形成sstable,此过程被称为compaction。
OB中不支持heap表,所有表都是cluster表,表数据被存储在主键索引上,同时支持对表数据创建二级索引。memtable为kv形式的存储结构,对于主键索引和二级索引,对应memtable的key值和value值表示的内容有所不同。

不论是主键索引还是二级索引,每个索引都包含两类查找结构:

memtable的物理结构如下图2所示:
dfd2438581ba90fc235433f20a2701cb_1216x712.png@900-0-90-f.png

图2 memtable物理结构

3 事务处理

3.1 提交协议

第1章中提到,Tablet是数据管理的最小切片,每个Tablet是一个paxos组,并拥有属于这个paxos组的独立日志流。逻辑上,数据库中的每个transaction可能修改若干个表的多个分区,物理上,这些修改操作可能涉及到若干个Tablet,OB的事务处理实际上实现了transaction对这些Tablet增删改查的ACID特性。根据transaction涉及的Tablet数量的不同,将transaction分为两类:

3.1.1 基本协议

本文主要介绍OB的两阶段提交协议,仅在必要处简单介绍一阶段提交相关内容。在介绍OB的两阶段提交协议之前,首先来回顾一下传统两阶段提交,相关流程如图3a所示:

图3 传统两阶段提交协议和OB两阶段提交协议

在传统的两阶段提交协议中,协调者通过持久化日志记录两阶段事务的状态转换,协调者是有状态的。OB的两阶段提交模型与传统两阶段提交有所不同,在OB的两阶段模型中,协调者无状态,不持久化日志,OB的两阶段提交状态全部通过参与者持久化日志来完成,相关流程如图3b所示:

3.1.2 问题讨论

问题1:两阶段提交是一个阻塞性协议,提交过程中,如果存在至少一个参与者故障,协议将无法推进,OB如何解决协议阻塞的问题?

OB的两阶段提交中,参与者是Tablet,根据第1章的描述可知,每个Tablet是一个paxos组,由若干个副本组成,在两阶段提交过程中,由Tablet paxos组的leader负责发起redo和transaction日志,并通过paxos协议持久化相关日志,当Tablet paxos组的leader发生故障后,会通过paxos协议自发选举出该Tablet paxos组的新leader,继续推进事务的两阶段提交。因此,在所有参与者Tablet paxos的多数派可用的情况下,两阶段提交就不会阻塞。

问题2:OB的两阶段提交中,为什么协调者不写日志,协调者不写日志的好处是什么?

与传统两阶段提交协调者写日志相比,协调者不写日志是有利于加快事务响应客户端的速度,同时降低事务在执行过程中两阶段锁的持锁时间,具体地,传统两阶段提交在响应客户端之前需要等待时间为:Tx = Ta(协调者同步写BEGIN日志的时间)+ Tb(所有参与者并行写prepare日志的时间)+ Tc(协调者同步写COMMIT日志的时间);OB的两阶段提交在响应客户端之前需要等待的时间为:Ty = Tb(所有参与者并行写prepare日志的时间)。两相比较,Ty比Tx减少了协调者两次持久化日志的时间。OB两阶段提交是对传统两阶段提交协议的一个优化,该优化有利于提高客户端的响应速度,降低两阶段锁的持锁时间,对提高数据库的整体吞吐量可能带来一定帮助。

问题3:GaussDB的两阶段提交中,协调者也没有记录日志,两者在实现上有哪些异同点?

  • 不同点1:OB比GaussDB相比,多了一个clear阶段。OB多出一个clear阶段的原因如下:假设OB的两阶段提交中没有clear阶段,考虑如下场景,存在一个两阶段提交事务,该事务的参与者涉及A和B,协调者收到A和B的prepare ok请求后决定提交该事务,并分别给参与者A和B发送commit请求,参与者A收到commit请求后,持久化commit日志后退出(注意,OB的参与者两阶段提交状态是在clear日志持久化后退出的,这里我们假设OB没有clear阶段,则参与者的两阶段提交状态在commit日志持久化后退出),参与者B没有收到commit请求,参与者B仍然处在prepared状态。此时如果协调者发生故障,由于参与者A的两阶段提交状态已经退出,参与者B无法查询到参与者A的状态,导致两阶段事务无法继续推进。因此OB的两阶段提交中增加了clear阶段,参与者持久化commit日志后其提交状态并没有退出,而是直到参与持久化clear后才退出。GaussDB的提交中,虽然没有clear阶段,但在GaussDB的实现中有CLog和CsnLog,CLog和CsnLog在参与者的提交状态退出后仍然能够提供事务提交状态的查询,保证两阶段提交最终完成。OB中由于没有类似于GaussDB中的CLog和CsnLog的实现,因此在两阶段提交中引入了clear阶段。
  • 不同点2:在OB的两阶段提交模型中,对于一个事务,只要所有的参与者都prepare ok并持久化了prepare日志,那么此后即使协调者发生故障,该事务最终也一定会提交,而GaussDB的处理中,在协调者故障后,这样的事务可能会回滚。出现这个差异与两者的消息处理机制有关:OB的两阶段提交中,消息交互通过异步rpc实现,消息的接收方不会判断消息的发送方是否正常,消息接收方对所有传入消息无差别处理。在OB这样的消息处理机制下考虑如下的两阶段提交场景,存在一个两阶段提交事务,该事务的参与涉及A和B,A和B分别成功持久化prepare日志,并等待commit请求,协调者正常推进两阶段提交向A发送commit请求消息后故障,由于网络延迟,协调者发送给A的commit请求飘在网络上迟迟没有送达到参与者A,状态机故障恢复后产生新的协调者继续推进两阶段提交,如果此时在OB的消息处理机制下,新的协调者选择回滚事务并给参与者A和B发送abort请求,该事务最终不能满足原子性。因为在OB的消息处理机制中,参与者不会检查消息发送方是否仍然有效,这就导致网络上存在着3个有效的消息包:其一为旧的协调者发给参与者A的commit请求,其二为新的协调者发送给参与者A的abort请求,其三为新的协调者发送给参与者B的abort请求;如果消息1和消息3分别在参与者A和参与者B上执行生效,就会导致该事务最终违反原子性。因此在OB的两阶段提交协议中强制规定,只要全部参与者都处于prepare ok状态,即使此后协调者发生故障,新的协调者也会强制提交该事务。而GaussDB中消息的交互建立在session之上,旧协调者故障后,参与者可以主动丢弃掉旧协调者的无效消息,进而避免上述OB中可能发生的问题。

3.2 全局时间戳服务

OB在给事务定序时,没有采用类似于MySQL的ReadView的思路,而是与GaussDB类似采用了版本号(CSN)的方案,但OB的方案又与GaussDB有所不同,在GaussDB中,CSN是一个逻辑版本号,采取单调加1的方式,而OB的CSN则是一个UNIX的timestamp,OB的版本号保证逻辑上单调递增,同时尽量与UNIX的timestamp接近。使用这种方式实现CSN,存在一些优缺点:

3.2.1 全局时间戳服务高可用

以下详细介绍OB中全局时间戳服务的具体实现。OB的全局时间戳是一个集中式的服务,所有需要获取全局时间戳的进程需要向该集中式服务发起请求,实现上该集中式服务是一个paxos选举组,全局时间戳服务由该paxos选举组的leader提供,并且该paxos选举组有自主选举能力,当就leader发生故障后,paxos组通常可在10s以内选举产生新leader,以保证全局时间戳服务的高可用。

3.2.2 全局时间戳复用

前面提到,OB的CSN基于物理时钟,每秒钟能分配的CSN个数上限为100W个,当分配CSN数量超过每秒钟100W时,可能导致CSN与物理时钟偏差过大的问题,为尽量降低全局时间戳服务的压力,OB设计了CSN Cache,CSN Cache使得事务在提交时并不是每一次都需要访问全局时间戳服务,满足特定条件时可对CSN Cache中的缓存版本号进行复用。具体的复用逻辑如下:如图4所示,T1时刻发生在ObServerA上的事务A收到事务提交的请求,想要获取一个CSN,T2时刻发生在ObServerA上的另一个事务B收到事务提交请求也想要获取一个CSN,T3时刻ObServerA请求全局时间戳服务为其授权一个CSN,ObServerA在T4时刻收到全局时间戳服务的响应得到一个CSN_A。实际上根据linearization的定义,事务A和事务B都可以使用CSN_A作为其提交版本号而不违背外部一致性。因此只要满足以下公式Tstc < Tstf,CSN即可服用。其中Tstc为事务开始提交的时间,Tstf为发起rpc获得全局时间戳服务的开始时间。在实际实现上,可以在每个ObServer都启动一个CSN Fetcher线程专门用来调用全局时间戳服务获取CSN并存入CSN Cache,事务提交时只需访问CSN Cache即可。
ce0d5df41a5e49db4de92a09ffe491f9_1926x492.png@900-0-90-f.png

图4 CSN Cache复用

3.3 并发控制

3.3.1 多版本数据维护

OB的数据写入全部发生在memtable中,memtable对上层提供kv存储接口。memtable使用多版本并发控制(MVCC)协议,会维护数据的多个版本。OB的多版本数据维护有自己的特点,以下简要对比OB与传统数据库多版本数据维护的异同。

图5 多版本数据维护

3.3.2 数据读取

本节不涉及读写冲突的并发控制,仅介绍Lsm-Tree的数据读取方法。与传统数据库不同,OB每一行的全量数据需要将memtable和磁盘上对应的sstable数据基于primary key做sort merge,在memtable中,由于每次修改仅记录涉及列的值,在读取行数据时,需要对memtable中对应行的所有历史数据链表进行一次遍历,然后再对应与sstable的行数据做merge。在memtable某行经过多次修改使得
链表过长时,可能对读取性能造成较大影响。为解决大量修改导致的读取性能下降问题,需要对历史数据链表过长的问题进行优化。

3.3.3 行历史数据优化

行历史数据链表过长问题有两方面的优化:

3.3.4 关于历史数据回收的讨论

在基于MVCC协议的实现中,历史数据的回收是每个数据库都需要解决的问题,解决上主要有两个思路:

3.3.5 写读冲突

3.3.5.1 事务内可见性

在一个事务内,当前语句应当读到本事务内该语句之前其他语句的修改,但不能读到本语句自己的修改。实现上OB与GaussDB类似,在事务内使用sql_no为每一个语句按照执行顺序编号(与command_id向对应),对于同一个事务,当前语句只能读取到比当前sql_no更小的语句的修改。

3.3.5.2 事务间可见性

很多数据库基于MVCC实现做到了读写相互不冲突,但在分布式数据库基于版本号的MVCC并发控制中,可能存在着写阻塞读的情况。这里首先介绍OB在两阶段提交中确定事务提交版本号的流程,然后再介绍OB中的写读冲突。OB的事务提交版本号确定细节如下流程:

在上面的流程中处于commit_in_progress和commit_determined的这段时间之内,参与者无法确定本事务的最终提交版本,相应地尝试读取本修改的读请求就会被阻塞,直到本事务的最终提交版本号确定为止。

3.3.5.3 写读冲突问题讨论

问题:在3.3.5.2节中提到,事务处于commit_in_progress和commit_determined之间时读请求会被阻塞,假设读请求不阻塞,而是直接忽略本次事务的修改,会有什么问题?
解释:会出现写读冲突的异常情况,违背事务原子性,出现一个语句读到不完整的事务。考虑一个场景,一个分布式事务Xact涉及两个参与者A和B,提交过程中参与者A先达到了commit_determined,参与者B还处于commit_in_progress状态。与此同时发生了一个分布式读请求,读取的目标对象刚好包含参与者A和B。如果读请求跳过参与者B的commit_in_progress状态,则最终会读取到Xact对参与者A的修改,却没有读到Xact对参与者B的修改,违背事务原子性。

3.3.5.4 优化讨论

在上述的描述中,存在两个可以优化的点:

3.3.6 写写冲突

OB通过两阶段锁解决写写冲突的问题,事务对行数据修改之前,需要首先加行锁,加行锁成功才能进行后续操作,并且OB采用Lsm-Tree架构,其行锁可以全部放在内存当中。针对lost_update异常,使用first-commit-win原则。

3.4 备机读

备机读是OB中一个非常重要的性质,OB的备机读实现非常有特色,既实现了一致性的弱读,又保证了弱一致性读与日志回放相互不阻塞。但OB的备机读将事务模块和日志模块紧密耦合在一起,需要两者紧密配合。备机弱一致性读的核心是获取一个安全的snapshot,使用该snapshot读取既可以满足一致性的弱读,又能保证弱读与日志回放互不影响。以下首先介绍OB日志模块的一些基础,然后剖析OB备机读的实现原理:

3.4.1 日志标识基础

在常见的数据库系统中使用LSN唯一标识一条日志,OB到4.x版本中才引入了LSN的概念,在3.x和之前版本的OB中是使用LogTag来唯一标识一条日志的(4.x版本的OB中仍然保留LogTag的概念)。LogTag包含两个字段分别为:LogID和LogTs,其中LogID是一个单调递增的逻辑ID号(如0,1,2,3,4…),LogTs是一个ObServer提供的一个本地时间戳。同时,对于任意两个LogTag来说:LogID1 > LogID2等价于LogTs1 > LogTs2,也即对于不同的LogTag,LogID与LogTs存在偏序关系。

3.4.2 日志回放基础

OB的日志回放有两个重要模块,LogDispatcher和LogReplayEngine,其中LogDispatcher负责按照LogID顺序分发日志,而LogReplayEngine可乱序回放日志,LogReplayEngine的乱序回放发生在事务之间,同一个事物的多条日志仍然顺序回放。假设LogDispatcher即将分发的下一条LogID为left_barrior,称所有LogID小于left_barrior的还未完成日志回放的所有事务的集合为InReplayingXactSet。

3.4.3 事务local_CSN

在之前的3.3.5.2中提到中提到了local_CSN的概念,之前的描述为了方便理解,没有详细描述local_CSN的详细获取过程。本节详细描述如何结合事务模块和日志模块获得local_CSN(此处解释3.3.5.2节b中埋的坑),并描述commit_CSN与日志模块的关系。首先聚焦参与者的两阶段提交的详细流程:

local_CSN == MyLogTs (MyLogTs指事务调用日志模块,被日志模块写下的该条日志的LogTs)
local_CSN > tmp_CSN。
commit_CSN >= MyLogTs

注意,确定commit_CSN时,各参与者还没有写commit日志,所以此时commit日志不在讨论之列。基于上面的推倒,可以得出结论:

结论1: 一个事务的最终提交版本号大于除commit日志以外,本事务内其他所有日志的LogTs的。

这个结论将事务的提交版本号和日志的LogTs之间建立起了明确的大小关系,为备机弱一致性读snapshot的确定奠定了基础。

3.4.4 备机弱一致性读snapshot的确定

想要做到备机弱一致性读与日志回放互不阻塞,选取的备机弱一致性读snapshot就需要满足:

原则1:所有提交版本号小于snapshot的事务在使用snapshot读取的时刻已经全部回放完成。

以下介绍选取备机弱一致性读snapshot的选取方法,并证明该snapshot能够满足上述原则。
InReplayingXactSet中存在着N个正在回放的读写事务(只读事务不产生日志与备机读无关),每个事务包含若干条日志,假设事务k(0<= k <= N-1)的第1条日志对应的LogTs为LogTs(k),则snapshot的选取公式为:

公式1:snapshot = min{ LogTs(k) },其中(0 <= k <= N-1),也即snapshot是InReplayingXactSet中所有的事务日志中的最小LogTs,记与其对应的LogTag的LogID为SnapshotLogID。

以下证明该snapshot可以满足上述原则1,并且该snapshot可以作为备机弱一致性读的snapshot。采用反证法,假设存在一个事务Ta,该事务的提交版本号小于snapshot且该事务还没有完成回放,由于Ta还没有完成回放,则Ta有两种可能:

综上所述不存在这样一个事务Ta。也就是说采用公式1得到的snapshot可以满足原则1,可以作为备机弱一致性读的snapshot使用。

3.4.5 非分布式事务

上面结论1中说到是除commit日志以外,在事务提交时需要至少存在两条日志,上述推演才成立。如果事务在提交过程中仅写了一条日志,也就是一个只写了一条日志的非分布式事务,上述推演还是否成立?其实只要稍作变化,上述推演仍然能够成立。即对于单条日志的非分布式事务,只要将该日志的LogTs作为最终的事务提交版本号即可。

3.5 备机读snapshot与PITR

在OB的实现中CSN本身是一个物理时钟,其PITR实现中无需记录逻辑CSN与物理时间的映射关系。同时由于有了备机读snapshot的机制,OB将CSN和本地时钟(LogTs本质上是ObServer的本地时钟)建立起了关系。在进行日志归档时,对于不产生日志的那部分日志流,单个ObServer基于本地时钟就可以将不产生日志的日志流的归档进度向前推进。在PIRT恢复时,GaussDB是恢复到PITR时间点对应的CSN,如果相应时间段内某个日志流没有日志,则恢复到对应barrior点。OB在PITR恢复时,则一直向后扫描日志直到遇到LogTs大于等于PITR时间点的日志为止。
与GaussDB相比,OB的PIRT实现有两个优点:

但是也由于OB将CSN做成了Unix Timestamp物理时钟,这就导致当集群负载过大,全局时间戳服务的QPS超过100W/s时,CSN会被加速推进并偏离物理时间,此时的PITR在准确性上可能较差。

标签:事务,CSN,OceanBase,协调者,OB,剖析,日志,参与者,分布式
来源: https://www.cnblogs.com/qiumingcheng/p/16671880.html