暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

openGauss数据库源码解析系列文章——存储引擎源码解析(五)

Gauss松鼠会 2021-07-01
923

Gauss松鼠会
学习 探索 分享数据库知识 共建数据库技术交流圈
关注

上篇图文openGauss数据库源码解析系列文章——存储引擎源码解析(四)中,从内存表的总体架构和代码概述FDW进行了分享,本篇将从内存表的存储、索引、事务、并发控制、重做日志、检查点、恢复展开详细介绍。

(三)内存表的存储

Table类包含管理数据库中内存表所需的所有项。表由以下组件组成:列、主索引和可选的二级索引。关键成员变量说明如表1Table类的关键成员变量所示。

表1  Table类的关键成员变量

成员变量

描述

tableCounter : std::atomic<uint32_t>

原子表ID

m_tupleSize : uint32_t

原始元组大小(以字节为单位)

m_tableExId : uint64_t  

openGauss提供的外部表ID

m_secondaryIndexes : SecondaryIndexMap

按名称访问的二级索引映射

m_rwLock : pthread_rwlock_t  

RW Lock,防止在检查点/真空期间删除

m_rowPool : ObjAllocInterface*

row_pool行分配器对象池

m_primaryIndex : MOT::Index*

主索引

m_numIndexes : uint16_t  

正在使用的耳机索引数

m_indexes : MOT::Index**  

索引数组

m_fixedLengthRows : bool

指定行是否具有固定长度

m_fieldCnt : uint32_t

表schema中的字段个数

m_columns : Column**

列数组

Row类包含管理表中的内存行所需的所有项,关键成员变量如表2所示。

表2  Row类的关键成员变量

成员变量

描述

m_data : uint8_t

保存行数据的原始缓冲区,开始于类的结束位置

m_keyType : KeyType  

使用的键类型。

Ÿ Internal –用于内部测试(64位)

Ÿ Surrogate –用于无索引表

Ÿ External –需要从行生成

m_pSentinel : Sentinel*

指向主哨兵的指针

m_rowHeader : RowHeader

OCC行的头部,包含OCC操作的所有相关信息

m_rowId : uint64_t  

创建行时生成的唯一rowId

m_table : Table*

指向内存管理表的指针

(四)索引
MOT使用索引来高效地访问数据。MOT索引支持范围查询等所有基本操作。由于数据存储在Row类中,每个MOT索引都按顺序使用哨兵来访问数据。
IndexFactory类提供了创建新索引对象的能力。

作为Table类的一部分,Index抽象类提供了创建和访问数据索引的能力。索引是否满足唯一性决定了该索引是否允许插入重复键。如图1所示,描述了一个有三行和两个索引的MOT表T的结构,其中一个索引是非唯一索引,另一个索引是唯一索引。对于非唯一索引而言,MOT内部通过在插入时用唯一标识符填充每个键的方式将键视为唯一。在图2中,MOT将哨兵插入到带有键的唯一索引和带有键+后缀的非唯一索引中。使用哨兵方便了维护操作,因为在进行维护操作时,可以在不接触索引数据结构的情况下替换行。

 

图1  唯一、非唯一索引和哨兵

Sentinel类包含指向唯一索引情况下的行数据或非唯一索引情况下主哨兵的指针,还包含一些标志位和引用计数等支持跨事务并发的信息。每次向索引插入新键时都会创建哨兵。例如,对于具有3个索引的表,插入新键时将创建3个哨兵,每个索引对应一个哨兵。哨兵和行之间的关系如图2所示。

图2  哨兵与行关系

MasstreePrimaryIndex类实现了索引接口。它基于Masstree K/V存储实现,同时封装了OT内存分配池,根据对象分配任意大小内存。

IndexIterator抽象类提供了创建迭代器并根据提供的迭代器访问数据的能力。

(五)事务

事务部分覆盖了从openGauss映射到MOT的所有支持的DDL/DML操作。

