openGauss内存引擎
内存引擎作为在openGauss中与传统基于磁盘的行存储、列存储并存的一种高性能存储引擎,基于全内存态数据存储,为openGauss提供了高吞吐的实时数据处理分析能力及极低的事务处理时延,在不同业务负载场景下可以达到其他引擎事务处理能力的3~10倍。
内存引擎之所以有较强的事务处理能力,并不单是因为其基于内存而非磁盘所带来的性能提升,而更多是因为其全面地利用了内存中可以实现的无锁化的数据及索引结构、高效的数据管控、基于 NUMA 架构的内存管控、优化的数据处理算法及事务管理机制。
值得一提的是,虽然是全内存态存储,但是并不代表着内存引擎中的处理数据会因为系统故障而丢失。相反,内存引擎有着与openGauss的原有机制相兼容的并行持久化、检查点能力,使得内存引擎有着与其他存储引擎相同的容灾能力以及主备副本带来的高可靠能力。
内存引擎总体架构如图13所示。
图13 内存引擎总体架构图
可以看到,内存引擎通过原有的 FDW(Foreign Data Wrapper,外部数据封装器) 扩展能力与 openGauss 的优化执行流程相交互,通过事务机制的回调以及与 openGauss相兼容的 WAL机制,保证了与其他存储引擎在这一体系架构内的共存,保 证了整体对外的一致表现;同时通过维护内部的内存管理结构、无锁化索引、乐观事务 机制来为系统提供极致的事务吞吐能力。
以下将逐步展开讲解相关关键技术点与设计。
(一)内存引擎的兼容性设计
由于数据形态的不同以及底层事务机制的差别,此处如何与一个以段页式为基础的系统对接是内存引擎存在于openGauss中的重点问题之一。
此处openGauss原有的 FDW 机制为内存引擎提供了一个很好的对接接口,优化器可以通过 FDW 来获取内存引擎内部的元信息,内存引擎的内存计算处理机制可以直接通过 FDW 的执行器接口算子实现直接调起,并通过相同的结构将结果以符合执行器预期的方式[比如扫描(Scan)操作的流水线(pipelining)]将结果反馈回执行器进行进一步处理[如排序、分组(Groupby)]后返回给客户端应用。
与此同时内存引擎自身的错误处理机制(ErrorHandling),也可以通过与FDW的交互,提交给上次的系统,以此同步触发上层逻辑的相应错误处理(如回滚事务、线程退出等)。
内存引擎借助 FDW 的方式接近无缝地工作在整个系统架构下,与以磁盘为基础的行、列存储引擎实现共存。
在内存引擎中创建表(CreateTable)的实际操作流程如图14所示。
图14 内存引擎创建表的操作流程图
从图中可以看到,FDW 充当了一个整体交互 API的作用。实现中同时扩展了FDW 的机制,使其具有更完备的交互功能,具体包括:
(1)支持 DDL接口;(2)完整的事务生命周期对接;(3)支持检查点操作;(4)支持持久化 WAL;(5)支持故障恢复(Redo);(6)支持 Vacuum 操作。
借由 FDW 机制,内存引擎可以作为一个与原有openGauss代码框架异构的存储引擎存在于整个体系中。
(二)内存引擎索引
内存引擎的索引结构以及整体的数据组织都是基于 Masstree实现的。其主体结构如图15所示。
图15 内存引擎索引主体结构
图15很好地呈现了内存引擎索引的组织架构。主键索引(primary index)在内存引擎的一个表中是必须存在的要素,因此要求表在组织时尽量存在主键索引;如果不存在,内存引擎也会额外生成代理键(surrogatekey)用于生成主键索引。主键索引指向各个代表各个行记录的行指针(sentinel),由行指针来对行记录数据进行内存地址的记录以及引用。二级索引(secondaryindex)索引后指向一对键值,键的值(value)部分为到对应数据行指针的指针。
Masstree作为并行 B+树(Concurrent B+tree),集成了大量 B+树的优化策略,并在此基础上做了进一步的改良和优化,其大致实现方式如图16所示。
图16 Masstree实现方式
相比于传统的 B树,Masstree实际上是一个类似于诸多 B+树以前缀树(trie)的组织形式堆叠的基数树(radix tree)模式,以键(key)的前缀作为索引,每k 个字节形成一层 B+ 树结构,在每层中处理键中这k 个 字 节 对 应 所 需 的INSERT/LOOKUP/ UPDATE/DELETE流程。图17为k=8时情况。
图17 k等于8时的Masstree
Masstree中的读操作使用了类 OCC(OptimisticConcurrency Control,乐观并发控制)的实现,而所有的更新(update)锁仅为本地锁。在树的结构上,每层的内部节点(interior node)和叶子节点(leaf node)都会带有版本,因此可以借助版本检查(version validation)来避免细粒度锁(fine-grained lock)的使用。
Masstree除了无锁化(lockless)之外,最大的亮点是缓存块(cache line)的高效利用。无锁化本身在一定程度避免了 LOOKUP/INSERT/UPDATE 操作互相失效共享缓存块(invalidat ecacheline)的情况。而基于前缀(prefix)的分层,辅以合适的每层中 B+树扇出(fanout)的设置,可以最大限度地利用 CPU 预取(prefetch)的结果(尤其是在树的深度遍历过程中),减少了与 DRAM 交互所带来的额外时延。
预取在 Masstree的 设 计 中 显 得 尤 为 关 键,尤 其 是 在 Masstree 从 根 节 点 (tree root)向叶子节点遍历,也就是树的下降过程中。此过程中的执行时延大部分由于内存
交互的时延组成,因此预取可以有效地提高遍历(masstreetraverse)操作的执行效率以及缓存块的使用效率(命中)。
(三)内存引擎的并发控制
内存引擎的并发控制机制采用 OCC,在操作数据冲突少的场景下,并发性能很好。
内存引擎的事务周期及并发管控组件结构,如图18所示。
图18 内存引擎的事务周期及并发管控组件结构
这里需要解释一下,内存引擎的数据组织为什么整体是一个接近无锁化的设计。
除去以上提到的 Masstree本身的无锁化机制外,内存引擎的流程机制也进一步最小化了并发冲突的存在。
每个工作线程会将事务处理过程中所有需要读取的记录,复制一份至本地内存,保存在读数据集(read set)中,并在事务的全程基于这些本地数据进行相应计算。相应的运算结果保存在工作线程本地的写数据集(writeset)中。直至事务运行完毕,工作线程会进入尝试提交流程,对读数据集和写数据集进行检查验证(validate)操作并在允许的情况下对写数据集中数据对应的全局版本进行更新。
这样的流程,是把事务流程中对于全局版本的影响缩小到检查验证的过程,而在事务进行其他任何操作的过程中都不会影响到其他的并发事务,并且在仅有的检查验证过程中,所需要的也并不是传统意义上的锁,而仅是记录头部信息中的代表锁的数位(lock bit)。相应的这些考虑,都是为了最小化并发中可能出现的资源争抢以及冲突,并更有效地使用 CPU 缓存。
同时读数据集和写数据集的存在可以良好地支持各个隔离级别,不同隔离级别可以通过在检查验证阶段对读数据集和写数据集进行不同的审查机制来获得。通过检查两个数据集(set)中行记录在全局版本中对应的锁定位(lock bit)以及行头中的TID结构,可以判断自己的读、写与其他事务的冲突情况,进而判断自己在不同隔离级别下是否可以提交(commit)或是终止(abort)。同时由于 Masstree的 Trie节点(node)中存在版本记录,Masstree的结构性改动(insert/delete,插入/删 除)操作会更改相关Trie节点上面的版本号。因此维护一个范围查询(Range query)涉及的节点集(node set),并在检查验证(validation)阶段对其进行对比校验,可以比较容易地在事务提交阶段检查此范围查询所涉及的子集是否有过变化,从而能够检测到幻读(Phantom)的存在,这是一个时间复杂度很低的操作。
(四) 内存引擎的内存管控
由于内存引擎的数据是全内存态的,因此可以按照记录来组织数据,不需要遵从页面的数据组织形式,从而从数据操作的冲突粒度这一点上有着很大优势。摆脱了段页式的限制,不再需要共享缓存区进行缓存以及与磁盘间的交互淘汰,设计上不需要考虑IO 以及磁盘性能的优化[比如索引 B+树的高度以及 HDD(HardDiskDrive,磁盘)对应的随机读写问题],数据读取和运算就可以进行大量的优化和并发改良。
由于是全内存的数据形态,内存资源的管控就显得尤为重要,内存分配机制及实现会在很大程度上影响内存引擎的计算吞吐能力。内存引擎的内存管理主要分为3 层,如图19所示。
图19 内存引擎的内存管理示意图
下面分别对3层设计进行介绍:
(1)第一层为应用消费者层,为内存引擎自身,包含了临时的内存使用以及长期的内存使用(数据存储)。(2)第二层为应用对象资源池层,主要负责为第一层对象,如表、索引、行记录、键值以及行指针提供内存。该层从底层索取大块内存,再进行细粒度的分配。(3)第三层为内存管理层,主要负责与操作系统之间的交互及实际的内存申请。为降低内存申请的调用开销,交互单位一般在2MB 左右。此层同时也有内存预取和预占用的功能。
第三层实际上是非常重要的,主要因为:
(1)内存预取可以非常有效地降低内存分配开销,提高吞吐量。(2)与 NUMA 库进行交互的性能成本非常高,如果直接放在交互层会对性能产生很大影响。
内存引擎对短期与长期的内存使用针对 NUMA 结构适配的角度也是不同的。短期使用,一般为事务或会话(session)本身,那么此时一般需要在处理该会话的 CPU 核对应的 NUMA 节点上获取本地内存,使得交易(transaction)本身的内存使用有着较小的开销;而长期的内存使用,如表、索引、记录的存储,则需要用到 NUMA 概念中类似全局分布(interleaved)内存,并且要尽量将其平均分配在各个 NUMA 节点上,以防止单个 NUMA 节点内存消耗过多所带来的性能下降。
短期的内存使用,也就是 NUMA 角度的本地内存,也有一个很重要的特性,就是这部分内存仅供本事务自身使用(比如复制的读取数据及做出的更新数据),因此也就避免了这部分内存上的并发管控。
(五)内存引擎的持久化
内存引擎基于同步的 WAL机制以及检查点来保证数据的持久化,并且此处通过兼容openGauss的 WAL机制(即 Transaction log,事务日志),在数据持久化的同时,也可以保证数据能够在主备节点之间进行同步,从而提供 RPO=0的高可靠以及较小RTO 的高可用能力。
内存引擎的持久化机制如图20所示。
图20 内存引擎的持久化机制
可以看到,openGauss的 Xlog模块被内存引擎对应的管理器(manager)所调用,持久化日志通过 WAL的写线程(刷新磁盘线程)写至磁盘,同时被 wal_sender(事务日志发送线程)调起发往备机,并在备机 wal_receiver(事务日志接收线程)处接收、落盘与恢复。
内存引擎的检查点也是根据 openGauss自身的检查点机制被调起。openGauss中的检查点机制是通过在做检查点时进行shared_buffer(共享缓冲区)中脏页的刷盘,以及一条特殊检查点日志来实现的。内存引擎由于是全内存存储,没有脏页的概念,因此实现了基于 CALC的检查点机制。
这里主要涉及一个部分多版本(partial multi-versioning)的概念:当一个检查点指令被下发时,使用两个版本来追踪一个记录:活跃(live)版本,也就是该记录的最新版本;稳定(stable)版本,也就是在检查点被下发且形成虚拟一致性点时此记录对应的版本。在一致性点之前提交的事务需要更新活跃和稳定两个版本,而在一致性点之后的事务仅更新活跃版本,保持稳定版本不变。在无检查点状态的时候,实际上稳定版本是空的,代表稳定与活跃版本在此时实际上其值是相同的;仅有在检查点过程中,在一致性点后有事务对记录进行更新时,才需要根据双版本来保证检查点与其他正常事务流程的并行运作。
CALC(CheckpointingAsynchronously using Logical Consistency,逻辑一致性异步检查点)的实现有下面5个阶段:
(1)休息(rest)阶段:这个阶段内,没有检查点的流程,每个记录仅存储活跃版本。(2)准备(prepare)阶段:整个系统触发检查点后,会马上进入这个阶段。在这个阶段中事务对读写的更改,也会更新活跃版本;但是在更新前,如果稳定版本不存在,那么在更新活跃版本前,活跃版本的数据会被存入稳定版本。在此事务的更新结束,在放锁前,会进行检查:如果此时系统仍然处于准备阶段,那么刚刚生成的稳定版本可以被移除;反之,如果整个系统已经脱离准备阶段进入下一阶段,那么稳定版本就会被保留下来。(3)解析(resolve)阶段:在进入准备阶段前发生的所有事务都已提交或回滚后,系统就会进入解析阶段,进入这个阶段也就代表着一个虚拟一致性点已经产生,在此阶段前提交的事务相关的改动都会被反映到此次检查点中。(4)捕获(capture)阶段:在准备阶段所有事务都结束后,系统就会进入捕获阶段。此时后台线程会开始将检查点对应的版本(如果没有稳定版本的记录即则为活跃版本)写入磁盘,并删除稳定版本。(5)完成(complete)阶段:在检查点写入过程结束后,并且捕获阶段中进行的所有事务都结束后,系统进入完成阶段,系统事务的写操作的表现会恢复和休息阶段相同的默认状态。CALC有着以下优点:(1)低内存消耗:每个记录至多在检查点时形成两份数据。在检查点进行中如果该记录稳定版本和活跃版本相同,或在没有检查点的情况下,内存中只会有数据自身的物理存储。(2)较低的实现代价:相对其他内存库检查点机制,对整个系统的影响较小。(3)使用虚拟一致性点:不需要阻断整个数据库的业务以及处理流程来达到物理一致性点,而是通过部分多版本来达到一个虚拟一致性点。
小结
openGauss的整个系统设计是可插拔、自组装的,openGauss通过支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。其中面向 OLTP不同的时延要求,需要的存储引擎技术是不同的。例如在银行的风控场景里,对时延的要求是非常苛刻的,传统的行存储引擎的时 延很难满足业务要求。openGauss除了支持传统行存储引擎外,还支持内存引擎。在 OLAP(联机分析处理) 上openGauss提供了列存储引擎,有极高的压缩比和计算效率。另外一个事务里可以同时包含三种引擎的 DML操作,且可以保证 ACID特性。