openGauss数据库源码解析系列文章——存储引擎源码解析
Posted Gauss松鼠会
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了openGauss数据库源码解析系列文章——存储引擎源码解析相关的知识,希望对你有一定的参考价值。
上一篇详细讲述了“4.2.5 行存储索引机制”、“4.2.6 行存储缓存机制”及“4.2.7 cstore”等精彩内容。本篇我们详细讲述“4.3 内存表”相关内容。
4.3 内存表
MOT(memory-optimized tables,内存表)是事务性、基于行存储的存储引擎,针对众核和大内存服务器进行了优化。MOT是openGauss数据库的一个先进特性,可提供非常高的事务性工作负载性能。MOT完全符合ACID要求,并支持严格的持久性和高可用性。企业用户可以将MOT用于关键任务、性能敏感的在线事务处理应用程序,以实现高性能、高吞吐量、低且可预测的延迟,并提升众核服务器的利用率。
4.3.1 总体架构和代码概述
MOT引擎架构概述如4-43所示。
MOT的关键技术包括下面几个内容。
(1) 面向内存优化的数据结构。
(2) 乐观并发控制。
(3) 无锁索引。
(4) NUMA感知技术,事务性本地内存。
(5) 即时编译(Just-In-Time,JIT)。
总体而言,MOT的目标是建立一个在众核CPU架构中表现出色的OLTP系统,特别是性能可以随核数增加而线性扩展。根据经验实验,Masstree无锁实现和针对Silo(请参阅错误!未找到引用源。)的改进是最佳组合。
索引方面,通过比较各种先进解决方案后选择了Masstree,因为它在点查询、迭代等方面表现出最佳的整体性能。Masstree是Trie和B+树的组合,实现了对缓存、预取和细粒度锁的高效利用。它针对锁冲突严重的情况进行了优化,并在其他先进索引的基础上增加了各种优化。Masstree索引的缺点是内存消耗较大,虽然每行数据消耗的内存大小相同,但是每行每个索引(主索引或次索引)的内存平均高出16个字节:在磁盘表使用的基于锁的B树中为29个字节,而MOT的Masstree为45个字节。
并发控制算法方面,为了从内存架构中获得优势,设计考虑上最大限度地提高OLTP事务处理速度。虽然最近有一些内存多版本并发控制方面的改进,但为了避免迅速的垃圾收集,MOT只维护实际数据。MOT的另一个设计选择是不像HStore那样对数据进行分区,因为在实际的工作负载中事务跨区时性能会急剧下降。尽管已出现一些新的方法通过静态和动态分析来调整并行性,但此类方法会增加时延,并引入额外限制。
内存存储引擎常用的单版本、shared-everything类型的并发控制算法主要分为三类。
- 乐观并发控制(Optimistic Concurrency Control,OCC),乐观并发控制有三个阶段。
- 读取阶段,从共享内存中读取事务记录,并将所有记录写入本地的私有副本。
- 验证阶段,执行一系列事务检查以确保一致性。
- 写阶段,验证成功后,提交该事务;验证失败时将中止该事务,不会提交。两个OCC事务同时执行时不会互相等待。
- 遭遇时间锁定(encounter time locking,ETL)。在ETL中,读取者是乐观的,但写入者会锁定待访问的数据。因此,来自不同ETL事务的写入者会互相看到,并可据此决定是否中止事务。ETL在两个方面提高了OCC的性能。首先,ETL能尽早发现冲突,而事务处理是有代价的,因为在提交时发现的冲突,需要中止至少一个事务,因此ETL可以在冲突情况下提高事务吞吐量。其次,ETL能够高效地处理写后读(reads-after-writes,RAW)。
- 悲观的并发控制(以2PL为例)。在读取或写入时锁定一行,提交后释放锁。这些算法需要一些避免死锁的方法。死锁可以通过周期性的计算等待图(wait-for graph)来检测,也可以通过保持TSO(total store ordering)中的时间顺序或一些其他的规避方案来避免。在2PL算法中,如果一个事务正在写一行数据,其他事务就不能访问或写入该行数据,如果正在读一行数据,则不允许任何事务写该行数据,但可以读取这行数据。
对于大多数工作负载而言,OCC是最快的选择。原因之一是当CPU执行多个交互线程时,一个锁很可能被一个切换出去的线程持有。另外一个原因是悲观的算法涉及死锁检测,这会增大开销,同时读写锁比标准的自旋锁效率低。Silo来自Stephen Tu等人在计算机顶级会议SOSP13上发表的《Speedy Transactions in Multicore In-Memory Databases》,可以在现代众核服务器上实现卓越的性能和可扩展性。MOT最终选择了Silo,因为它比其他现有的方案,如TicToc更简单,同时在大多数工作负载下可保持很高的性能。ETL虽然有时比OCC快,但可能会触发不必要的中止退出。相比之下,OCC只在提交时实际发生冲突时中止退出。
目前,与业界其他领先的内存数据库理系统类似,MOT表的数据容量被限制在最大可用内存范围内。通过操作系统的内存页面交换技术可以扩展内存范围,但在这种情况下性能可能会下降。观察近年来业界出现了几种技术来缓解这个问题,包括数据重组、反缓存和分层等,这也是MOT未来的工作方向之一。
与磁盘引擎(包括共享内存等磁盘数据库的内存优化技术)相比,设计内存引擎的挑战主要是避免磁盘引擎那样基于页面的间接访问方式。
MOT存储引擎代码位于src/gausskernel/storage/mot目录下。目录结构如下。
src/gausskernel/storage/mot/
├── core
├── fdw_adapter
└── jit_exec
MOT文件夹下有三个顶层子目录。
(1) core:包含MOT引擎的核心模块,如并发控制、事务管理、内存管理、存储、检查点、重做、恢复、事务、基础设施组件、统计、实用程序等。
(2) fdw_adapter:包含FDW适配器接口和实现。
(3) jit_exec:包含MOT JIT(Just-In-Time)组件。它有两种实现,一种使用本地LLVM(low level virtual machine),另一种使用TVM(tiny virtual machine),可以在不提供本地LLVM支持的计算机上使用。
4.3.2 FDW
openGauss使用FDW API与内存引擎进行对接。实现上分为两个层次。
(1) 消费者层——FDW API的实现,它由提供数据管理和操作的静态函数组成。这些函数通过fdwapi.h中的FdwRoutine结构以回调的形式暴露给上层。
(2) 通信层——连接openGauss其他部分和MOT内部API。这包括数据和数据定义转换和对MOT内部表示的调整。
1. 消费者层
MOT消费者层FDW API的功能和用途如表4-35所示。其中,计划、执行阶段请参考《SELECT/UPDATE/DELETE(计划阶段)》以及《SELECT/UPDATE/DELETE(执行阶段)》两小节。
函数名 | 使用阶段(数字为调用顺序) | 描述 |
---|---|---|
GetForeignRelSize | 计划1 | 查询过程中表的每个实例均调用,以评估大小 |
GetForeignPaths | 计划2 | 有索引情况下调用,确定哪些索引可用于从当前查询中的表中获取数据 |
GetForeignPlan | 计划3 | 创建用于从表中取数据的执行计划 |
PlanForeignModify | 计划4 | 在数据修改查询时调用。设置数据修改的附加信息 |
AddForeignUpdateTargets | 计划5 | 向查询输出添加其他结果列 |
BeginForeignScan | 执行1 | 在数据提取开始时,对查询中的每个表实例调用 |
BeginForeignModify | 执行1.1 | 修改查询 |
IterateForeignScan | 执行2 | 调用以获取相应的记录 |
ReScanForeignScan | 执行3 | 应重新启动迭代时调用 |
EndForeignScan | 执行4 | 调用以完成取数据 |
EndForeignModify | 执行4.1 | 在通过修改查询启动数据扫描时调用 |
ExecForeignInsert | 执行过程 | 满足其他条件时调用以完成记录插入 |
ExecForeignUpdate | 执行过程 | 满足其他条件时调用以完成记录修改 |
ExecForeignDelete | 执行过程 | 满足其他条件时调用以完成删除记录 |
ExplainForeignScan | 输出计划 | 执行explain时调用以打印详细计划信息 |
AnalyzeForeignTable | 分析1 | 对表做analyze操作 |
AcquireSampleRows | 分析2 | 收集采样信息用来做analyze操作 |
TruncateForeignTable | 截断 | 调用以清空表数据 |
VacuumForeignTable | 垃圾回收 | 清理表 |
NotifyForeignConfigChange | 配置 | 在数据库配置更改事件中调用 |
ValidateTableDef | DDL | 查询数据定义时调用 |
IsForeignRelUpdatable | 信息 | 调用以确定FDW支持的操作(SELECT/INSERT/UPDATE/DELETE) |
GetFdwType | 信息 | 提供FDW类型 |
GetForeignMemSize | 统计信息 | 以字节为单位提供MOT引擎的内存使用情况 |
GetForeignRelationMemSize | 统计信息 | 以字节为单位提供存储表/索引数据的内存使用情况 |
2. 主要流程时序图
请注意,为了便于读者更好的理解正常流程和异常流程的关系,本节中的时序图均将正常流程和异常流程放在同一张图中,其中P1、P2……Pn为异常流程。同时,为简化时序图帮助理解流程,异常流程仅在异常发生的位置进行标识,未完整绘制异常流程的时序。
1) CREATE表
用户创建一个新的内存表时,openGauss通过FDW适配器将请求转发给MOT存储引擎。创建表的正常流程和主要异常流程如图4-44所示。
正常事件流:FDW创建一个新的表对象。然后对每个列执行以下操作。
(1) FDW验证列定义。
(2) MOT引擎进一步验证列定义。
(3) 创建给定类型的列对象并将其添加到表中。
(4) 对所有列定义重复此过程。
添加完所有列后表定义本身就被验证,表对象已添加到MOT引擎,并通过锁保护,最后,由于表还没有索引,所以会向表中添加一个伪主索引/键。DDL命令会持久化到重做日志中。
P1:在此异常事件流中,列定义失败时FDW通过ereport函数向openGauss报告无效列定义(invalid column definition)错误。
P2:在此异常事件流中,由于以下原因之一,导致表的列定义验证失败:①不支持的列类型;②字段大小无效;③资源限制:已超过允许的表最大列数;④资源限制:列的总大小已超过最大元组大小;⑤资源限制:列名大小超过允许的最大值。
P3:在此异常事件流中,由于以下原因之一,导致表的列定义验证失败:①资源限制:超出每个表的最大列数;②资源限制:列的总大小超过最大元组大小;③资源限制:列名大小超过允许的最大值。
P4:总元组大小超过了允许的最大元组大小。
2) DELETE表
如图4-45所示,用户DELETE内存表时,openGauss通过FDW适配器将请求转发给MOT存储引擎。
正常事件流:FDW从MOT引擎中检索表对象,并将DELETE表的请求转发给MOT引擎。DDL命令在重做日志中持久化,然后对于表中的每个索引,索引数据将被截断并DELETE索引对象。随后对表中的每个索引重复此过程。在DELETE所有索引对象之后,MOT将DELETE表对象并返回给FDW。
P1:在此异常事件流中,没有找到索引所属的表。此错误条件被静默忽略,FDW不会向openGauss报告错误。
P2:在此异常事件流中,在表对象中找不到请求的二级索引。此错误条件被静默忽略,FDW不会向openGauss报告错误。
3) CREATE索引
如图4-46所示,用户希望在现有的内存表中创建新索引时,openGauss通过FDW适配器将请求转发给MOT存储引擎。
正常事件流:FDW从MOT引擎中检索表对象并创建一个索引对象。然后,对每个列执行以下操作:①FDW验证列大小;②FDW验证列类型。对所有列定义重复此过程。验证所有列之后,生成的键大小也会被验证。在创建主索引时,原创建表阶段时添加的伪主索引将被新的主索引替换,应当在表仍然为空时完成。否则,将向表添加二级索引。索引数据本身是由主索引数据创建的。最后,整个DDL命令将持久化到重做日志。
P1:在此异常事件流中,不支持索引类型,FDW通过ereport工具向openGauss报告未支持的特性(feature unsupported)错误。目前只支持BTREE索引类型。
P2:在此异常事件流中,列大小验证失败,FDW通过ereport实用程序向openGauss报告无效列定义错误。
P3:在此异常事件流中,列类型验证失败,FDW通过ereport实用程序向openGauss报告未支持的特性(feature unsupported)错误。
P4:在此异常事件流中,索引的总键大小超过了最大允许的键大小,FDW通过ereport工具向openGauss报告无效列定义错误。
P5:在此异常事件流中,由于资源限制(内存不足),无法执行操作。
P6:在此异常事件流中,由于以下原因之一无法执行操作:①资源限制(内存不足),无法执行操作,②唯一主键冲突。
4) DELETE索引
如图4-47所示,用户希望DELETE内存表中的现有索引时,openGauss通过FDW适配器将请求转发给MOT存储引擎。
正常事件流:FDW从MOT引擎中检索表对象,并转发从表中DELETE二级索引的请求。DDL命令在重做日志中持久化,然后截断索引数据并DELETE索引对象。
P1:在此异常事件流中,没有找到索引所属的表。此错误条件被静默忽略,FDW不会向openGauss报告错误。
P2:在此异常事件流中,在表对象中找不到请求的二级索引。此错误条件被静默忽略,FDW不会向openGauss报告错误。
5) 截断表
如图4-48所示,用户截断现有的内存表内容时,openGauss通过FDW适配器将请求转发给MOT存储引擎。
正常事件流:FDW从MOT引擎中检索表对象并转发截断表的请求。表中每个索引的索引数据被截断,并且将DDL命令持久化到重做日志。
P1:在此异常事件流中,没有找到该表。此错误条件被静默忽略,FDW不会向openGauss报告错误。
P2:在此异常事件流中,由于资源限制(内存不足),无法执行操作。
6) INSERT行
如图4-49所示,openGauss通过FDW适配器将请求转发给MOT存储引擎。可以通过自动提交(auto-commit)INSERT行,也可以在事务中INSERT行。
正常事件流:FDW从MOT引擎中检索表对象并创建新的行对象。由于内存引擎不同于磁盘引擎,不使用基于页面的间接访问形式,因此需要将行格式从openGauss行格式转换为MOT行格式(MOT将这种行格式转换称为Pack,反向转换称为unpack)后才能INSERT到表中。随后为该表的每个索引创建一个键。INSERT行的整个请求被传递到当前Txn,随后将该请求转发到并发控制模块,并持久化到重做日志。
P1:在此异常事件流中,由于资源限制(内存不足),无法执行操作。
P2:在此异常事件流中, INSERT行失败,原因如下:①内存分配失败;②在主节点上违反了唯一约束。在这两种情况下,父事务都将使用正确的错误代码中止。
7) SELECT/UPDATE/DELETE(计划阶段)
如图4-51所示,计划阶段完成后,执行阶段开始。
正常事件流:openGauss调用BeginForeignScan,FDW检索相关表并初始化查询的FDW状态。在进行UPDATE/DELETE操作时,openGauss通过调用BeginForeignModify接口触发一个额外的初始化阶段,然后返回NULL。openGauss通过调用IterateForeignScan接口进行如下操作:①仅在需要时一次性初始化游标;②在当前事务对象中查找下一行;③将行数据从MOT格式转换为openGauss格式;④游标前进;⑤返回包含unpack行的槽位。重复此过程,直到游标中不再有行,并且返回NULL到openGauss。然后openGauss应用本地条件/查询子句等本地过滤器来决定是否继续处理该行。在进行SELECT操作时,该行将被添加到结果集中,并返回结果集给用户。进行UPDATE和DELETE操作需执行的其余部分将在后文中进行描述。
9)UPDATE(结束执行阶段)
执行SELECT、UPDATE和DELETE语句的公共部分后,每个语句的剩余部分有所不同。图4-52描述UPDATE的剩余部分。
正常事件流:openGauss为特定的更新元组调用ExecForeignUpdate接口。FDW更新当前事务对象中最后一行的状态以进行并发控制,然后FDW将行数据从openGauss格式转换为MOT格式,并通过覆盖该行的方式完成变更字段的更新。该操作在重做日志中持久化,并返回openGauss。
P1:在此异常事件流中,由于并发控制事务对象的行状态更新失败。在这种情况下,父事务将以适当的错误代码中止。
10) DELETE行(结束执行阶段)
图4-53描述了DELETE操作执行的剩余部分。
正常事件流:openGauss为特定更新的元组调用ExecForeignDelete接口。FDW更新当前事务对象中最后一行的状态以进行并发控制,然后将操作持久化到重做日志中,并返回openGauss。
P1:在此异常事件流中,由于并发控制事务对象的行状态更新失败。在这种情况下,父事务将以适当的错误代码中止。
4.3.3 内存表的存储
Table类包含管理数据库中内存表所需的所有项。表由以下组件组成:列、主索引和可选的二级索引。关键成员变量说明如表4-36 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类包含管理表中的内存行所需的所有项,关键成员变量如表4-37所示。
成员变量 | 描述 |
m_data : uint8_t | 保存行数据的原始缓冲区,开始于类的结束位置 |
m_keyType : KeyType | 使用的键类型。
|
m_pSentinel : Sentinel* | 指向主哨兵的指针 |
m_rowHeader : RowHeader | OCC行的头部,包含OCC操作的所有相关信息 |
m_rowId : uint64_t | 创建行时生成的唯一rowId |
m_table : Table* | 指向内存管理表的指针 |
4.4.4 索引
MOT使用索引来高效地访问数据。MOT索引支持范围查询等所有基本操作。由于数据存储在Row类中,每个MOT索引都按顺序使用哨兵来访问数据。
IndexFactory类提供了创建新索引对象的能力。
作为Table类的一部分,Index抽象类提供了创建和访问数据索引的能力。索引是否满足唯一性决定了该索引是否允许插入重复键。如图4-54所示,描述了一个有三行和两个索引的MOT表T的结构,其中一个索引是非唯一索引,另一个索引是唯一索引。对于非唯一索引而言,MOT内部通过在插入时用唯一标识符填充每个键的方式将键视为唯一。在图4-54中,MOT将哨兵插入到带有键的唯一索引和带有键+后缀的非唯一索引中。使用哨兵方便了维护操作,因为在进行维护操作时,可以在不接触索引数据结构的情况下替换行。
Sentinel类包含指向唯一索引情况下的行数据或非唯一索引情况下主哨兵的指针,还包含一些标志位和引用计数等支持跨事务并发的信息。每次向索引插入新键时都会创建哨兵。例如,对于具有3个索引的表,插入新键时将创建3个哨兵,每个索引对应一个哨兵。哨兵和行之间的关系如图0-所示。
MasstreePrimaryIndex类实现了索引接口。它基于Masstree K/V存储实现,同时封装了MOT内存分配池,根据对象分配任意大小内存。
IndexIterator抽象类提供了创建迭代器并根据提供的迭代器访问数据的能力。
4.3.5 事务
事务部分覆盖了从openGauss映射到MOT的所有支持的DDL/DML操作。
事务与并发控制机制紧密耦合,每个操作都必须通过并发控制管理,并完成相应的行为。
MOT基于乐观并发机制,几乎不使用锁,因此每个客户端都有自己的事务视图,并且不会阻塞DML,与磁盘表对每个非SELECT操作都加锁的使用方式有显著区别。
每个局部行都有一个初始状态,状态由txn_state_machine管理。txn_state_machine扩展了Silo,支持新操作写后读和读后写,类似于MESI缓存一致性协议。如图4-56所示,MOT将新操作(RD/WR)视为本地缓存中的缓存不命中,并将状态从无效提升为新状态。
详细流程
SELECT具体流程如图4-57所示。
(1) 当SELECT操作被发送到FDW,FDW就会打开一个游标并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效则在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估隔离级别和来自缓存的结果:如果TxnAccess返回了一行,直接将其返回给openGauss;否则以下下两种情况。
① 隔离级别为READ_COMMITED时生成行的副本并返回给FDW。
② 隔离级别为REPEATABLE_READ时映射缓存中的行,并将缓存的行返回给FDW。
UPDATE具体流程如图4 58所示。
(1) 当UPDATE操作被发送到FDW,FDW就会打开一个游标,并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效就在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估来自缓存的结果。
① 如果TxnAccess返回了一行,直接将其返回openGauss。
② 如果没有找到该行,则映射哨兵并返回缓存的行。
(5) openGauss计算返回的行,如果该行与筛选器匹配则openGauss向FDW发送带有更新数据的更新操作。
(6) TxnManager将行的状态提升为WR,并用从openGauss接收的新数据更新本地行。
DELETE具体流程如图4-59所示。
(1) 当DELETE操作被发送到FDW,FDW就会打开一个游标并将正确的哨兵发送到事务管理器。
(2) 事务管理器检查哨兵,如果哨兵有效就在缓存中搜索,否则返回未找到该行。
(3) TxnAccess在内部查找哨兵,如果在高速缓存中找到该行则返回该行,并认为是高速缓存命中。
(4) TxnManager评估来自缓存的结果。
① 如果TxnAccess返回了一行,直接将其返回openGauss。
② 如果没有找到该行,则映射哨兵并返回缓存的行。
(5) openGauss计算返回的行,如果该行与筛选器匹配,则openGauss向FDW发送带有更新数据的删除操作。
(6) TxnManager将行的状态提升为DEL,并将本地行标记为已删除。
INSERT具体流程如图4-60所示。
(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用于保存行插入请求的数据。
4.3.6 并发控制
MOT采用源自SILO的单版本并发控制(concurrency control,CC)算法,是一种OCC算法。并发控制模块满足内存引擎的所有事务性需求,其主要设计目标是为MOT内存引擎提供各种隔离级别的支持。当前支持如下隔离级别。
(1) 读已提交(READ-COMMITED)。
(2) 可重复读(REPEATABLE-READ)。
图4-61显示了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)事务流程
①在索引中搜索行引用。
② 将数据免锁复制到基于类型的本地集,包括读写集(Read/Write Set, R/W set)。
③ 基于本地副本进行处理。
(2)校验流程
① 按主键顺序对写集(Write Set)进行排序。
② 锁定写集中的所有行。
③ 验证读写集的行。
④ 验证本地行CSN是否更改。
⑤ 验证该行是否为该键的最新版本(由于存在本地数据,可能并非最新)。
⑥ 验证该行未被其他事务锁定。
⑦ 如果以上任一项验证失败,则中止事务。
⑧ 否则将更新CSN后的所有写集中的行复制回去,然后释放这些行上的锁。
2) 插入流程
(1)事务流程
① 构造一个CSN=0且状态为不存在的新行r。
添加r到写集并视为常规更新。
生成唯一的键k。
② 在状态为不存在的情况下,向树/索引添加从k → r的映射。
如果k已经映射到一个状态为存在的记录,则插入失败。
否则在读阶段增大版本号。
(2)校验流程
① 锁定写集。
② 验证插入集(insert set)。
③ 若事务中止,则垃圾回收器记录状态为不存在的行。
3) 删除流程
(1)事务流程
① 在索引中搜索行引用。
② 将行映射到本地缓存。
③ 将本地副本标记为已删除。
(2)校验流程
① 验证行保持不变;已删除的行将被视为更新。
② 从索引中删除行,即将已删除的哨兵/行放入垃圾回收器中。
5. 关键类和数据结构
并发控制的关键类和数据结构如表4-38所示。
关键类和数据结构 | 描述 |
OccTransactionManager类 | 管理整个事务验证机制,与事务类紧耦合 |
RowHeader类 | 每一行的OCC元数据。头部为64位,包含状态位和CSN |
4.3.7 重做日志
MOT重做日志(Redo Log)使用预写式日志(write-ahead logging,WAL)技术来确保数据完整性。WAL的核心概念是,内存中的数据和索引的更改只有在记录下这些更改之后才会发生。因此写入重做日志是MOT提交协议的一部分。
如图4-63所示,MOT存储引擎的重做日志模块同样使用openGauss磁盘引擎的日志接口进行持久化和恢复。这意味着MOT重做数据被写入相同的XLOG文件,并使用相同的XLOG逻辑。使用与openGauss磁盘引擎相同的日志记录接口可确保跨引擎事务的一致性,并减少复制、日志恢复等模块的冗余实现。
1. 事务日志记录
与openGauss其他存储引擎不同,MOT内存引擎仅在事务实际提交时才会写入重做日志。因此,在事务期间或事务中止时,数据不会写入重做日志。这样可以减少写入的数据量,从而减少不必要的磁盘IO调用,因为这种磁盘IO调用很慢。例如,如果在事务期间多次更新同一行,则只将表示已提交行的最终状态写入日志。
由于设计MOT内存引擎时考虑了对接不同的数据库的可能性,因此如图4-64所示,MOT通过抽象的ILogger接口对接重做日志。
2. 日志类型
设计MOT内存引擎时同样考虑了支持不同的日志记录方式。如图4-65所示,MOT当前已实现同步日志(synchronous redo Log)和同步组日志(group synchronous redo log)。这是通过RedoLogHandler类实现的。RedoLogHandler封装了日志逻辑,在数据库启动时初始化。RedoLogHandler可以根据需要扩展实现新的日志记录方式。
每个事务管理器对象(TxnManager)都包含一个Redolog类,该类负责在提交时将事务数据序列化到缓冲区中。如图4-66所示,该缓冲区被传输到RedologHandler以进行日志记录。
1) 同步日志记录
同步日志使用SynchronousRedoLogHandler。如图4-67所示,这是一个简单的RedoLogHandler实现,它只将序列化缓冲区委托给ILogger(XLOGLogger),以便将其写入XLOG。因为在写缓冲区时,事务被阻塞,所以称为同步。只有当所有事务数据被序列化并写入日志时,提交协议才会继续。
2) 同步组提交日志记录
同步组提交日志由SegmentedGroupSyncRedoLogHandler类实现。它通过将几个事务分组到一个写块(write block)中并一起写入的方式优化日志记录。这种方法在一次调用中收集更多数据,可以最大限度地减少磁盘IO次数。除此之外, SegmentedGroupSyncRedoLogHandler将每个NUMA处理器(socket)的事务分组,以减少跨NUMA处理器的数据传输,因为跨NUMA处理器的数据访问比同一NUMA处理器本地内存访问慢。
当事务提交时,它将数据序列化到缓冲区中,这个缓冲区被传输到SegmentedGroupSyncRedoLogHandler,并放入一个提交组中。提交组(Commit Group)是一组序列化事务缓冲区的集合,这些事务缓冲区将被提交并写入磁盘。根据不同的配置参数,当一个组被填满或超过预先配置的时间时,MOT将关闭该组,并将该组内所有缓冲区一起写入日志。
图4-68描述了将多个事务分组一起写入的组提交逻辑。
3) 异步日志
MOT暂未开发专用的异步日志机制,异步日志是通过在conf配置文件中将synchronization_commit参数设置为“off”来实现的。
3. 关键类和数据结构
重做日志的关键类和数据结构如表4-39所示。
关键类和数据结构 | 描述 |
---|---|
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处理。AddToLog为XLOGLogger子接口,XLOGLogger使用openGauss日志基础能力。因此,AddToLog是一个对openGauss XLOG接口的简单委托 |
图4-69所示代码为XLOGLogger::AddToLog接口的实际实现。
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以将线程委托给正确的处理程序。
4.3.8 检查点
与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不需要停止数据库就能实现物理一致性。虚拟一致性点是数据库的视图,它反映了在指定
以上是关于openGauss数据库源码解析系列文章——存储引擎源码解析的主要内容,如果未能解决你的问题,请参考以下文章
openGauss数据库源码解析系列文章——存储引擎源码解析
openGauss数据库源码解析系列文章——存储引擎源码解析
openGauss数据库源码解析系列文章——公共组件源码解析(上)