事务与并发控制机制紧密耦合,每个操作都必须通过并发控制管理,并完成相应的行为。MOT基于乐观并发机制,几乎不使用锁,因此每个客户端都有自己的事务视图,并且不会阻塞DML,与磁盘表对每个非SELECT操作都加锁的使用方式有显著区别。

每个局部行都有一个初始状态,状态由txn_state_machine管理。txn_state_machine扩展了Silo,支持新操作写后读和读后写,类似于MESI缓存一致性协议。如图3所示,MOT将新操作(RD/WR)视为本地缓存中的缓存不命中,并将状态从无效提升为新状态。

 

图3  DML事务状态机

详细流程

SELECT具体流程如图4所示。

 

图4 SELECT时序图

(1) 当SELECT操作被发送到FDW,FDW就会打开一个游标并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效则在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估隔离级别和来自缓存的结果:如果TxnAccess返回了一行,直接将其返回给openGauss;否则以下下两种情况。

① 隔离级别为READ_COMMITED时生成行的副本并返回给FDW。

② 隔离级别为REPEATABLE_READ时映射缓存中的行,并将缓存的行返回给FDW。

UPDATE具体流程如图5所示。

 

图5  UPDATE时序图

(1) 当UPDATE操作被发送到FDW,FDW就会打开一个游标,并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效就在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估来自缓存的结果。
① 如果TxnAccess返回了一行,直接将其返回openGauss。
② 如果没有找到该行,则映射哨兵并返回缓存的行。
(5) openGauss计算返回的行,如果该行与筛选器匹配则openGauss向FDW发送带有更新数据的更新操作。
(6) TxnManager将行的状态提升为WR,并用从openGauss接收的新数据更新本地行。
DELETE具体流程如图6所示。

 

图6 DELETE时序图

(1) 当DELETE操作被发送到FDW,FDW就会打开一个游标并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效就在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估来自缓存的结果。

① 如果TxnAccess返回了一行,直接将其返回openGauss。

② 如果没有找到该行,则映射哨兵并返回缓存的行。
(5) openGauss计算返回的行,如果该行与筛选器匹配,则openGauss向FDW发送带有更新数据的删除操作。
(6) TxnManager将行的状态提升为DEL,并将本地行标记为已删除。
INSERT具体流程如图7所示。

 

图7 INSERT序列图

(1) 操作发送到FDW后,FDW使用表API准备插入的行,并将该行发送到事务管理器。
(2) 事务管理器执行以下算法。
对于表中的每个索引执行以下操作。
① 将哨兵插入索引。
② 如果已提交行–中止事务。
③ 如果成功插入行–映射并完成插入。
④ 如果行不存在,如下。
Ÿ 如果已映射-自己插入,则中止。

Ÿ 否则将它映射到本地缓存。

(3) TxnManager对于重复的key返回RC_OK或RC_ABORT。TxnDDLAccess用于缓存和访问事务性DDL更改。事务中执行的所有DDL都存储在TxnDDLAccess中,并在事务提交/回滚时应用回滚。假设openGauss负责DDL并发,并确保并发的DDL更改不会并行执行。TxnAccess类用于缓存和访问事务性DML更改的。在事务中执行的所有DML都存储在TxnAccess中,并在事务提交/回滚中应用回滚。Access类用于保存单行访问的数据。AccessParams用于保存当前访问的参数,为CC管理提供额外的信息。InsItem用于保存行插入请求的数据。

(六)并发控制

MOT采用源自SILO的单版本并发控制(concurrency control,CC)算法,是一种OCC算法。并发控制模块满足内存引擎的所有事务性需求,其主要设计目标是为MOT内存引擎提供各种隔离级别的支持。当前支持如下隔离级别。

(1) 读已提交(READ-COMMITED)。
(2) 可重复读(REPEATABLE-READ)。

图8  MOT本地内存和全局内存

图8显示了MOT运行事务时的关键技术,包括如下内容。
(1) 私有事务内存用于无锁读写,仅在最终提交时使用锁,低争用。
(2) 低时延,NUMA感知的本地内存。
(3) 乐观并发控制:数据锁最小化,低争用。
(4) 无锁自动清理(Auto-Vacuum),无开销。
(5) 极致优化的Masstree实现。

