OceanBase分布式事务剖析

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

1 整体架构

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

图1 OceanBase整体架构

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

  • Tablet:是OB集群数据管理的最小切片(注意这里的Tablet与数据库系统中分区表的partition不是一个概念),一个OB集群管理大量Tablet,每个Tablet是一个Paxos选举组,有属于该Paxos选举组单独的日志流,一个Paxos选举组由N个(通常为3个或5个)副本组成,其中1个副本为该Paxos选举组的leader,其他N-1个副本为follower。leader负责产生paxos log,并将paxos log同步给各follower,follower通过同步leader的paxos log来跟随leader的状态,以实现数据的高可用。leader副本提供数据的强一致性读写服务,follower可提供数据的弱一致性读服务。
  • ObServer:是OB集群的数据库进程节点,OB集群中的所有ObServer进程完全对等,每个ObServer包含各自的SQL引擎,事务引擎和存储引擎。同时ObServer是Tablet副本的容器,每个ObServer管理若干Tablet副本,并为这些Tablet副本提供存储,事务和读写等各类数据库服务。
  • Available Zone(数据中心):是OB管理ObServer的容器,每个Available Zone中部署若干个ObServer。针对每个Tablet的多个paxos副本,每个Available Zone内包含,并且仅包含该Tablet的一个paxos副本,也即,每个Available Zone内都包含有该OB集群的一份完整数据。根据不同的容灾需求,OB集群常见的部署方式有:
    • 一地三中心部署:解决Available Zone(数据中心)级容灾需求;
    • 两地三中心部署:解决城市级故障后仍能提供读服务的容灾需求;
    • 三地五中心部署:解决城市级故障后仍能提供读写服务的容灾需求。

2 存储引擎

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

  • 主键索引的memtable中:key值为该行的primary key列,value值为该行的所有列。
  • 二级索引的memtable中:key值为相关二级索引列+primary key列,value列为空。

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

  • hash查找结构:主要服务于对memtable的点查询。
  • btree查找结构:主要服务于对memtable的范围查询。

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分为两类:

  • 当transaction的修改仅涉及到一个Tablet时,只需要写一个日志流,该transaction为非分布式事务,使用一阶段提交流程。
  • 当transaction的修改涉及两个及以上Tablet时,需要写多个日志流,该transaction为分布式事务,使用两阶段提交流程。

3.1.1 基本协议

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

  • a 协调者收到事务提交请求后,首先同步记录一条BEGIN日志,该日志包含本事务的transactionID和参与者列表;
  • b 协调者向每个参与者发送prepare请求;
  • c 参与者收到协调者的prepare请求,确认全部redo日志持久化成功,记录prepared日志;
  • d 参与者响应协调者,给协调者发送prepare ok消息;
  • e 协调者等待并搜集全部参与者的prepare ok消息,同步记录下commit日志,至此本事务状态确定;
  • f 协调者响应客户端,通知客户端事务commit成功;
  • g 协调者给每个参与者发送commit请求;
  • h 参与者写commit日志,响应协调者commit ok,并退出;
  • i 协调者收集所有commit ok请求后,写clear日志,并退出。
    5a7d3bc283e0280bbd6a357842657139_1776x1116.png@900-0-90-f.png
图3 传统两阶段提交协议和OB两阶段提交协议

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

  • a 协调者收到事务提交请求后,向所有参与者发送prepare请求,该prepare请求包中包含所有参与者列表;
  • b 参与者收到协调者的prepare请求,确认全部redo日志持久化成功,记录prepared日志,该prepared日志中包含全部参与者列表;
  • c 参与者响应协调者,给协调者发送prepare ok消息;
  • d 协调者搜集所有参与者的prepare ok消息后,应答客户端(这里协调者虽然不写日志,但在OB的2PC模型中,只要全部参与者都prepare ok,就认为该事务确定为commit);
  • e 协调者向所有参与者发送commit请求;
  • f 参与者收到协调者的commit请求,持久化commit日志,并响应协调者commit ok,注意此时参与者状态不能退出;
  • g 协调者搜集所有参与者的commit ok请求后,向参与者发送clear请求;
  • h 参与者收到clear请求,持久化clear日志,响应协调者clear ok,并退出;
  • i 协调者搜集到所有clear ok响应后退出。

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,存在一些优缺点:

  • 优点:事务的提交CSN整体与物理时间接近,在进行容灾恢复等操作时,恢复到指定时间可直接使用CSN作为物理时间。实现上无需记录CSN到物理时间的映射。
  • 缺点:由于尽量靠近物理时间,每秒钟能够提供的最多CSN数量上限为100W个(CSN单位是微秒),当集群存在超大负载时,可能出现CSN推动速度大于物理时钟的运行速度,导致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与传统数据库多版本数据维护的异同。

  • oracle和mysql的数据更新为in-place update,并将历史的多版本数据通过undo log维护在rollback segment内;
  • postgresql的数据更新使用append方式,历史数据和当前数据同时存储在同一个heap中;
  • OceanBase的数据写入全部发生在内存中,对于同一行数据的修改,将所有历史数据通过链表串联在同一kv存储单元中,并在链表中记录每一次修改对应的事务提交版本号,为节约内存开销,对memtable的每一次修改都只记录被修改列的值,修改不涉及的列不会记录在memtable中,OB的多版本数据的维护如图5所示。
    763709543f8c31ee0cb1f3df8e8278fa_1788x618.png@900-0-90-f.png
