openGauss 数据库源码解析系列文章——存储引擎源码解析(五)
上篇图文openGauss数据库源码解析系列文章——存储引擎源码解析(四)中,从内存表的总体架构和代码概述、FDW 进行了分享,本篇将从内存表的存储、索引、事务、并发控制、重做日志、检查点、恢复展开详细介绍。
(三)内存表的存储
Table 类包含管理数据库中内存表所需的所有项。表由以下组件组成:列、主索引和可选的二级索引。关键成员变量说明如表 1Table 类的关键成员变量所示。
表 1 Table 类的关键成员变量
Row 类包含管理表中的内存行所需的所有项,关键成员变量如表 2 所示。
表 2 Row 类的关键成员变量
(四)索引
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 并发控制的关键类和数据结构简介
(七)重做日志
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 重做日志的关键类和数据结构简介
图 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 检查点的关键类和数据结构简介
(九)恢复
恢复部分有两个目的,一是在崩溃或关机后达到最新的一致状态,也称为冷启动(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 关键类和数据结构简介
四、小结
本章主要介绍了 openGauss 的存储引擎,包括磁盘引擎和内容表。在磁盘引擎中,openGauss 提供不同存储格式的磁盘引擎来满足不同业务场景对于数据不同的访问和使用模式。内存表针对众核和大内存服务器进行了优化,可提供非常高的事务性工作负载性能。
评论