1. SILO并发控制背景&算法

Silo来自Stephen Tu等人在计算机顶级会议SOSP13上发表的《Speedy Transactions in Multicore In-Memory Databases》,在现代众核服务器上实现了卓越的性能和可扩展性。Silo的设计完全是为了高效地使用系统内存和高速缓存。例如,它避免了所有集中的争用点,包括集中事务ID分配。Silo的关键贡献是一种基于乐观并发控制的提交协议,它支持序列化,同时避免对仅读取的记录进行共享内存写入。Silo可提供与其他可序列化数据库一样的保证,而不会出现不必要的可扩展性瓶颈或额外的延迟。
设计MOT的设计原则是通过减少对共享内存的写入来消除不必要的争用。Silo按照固定时间间隔的epoch进行时间分段,因此Silo这种OCC的变体可以支持序列化,即在epoch边界形成自然序列化点。在恢复之后也能通过CSN或周期性更新的epoch实现序列化。Epoch还有助于提高垃圾回收效率并使能快照事务。其他一些设计,如事务ID的设计、记录覆盖和支持范围查询等,进一步加快了事务执行,同时非中心化的持久化子系统也避免了争用。

2. 事务ID

SILO的并发控制以事务ID(tansaction ID,TID)为中心,它标识事务并记录版本,也用作锁和检测数据冲突。每个记录都包含最近修改它的事务的TID。TID为64位整数。每个TID的高位包含一个CSN,CSN等于对应事务提交时间的全局序列号;低三位分别为:Bit 63:锁定标志位,Bit 62:最新版本标志位,Bit 61:不存在状态标志位。由于CSN有效长度为61bit,因此MOT忽略了事务ID回卷。另外,与许多系统不同,Silo以分散而非集中的方式分配TID。

3. 数据布局

Silo中的一条记录包含以下信息。
(1) 一个64位的TID(MOT使用CSN)。
(2) 记录数据。提交的事务通常就地修改记录数据,主要通过减少记录对象的内存分配开销来提升短写的性能。然而,读者必须使用版本验证协议以确保已读取每个记录数据的一致性版本。

4. 乐观并发控制的数据库操作

1) 读/写流程
(1) 在索引中搜索行引用。
(2) 将数据免锁复制到基于类型的本地集,包括读写集(Read/Write Set, R/W set)。
(3) 基于本地副本进行处理。
2) 校验流程
(1) 按主键顺序对写集(Write Set)进行排序。
(2) 锁定写集中的所有行。
(3) 验证读写集的行。
(4) 验证本地行CSN是否更改。
(5) 验证该行是否为该键的最新版本(由于存在本地数据,可能并非最新)。
(6) 验证该行未被其他事务锁定。
(7) 如果以上任一项验证失败,则中止事务。
(8) 否则将更新CSN后的所有写集中的行复制回去,然后释放这些行上的锁。
3) 插入流程
(1) 构造一个CSN=0且状态为不存在的新行r。
① 添加r到写集并视为常规更新。
② 生成唯一的键k。
(2) 在状态为不存在的情况下,向树/索引添加从k → r的映射。
① 如果k已经映射到一个状态为存在的记录,则插入失败。
② 否则在读阶段增大版本号。
4) 校验流程
(1) 锁定写集。
(2) 验证插入集(insert set)。
(3) 若事务中止,则垃圾回收器记录状态为不存在的行。
5) 删除流程
(1) 在索引中搜索行引用。
(2) 将行映射到本地缓存。
(3) 将本地副本标记为已删除。
6) 校验流程
(1) 验证行保持不变;已删除的行将被视为更新。
(2) 从索引中删除行,即将已删除的哨兵/行放入垃圾回收器中。

图9  MOT提交协议伪代码

MOT提交协议伪代码如图9所示。

5. 关键类和数据结构

并发控制的关键类和数据结构如表3所示。