图5 多版本数据维护

3.3.2 数据读取

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

3.3.3 行历史数据优化

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

  • memtable内部的优化:在memtable内部,将一行历史数据链表的多次修改合并成一次修改,使链表长度变短,合并生成的新节点的事务版本号记作合并前所有被合并节点中事务版本号最大的那一个,这样合并会丢失掉一部分历史快照信息,因此当读操作遇到版本丢失的历史数据节点时,会报错snapshot too old。
  • 通过memtable dump生成sstable:在sstable中将行历史数据的多次修改dump成单一版本的磁盘行数据,与memtable内部优化类似,memtable dump生成sstable也会丢失历史快照数据,当读操作的snapshot过老时,同样会报错snapshot too old。

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

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

  • 第一:跟踪当前系统中的最老snapshot,只有小于snapshot的垃圾或历史数据才可以被回收。
  • 第二:回收垃圾或历史数据时,不跟踪最小snapshot,而是记录下当前垃圾或历史数据回收到了哪个版本号,读取发生时,如果snapshot比回收版本号小,则读取报错snapshot too old。很显然OB采取的是这种方式。

3.3.5 写读冲突

3.3.5.1 事务内可见性

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

3.3.5.2 事务间可见性

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

  • a 事务两阶段提交状态机启动,协调者给全部参与者发起prepare请求;
  • b 各参与者收到协调者发来的prepare请求,参与者将本事务的状态设置为commit_in_progress,然后每个参与者与全局时间戳服务通信获取CSN,并通过转换获得该参与者的临时提交版本号称为local_CSN(转换细节此处埋坑,详见后文“备机读”),并将该临时提交版本号持久化写入prepared日志中。参与者响应prepare ok给协调者,prepare ok报文中包含每个参与者各自的临时提交版本号。
  • c 协调者搜集全部参与者的prepare ok报文,并选取所有prepare ok报文中临时提交版本号最大的那一个,记作commit_CSN。
  • d 协调者给全部参与者发送commit请求,该commit请求报文中包含commit_CSN。
  • e 参与者收到协调者发来的commit请求,,commit日志中包含协调者发来的commit_CSN,参与者将本事务状态设置为commit_determined,持久化commit日志。并以此commit_CSN作为本事务最终的提交版本号。

在上面的流程中处于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 优化讨论

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

  • 各参与者与全局时间戳服务获取CSN的优化,实际上并不需要所有参与者都与全局时间戳交互,只需要其中一个参与者获取全局时间戳服务即可,其他参与者获取本地的local timestamp即可。这可以降低全局时间戳服务的压力。但是参与者获取的local timestamp可能比全局时间戳的当前CSN更大,这是否会有问题?实际上事务会等到commit_CSN跨过全局时间戳的CSN才提交,这类似于spanner的comit wait逻辑。
  • 在commit_in_progress阶段,可以将local_CSN作为本事务的临时提交版本号,针对snapshot小于临时提交版本的读取请求,该事务的修改对这样的读请求一定不可见,可直接跳过。针对snapshot大于比临时提交版本的读请求则仍然需要被阻塞。这个优化能够在一定程度上降低写读冲突的概率。

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与日志模块的关系。首先聚焦参与者的两阶段提交的详细流程:

  • a 参与者收到协调者发送的prepare请求,与全局时间戳服务交互,获取一个CSN(如果是优化算法,只有一个参与者与全局时间戳交互,其他参与者直接获取本地当前timestamp,无论是否与全局时间戳服务交互,此处统称此值为CSN);称此CSN为tmp_CSN。使用此tmp_CSN调用日志模块写prepare日志,注意此处日志模块的LogTag不但会满足LogID于LogTs的偏序,同时会给调用者(此处调用日志模块的调用者为事务参与者)返回一个返回值local_CSN。此local_CSN满足:
local_CSN == MyLogTs (MyLogTs指事务调用日志模块,被日志模块写下的该条日志的LogTs)
local_CSN > tmp_CSN。
  • b 同时基于3.3.5.2节中c的描述可知commit_CSN >= local_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不在InReplayingXactSet中:由于Ta不在InReplayingXactSet中,说明Ta的第一条日志还没有被LogDispatcher分发,也就是说Ta的FirstLogID >= left_barrior,有因为left_barrior > SnapshotLogID,所以Ta的FirstLogID > SnapshotLogID,根据LogTag的偏序关系可知FirstLogTs > snapshot。又根据结论1可知Ta的commit_CSN > FirstLogTs > snapshot。这与前面的假设Ta的事务提交版本号小于snapshot的假设矛盾,所以该种假设不成立。
  • Ta在InReplayingXactSet中:由于Ta在InReplayingXactSet中,所以Ta的FirstLogTs >= snapshot (这是因为snapshot是所有InReplayingXactSet事务中LogTs的最小值)。而根据结论1可知Ta的commit_CSN >= FirstLogTs > snapshot。这又与前面的假设Ta的事务提交版本号小于snapshot的假设矛盾,所以该种假设也不成立。

综上所述不存在这样一个事务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实现有两个优点:

  • 不需要一个中心节点周期性的记录逻辑CSN与物理时间的对应关系。
  • 不需要一个中心节点周期性的通过打点的方式通知没有日志的日志流的归档进度,各ObServer单独写自己的归档进度日志即可。

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

posted @ 2022-09-09 10:19  邱明成  阅读(1191)  评论(1编辑  收藏  举报