表3并发控制的关键类和数据结构简介

关键类和数据结构

描述

OccTransactionManager类

管理整个事务验证机制,与事务类紧耦合

RowHeader类

每一行的OCC元数据。头部为64位,包含状态位和CSN

(七)重做日志

MOT重做日志(Redo Log)使用预写式日志(write-ahead logging,WAL)技术来确保数据完整性。WAL的核心概念是,内存中的数据和索引的更改只有在记录下这些更改之后才会发生。因此写入重做日志是MOT提交协议的一部分。

如图10所示,MOT存储引擎的重做日志模块同样使用openGauss磁盘引擎的日志接口进行持久化和恢复。这意味着MOT重做数据被写入相同的XLOG文件,并使用相同的XLOG逻辑。使用与openGauss磁盘引擎相同的日志记录接口可确保跨引擎事务的一致性,并减少复制、日志恢复等模块的冗余实现。

图10  使用相同的XLOG (WAL)基础架构的openGauss磁盘库和MOT

1. 事务日志记录

与openGauss其他存储引擎不同,MOT内存引擎仅在事务实际提交时才会写入重做日志。因此,在事务期间或事务中止时,数据不会写入重做日志。这样可以减少写入的数据量,从而减少不必要的磁盘IO调用,因为这种磁盘IO调用很慢。例如,如果在事务期间多次更新同一行,则只将表示已提交行的最终状态写入日志。

由于设计MOT内存引擎时考虑了对接不同的数据库的可能性,因此如图11所示,MOT通过抽象的ILogger接口对接重做日志。

图11 ILogger接口

2. 日志类型

设计MOT内存引擎时同样考虑了支持不同的日志记录方式。如图12所示,MOT当前已实现同步日志(synchronous redo Log)和同步组日志(group synchronous redo log)。这是通过RedoLogHandler类实现的。RedoLogHandler封装了日志逻辑,在数据库启动时初始化。RedoLogHandler可以根据需要扩展实现新的日志记录方式。

图12  RedoLogHandler接口

每个事务管理器对象(TxnManager)都包含一个Redolog类,该类负责在提交时将事务数据序列化到缓冲区中。如图13所示,该缓冲区被传输到RedologHandler以进行日志记录。

图13  使用RedoLogHandler的事务日志记录

1) 同步日志记录
同步日志使用SynchronousRedoLogHandler。如图14所示,这是一个简单的RedoLogHandler实现,它只将序列化缓冲区委托给ILogger(XLOGLogger),以便将其写入XLOG。因为在写缓冲区时,事务被阻塞,所以称为同步。只有当所有事务数据被序列化并写入日志时,提交协议才会继续。

 

图14  SynchronousRedoLogHandler

2) 同步组提交日志记录

同步组提交日志由SegmentedGroupSyncRedoLogHandler类实现。它通过将几个事务分组到一个写块(write block)中并一起写入的方式优化日志记录。这种方法在一次调用中收集更多数据,可以最大限度地减少磁盘IO次数。除此之外, SegmentedGroupSyncRedoLogHandler将每个NUMA处理器(socket)的事务分组,以减少跨NUMA处理器的数据传输,因为跨NUMA处理器的数据访问比同一NUMA处理器本地内存访问慢。

当事务提交时,它将数据序列化到缓冲区中,这个缓冲区被传输到SegmentedGroupSyncRedoLogHandler,并放入一个提交组中。提交组(Commit Group)是一组序列化事务缓冲区的集合,这些事务缓冲区将被提交并写入磁盘。根据不同的配置参数,当一个组被填满或超过预先配置的时间时,MOT将关闭该组,并将该组内所有缓冲区一起写入日志。

图15描述了将多个事务分组一起写入的组提交逻辑。

 

图15  同步组提交对每个NUMA处理器的事务进行分组

3) 异步日志

MOT暂未开发专用的异步日志机制,异步日志是通过在conf配置文件中将synchronization_commit参数设置为“off”来实现的。

3. 关键类和数据结构

重做日志的关键类和数据结构如表4所示。

表4  重做日志的关键类和数据结构简介

关键类和数据结构

描述

RedoLog类

负责事务数据序列化的主要类,它是TxnManager类的成员对象。RedoLog的commit方法由TxnManager在事务通过验证阶段并且所有更新的行均已锁定后在提交协议中调用。在将变更应用到存储之前,RedoLog将在加锁后序列化事务数据,持久化到日志,释放锁。RedoLog使用RedoLogHandler将数据写入日志。RedoLogBuffer类是序列化缓冲区的一个简单实现,RedoLog类通过RedoLogBuffer序列化事务操作。RedoLogBuffer的前4个字节预留给缓冲区大小,在序列化时写入。OperationCode枚举是支持的事务操作列表。RecoveryManager通过根据OperationCode确定如何解析数据并应用事务操作

RedoLogWriter

用于将操作序列化到缓冲区中。RedoLogWriter是一个简单的helper类,获取数据和缓冲区,并将数据序列化到缓冲区中

EndSegmentBlock

控制块,写在每个flushed缓冲区的末尾。包括事务、CSN、是否提交等信息

EndSegmentBlockSerializer

一个简单的helper类,用于序列化和反序列化EndSegmentBlock

Ilogger接口

写日志接口的抽象。MOT通过ILogger可以写入不同类型的日志

LoggerFactory

一个工厂类,用于创建不同类型的ILogger。LoggerFactory通过MOT配置项确定要创建哪种logger

XLOGLogger

基于openGauss XLOG (WAL)的ILogger的一个实现,它简单地使用openGauss WAL接口将序列化的事务写入WAL中。MOT WAL日志项有自己的资源管理器,这可以使openGauss识别到该日志项是一个MOT WAL日志项,并将其转发到MOT处理。XLGLogger使用openGauss日志基础能力。因此,AddToLog是一个对openGauss XLOG接口的简单委托

图16所示代码为XLOGLogger::AddToLog接口的实际实现。

 

图16  XLOGger对openGauss XLOG的委托

RedoLogHandler是重做日志逻辑的抽象。RedoLogHandler的派生类可实现不同的日志方法。RedoLogHandler是一个单例模式,由MOT管理,为RedoLog所用。

RedoLogHandlerFactory用于创建RedoLogHandler。MOT根据配置项中配置的RedoLogHandlerType创建RedoLogHandler。

SynchronousRedoLogHandler简单地将RedoLogBuffers委托给ILogger,以便写入重做日志。请参阅前述的同步日志记录小节。

GroupSyncRedoLogHandler是最先进的无锁组提交RedoLogHandler。GroupSyncRedoLogHandler将几个事务的redo log缓冲区分组到一个组,并把他们写在一起,以便优化和最小化磁盘IO。请参阅前述同步组提交小节。CommitGroup表示将一组RedoLogBuffer一起记录。一个提交组有一个主线程,由该主线程创建该提交组,它负责将组内的所有RedoLogBuffer写入日志。主线程写日志时,所有其他线程都在等待。主线程完成写入后将发送信号来唤醒组内其他所有线程,一旦唤醒,事务就可以继续。SegmentedGroupSyncRedoLogHandler是配置了GroupCommit日志方法时的RedoLogHandler。它是RedoLogHandler的一个实现,每个socket都有GroupSyncRedoLogHandler。SegmentedGroupSyncRedoLogHandler的优点在于可以通过维护多个组提交处理程序实现更高的并发。SegmentedGroupSyncRedoLogHandler维护一个GroupSyncRedoLogHandler数组,并将线程绑定到Socket以将线程委托给正确的处理程序。

(八)检查点

与openGauss磁盘存储引擎不同,MOT存储引擎不基于页面存储数据,因此MOT的检查点机制与磁盘引擎的检查点机制完全不同。MOT检查点机制基于CALC(checkpointing asynchronously using logical consistency,使用逻辑一致性异步检查点)算法,该算法出自耶鲁大学Kun Ren等人在数据库顶级会议SIGMOD 2016发表的《Low-Overhead Asynchronous Checkpointing in Main-Memory Database Systems》。 

1. CALC算法

CALC算法的优点如下。

(1) 内存占用少:任意时刻每行最多2个副本。只有当检查点为活动状态时,更具体地说,仅在检查点的一个特定阶段,才会创建第二个副本,从而减少了内存占用。
(2) 开销小:CALC比其他异步检查点算法开销小。
(3) 使用虚拟一致性点:CALC不需要停止数据库就能实现物理一致性。虚拟一致性点是数据库的视图,它反映了在指定时间之前提交的所有修改,而不包含指定时间之后提交的修改,而且在不停止数据库系统的情况下就可以获得。实际上,可以通过部分多版本创建虚拟一致性点。
如图1所示,精确部分多版本的总体思想如下。
(1) 每行都与两个版本相关联,一个是活动版本,一个是稳定版本。通常,稳定版本为空,表明稳定版本与活动版本一致,检查点线程可以安全地记录实时版本。稳定版本仅在检查点的一个特定阶段创建,此时检查点线程将记录该稳定版本。
(2) 每行维护一个稳定状态位,指示稳定行的状态。

 

图17检查点概述

MOT检查点算法在五个状态之间循环,如图18所示。

 

图18  检查点状态机

通常,在进入下一阶段之前,系统要等待所有上一阶段开始提交的事务完成。
1) REST阶段:初始阶段,不进行checkpoint。
(1) 在REST阶段,每行只存储一个活动版本。所有稳定版本都为空,稳定状态位始终为不可用(not available)。
(2) 在此阶段开始提交的任何事务将直接对行的活动版本进行操作,并且不会创建稳定版本。
2) PREPARE阶段:这是虚拟一致性点之前的阶段。当openGauss要求MOT创建快照时,系统从Rest阶段移动到Prepare阶段。
(1) 与Rest阶段类似,每行只存储一个活动版本。所有稳定版本都为空,稳定状态位始终为不可用。
(2) 在此阶段开始提交的任何事务将直接对行的活动版本进行操作,不会创建稳定版本。
3) RESOLVE阶段:该阶段标识出虚拟一致性点。在此时间点之前提交的所有事务都将包含在此检查点中,而随后提交的事务将不包含在检查点中。一旦在Rest阶段开始提交的事务完成,系统将自动从Rest阶段变为Resolve阶段。
(1) 在此阶段不允许任何事务启动提交,以避免这些事务在openGauss占用检查点的重做点前写入重做日志。
(2) 一旦在Rest阶段开始提交的事务完成,MOT将在此阶段获取要包含在此检查点中的任务列表。
4)CAPTURE阶段:在此阶段中,后台工作进程将数据刷入磁盘。Resolve阶段一直持续,直到在准备阶段已开始的所有事务完成并释放其所有锁为止。系统准备任务列表,然后进入Capture阶段。
(1) 在Capture阶段开始的事务已经在一致性点之后开始,因此他们肯定会在一致性点之后完成。因此,除非记录已经具有显式稳定版本,否则总是在更新前将活动版本复制为对应的稳定版本。
(2) 收到BEGIN_CHECKPOINT事件后,系统生成检查点工作进程,扫描所有记录,并将没有显式稳定版本的行,或活动版本的对应的稳定版本刷盘。在此过程中,显式稳定版本一旦刷盘就会被释放。
5) COMPLATE阶段:这是紧跟捕获阶段完成的阶段。检查点捕获完成后,系统进入Complate阶段。事务写入行为恢复为与Rest阶段相同的状态。
与Rest阶段类似,每行只存储一个活动版本。所有稳定版本都为空,稳定状态位始终为不可用(not available)。

一旦在捕获阶段开始的所有事务都完成,系统将转换回Rest阶段,并等待下一个触发检查点的信号。但是,在返回到Rest阶段之前,调用函数SwapAvailableAndNotAvailable翻转稳定状态位。这允许MOT避免只能通过完全扫描来重置稳定状态位,因为在Capture阶段之后,所有稳定状态位都可用,但在Rest阶段开始时,希望所有稳定状态位都不可用。

2. 详细流程

(1) 一旦触发了检查点,Checkpointer后台会触发MOT的CREATE_SNAPSHOT事件。
(2) 当检查点处于Rest阶段时,CheckpointManager将等待在Complete阶段启动的事务完成。
(3) CheckpointManager修改checkpoint阶段为Prepare。如果没有在Rest阶段启动提交的事务处于活动状态,则立即进入Resolve阶段,否则等待Rest阶段启动提交的最后一个事务完成后进入Resolve阶段。
(4) Resolve阶段标记了虚拟一致性点。在此阶段不允许任何事务开始提交,以避免在openGauss采取检查点的重做点之前这些事务写入重做日志。CheckpointManager等待在Prepare阶段启动的事务完成。
(5) CheckpointManager准备要flush的表的列表(任务列表)并读取这些表的锁状态。
(6) 然后获取写锁,锁定redolog handler,并将检查点阶段更改为Capture。这标志着CREATE_SNAPSHOT事件结束。
(7) openGaussCheckpointer获取WalInsertLock锁并计算此检查点的重做点。然后,该重做点触发MOT的SNAPSHOT_READY事件。
(8) CheckpointManager存储重做点,释放redolog handler锁。这标志着SNAPSHOT_READY事件结束。
(9) 然后openGaussCheckpointer释放WalInsertLock并将所有磁盘引擎脏页刷盘,即磁盘引擎的检查点。
(10) 然后触发MOT的BEGIN_CHECKPOINT事件。
(11) CheckpointManager在这个阶段生成检查点worker来完成MOT检查点任务列表。
(12) 检查点worker之间共享任务列表,并将所有符合条件的行刷入磁盘(行的稳定版本或没有显式稳定版本到磁盘的活动版本)。在此过程中任何显式稳定版本一旦刷新到磁盘,就会释放。
(13) 一旦所有检查点worker完成任务,CheckpointManager将解锁表并清除任务列表。
(14) CheckpointManager还可将检查点阶段提前到Complate。
(15) 通过创建map文件、结束文件等来完成检查点,然后更新mot.ctrl文件。
(16) 等待Capture阶段开始的事务完成。
(17) 交换可用位和不可用位,以便将他们映射到稳定状态位中的1和0值。
(18) 修改checkpoint阶段为Rest。这标志着BEGIN_CHECKPOINT事件和MOT检查点的结束。
(19) 然后openGauss将检查点记录插入到XLOG中,flush到硬盘,最后更新控制文件。openGauss中的检查点就此结束。

3. 关键类和数据结构

检查点的关键类和数据结构如表5所示。

表5 检查点的关键类和数据结构简介

关键类和数据结构

描述

CheckpointManager类

理整个检查点机制的主类,是MOT中所有检查点相关任务的接口类。CheckpointWorkerPool类用于生成检查点worker并刷盘。该类不提供真正的接口。实例化对象时会生成检查worker,并使用回调通知任务完成。在Capture阶段CheckpointManager实例化一个CheckpointWorkerPool对象,并简单地等待所有任务完成。CheckpointControlFile用于实现checkpoint控制文件逻辑。MOT控制文件保存着最后一个有效检查点ID、lsn(openGauss中的重做点)和最后一个重放lsn。控制文件mot.ctrl在每个检查点结束时更新

CheckpointWorkerPool类

用于生成检查点worker并刷盘

(九)恢复

恢复部分有两个目的,一是在崩溃或关机后达到最新的一致状态,也称为冷启动(coldstart),二是在HA复制场景中,在备机侧通过重放redo log完成复制。

冷启动时,当所有WAL记录都重放完成后恢复结束;但在HA复制场景中,复制将持续进行,直到备机改变状态。

在恢复过程中,可能存在跨越多个重做日志段的长事务,MOT将其保存在InProcessTransactions映射对象中,直到提交。包含在映射中的数据作为检查点处理过程的一部分进行序列化,并在检查点恢复期间进行反序列化。

此外,在最后恢复阶段,完成所有检查点/WAL记录之后将设置最后的CSN,并将代理键生成器恢复到崩溃或关闭前的最新状态。

为了恢复代理状态,每个恢复线程(检查点和重做日志)都在更新每个线程id的代理最大键数组。最后,这些数组被合并成单个数组,用于恢复最后状态。

1. 详细流程

具体恢复流程如图19所示。

 

图19  恢复时序图

恢复过程如下。
(1) 通过openGauss的StartupXLOG函数调用MOT恢复过程。
(2) 如果存在检查点则从检查点执行第一次恢复。
(3) 读取控制文件,并获取重放LSN。当重启点(备机检查点)存在时,将使用lastReplayLsn作为重放点,而非LSN。
(4) 处理检查点映射文件并生成任务列表。由于MOT检查点仅由行组成,不需要以特定顺序重放,因此这些行的恢复可以并行执行。这就是检查点进程将表拆分成段的原因。
(5) 从元数据文件中恢复所有表的元数据。
(6) 创建检查点恢复线程,每个线程尝试从列表中获取任务,读取与此任务关联的文件并恢复行数据。此恢复是非事务性的。
(7) 如果有进程内事务,也会从检查点恢复。
(8) 检查点恢复完成,返回StartupXLOG,开始重放redo记录。
(9) 当遇到MOT资源管理器(resource manager,rmgr)记录时,将调用MOT引擎的MOTRedo函数,该函数调用恢复管理器ApplyRedoRecord方法。
(10) 在ApplyRedoRecord中,只有当数据的lsn大于恢复的检查点lsn,并且调用ApplyLogSegmentFromData时,才会处理数据。
(11) ApplyLogSegmentFromData从数据中提取LogSegment,分配并插入InProcessTransactions Map。
(12) 当遇到提交记录时,该记录可以是仅MOT事务的LogSegment的一部分,也可以来自MOT注册到openGauss的DDL或跨引擎事务的提交后回调。事务的相关日志段将进行事务性重放和提交。
(13) 上述过程将继续循环,直到StartupXLOG完成。
(14) 调用RecoverDbEnd完成恢复,设置CSN并应用代理状态。

2. 关键类和数据结构

恢复的关键类和数据结构如表6所示。

表6 关键类和数据结构简介

关键类和数据结构

描述

RecoveryManager

实现了IRecoveryManager的API,它是恢复中使用的主类

IRecoveryManager

是一个抽象类,定义了每个RecoveryManager应该实现的接口

SurrogateState

保存了内部数组中每个连接ID的代理键值

LogStats

用于统计,统计信息在LogStats::Entry中按表收集,需在mot.conf中启用

CheckpointRecovery

负责从有效的检查点恢复所有行。它封装了恢复所需的所有操作,包括表的元数据和并行行插入

LogSegment

实际上是重做日志保存的数据段,除了缓冲区外还包括描述操作及其事务ID的元数据

RedoLogTransactionSegment

多个日志段的容器,可以形成一个完整的事务

RedoLogTransactionIterator

一个从XLOG数据中提取LogSegments的helper类

InProcessTransactions

一个独立的Map对象,它保留正在运行的事务LogSegments,在这些事务提交之前都会收集

RecoveryOps

从LogSegment执行DML和DDL操作的实际恢复

 四、小结

本章主要介绍了openGauss的存储引擎,包括磁盘引擎和内容表。在磁盘引擎中,openGauss提供不同存储格式的磁盘引擎来满足不同业务场景对于数据不同的访问和使用模式。内存表针对众核和大内存服务器进行了优化,可提供非常高的事务性工作负载性能。
END
Gauss松鼠会
汇集数据库从业人员及爱好者
互助解决问题 共建数据库技术交流圈
PC端阅读,请点击“阅读原文
文章转载自Gauss松鼠会,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论