eygle.com   eygle.com
eygle.com eygle
eygle.com  
 

« openGauss 数据库列存储引擎 | Blog首页 | openGauss 数据库事务概览 »

openGauss 数据库内存引擎
modb.pro

本文来源于墨天轮:https://www.modb.pro/db/170018

内存引擎作为在openGauss中与传统基于磁盘的行存储、列存储并存的一种高性能存储引擎,基于全内存态数据存储,为openGauss提供了高吞吐的实时数据处理分析能力以及极低的事务处理时延,在不同业务负载场景下可以达到其他引擎事务处理能力的3~10倍不等。

内存引擎之所以有较强的事务处理能力,并不单是因为基于内存而非磁盘带来的性能提升,而更多是因为其全面地利用了内存中可以实现的无锁化的数据及索引结构、高效的数据管控、基于NUMA架构的内存管控、优化的数据处理算法以及事务管理机制。

值得一提的是,虽然是全内存态存储,但是并不代表着内存引擎中的处理数据会因为系统故障而丢失;相反的,内存引擎有着与openGauss原有机制相兼容的并行持久化、checkpoint(检查点)能力,使得内存引擎有着与其他存储引擎相同的容灾能力以及主备副本带来的高可靠能力。

内存引擎总的架构如图所示。

内存引擎总体架构图.png

内存引擎总体架构图

可以看到,内存引擎通过原有的Foreign Data Wrapper(外部数据封装器)扩展能力与openGauss的优化执行流程相交互,通过事务机制的回调以及与openGauss相兼容的WAL机制,保证了与其他存储引擎在这一体系架构内的共存,保证了整体对外的一致表现;同时通过维护内部的内存管理结构、无锁化索引、乐观事务机制来为系统提供极致的事务吞吐能力。

以下将逐步展开讲解相关关键技术点与设计。

 

1. 内存引擎的兼容性设计

由于数据形态的不同以及底层事务机制的差别,此处如何与一个以段页式为基础的系统对接是内存引擎存在于openGauss中的重点问题之一。

此处openGauss原有的FDW(Foreign Data Wrapper)机制为内存引擎提供了一个很好的对接接口,优化器可以通过FDW来获取内存引擎内部的元信息,内存引擎的内存计算处理机制可以直接通过FDW的执行器接口算子实现直接调起、并通过相同的结构将结果以符合执行器预期的方式(比如Scan(扫描)操作的pipelining(流水线))将结果反馈回执行器进行进一步处理后(如排序、Group by(分组))返回给客户端应用。

与此同时内存引擎自身的Error Handling(错误处理机制),也可以通过与FDW的交互,提交给上次系统,以此同步触发上层逻辑的相应错误处理(如回滚事务、线程退出等)。

内存引擎借助FDW的方式接近无缝的工作在整个系统架构下,与以磁盘为基础的行列存储引擎实现共存。

在内存引擎中Create Table(创建表)的实际操作流程如图所示。

内存引擎操作流程图

可以看到FDW充当了一个整体交互API的作用。实现中同时扩展了FDW的机制,使得其具有更完备的交互功能,包括:

(1) 支持DDL接口;

(2) 完整的事务生命周期对接;

(3) 支持checkpoint(检查点);

(4) 支持持久化WAL;

(5) 支持故障恢复(Redo);

(6) 支持Vacuum(垃圾清理回收)。

借由FDW机制,内存引擎可以作为一个与原有openGauss代码框架异构的存储引擎存在于整个体系中。

  2. 内存引擎索引

内存引擎的索引结构以及整体的数据组织都是基于Masstree实现的。主体如图所示。

内存引擎主体结构.png

内存引擎主体结构

Primary Index(主键索引)在内存引擎的一个表中是必须存在的要素,因此要求表在组织时尽量存在primary index;如果不存在,内存引擎也会额外生成surrogate key(代理键)来用于生成Primary index。Primary Index指向各个代表各个行记录的Sentinel(行指针),由Sentinel来对行记录数据进行内存地址的记录以及引用。Secondary Index(二级索引)索引后指向一对键值,键值的value(值)部分为到对应数据Sentinel的指针。

Masstree作为Concurrent B+ tree(并行B+树),集成了大量B+树的优化策略,并在此基础上做了进一步的改良和优化。其大致实现如图所示。

Masstree实现方式

Masstree实现比于传统的B-tree,Masstree实际上是一个类似于诸多B+-树以trie(前缀树)的组织形式堆叠的Radix tree(基数树)模式,以Key(键)的前缀作为索引,每k个字节形成一层B+-树结构,在每层中处理Key(键)中这k个字节对应所需的insert/lookup/update/delete流程。下图为k=8时情况。

k等于8时的Masstree.png

k等于8时的Masstree

注:图来自于"Cache craftiness for fast multicore key-value storage" , Eddie Kohler et. al.

Masstree中的读操作使用了类OCC(Optimistic Concurrency Control,乐观并发控制)的实现,而所有的update(更新)锁仅为本地锁。在树的结构上,每层的interior node(内部节点)和leaf node(叶子节点)都会带有版本,因此可以借助version validation(版本检查)来避免fine-grained lock(细粒度锁)的使用。

Masstree除了lockless(无锁化)之外,最大的亮点是cache line(缓存块)的高效利用。Lockless本身一定程度避免了lookup/insert/update操作互相invalidate共享cache line(失效共享缓存块)的情况。而基于prefix(前缀)的分层,辅以合适的每层中B+-树fanout(扇出)的设置,可以最大程度的利用CPU prefetch(预取)的结果(尤其是在树的深度遍历过程中),减少了与DRAM交互带来的额外时延。

Prefetch(预取)在Masstree的设计中显得尤为关键,尤其是在Masstree从tree root(树根节点)向leaf node(叶子节点)遍历、也就是树的下降过程中。此过程中的执行时延大部分由于内存交互的时延组成,因此prefetch(预取)可以有效地提高masstree traverse(遍历)操作的执行效率以及cache line(缓存块)的使用效率(命中)。

 

3. 内存引擎的并发控制

内存引擎的并发控制机制采用OCC,在操作数据冲突少的场景下,并发性能很好。

内存引擎的事务周期以及并发管控组件结构,如图所示。

内存引擎的事务周期以及并发管控组件结构.png

内存引擎的事务周期以及并发管控组件结构

这里需要解释一下,内存引擎的数据组织为什么整体是一个接近无锁化的设计。

除去以上提到的Masstree本身的无锁化机制外,内存引擎的流程机制也进一步最小化了并发冲突的存在。

每个工作线程会将事务处理过程中所有需要读取的记录,复制一份至本地内存,保存在read-set(读数据集)中,并在事务全程基于这些本地数据进行相应计算。相应的运算结果保存在工作线程本地的write set(写数据集)中。直至事务运行完毕,工作线程会进入尝试提交流程,对read set(读数据集)和write set进行validate(检查验证)操作并在允许的情况下对write set中数据对应的全局版本进行更新。

这样的流程,会把事务流程中对于全局版本的影响,缩小到validation的过程,而在事务进行其他任何操作的过程中都不会影响到其他的并发事务。并且,在仅有的validation(检查验证)过程中,所需要的也并不是传统意义上的锁,而仅是记录头部信息中的代表锁的数位(lock bit)。相应的这些考虑,都是为了最小化并发中可能出现的资源争抢以及冲突,并更有效地使用CPU缓存。

同时read set(读数据集)和write set(写数据集)的存在,可以良好地支持各个隔离级别,不同隔离级别可以通过在validation(检查验证)阶段对read set(读数据集)和write set(写数据集)进行不同的审查机制来获得。通过检查两个set(数据集)中行记录在全局版本中对应的lock bit(锁定位)以及行头中的TID结构,可以判断自己的读、写与其他事务的冲突情况,进而判断自己在不同隔离级别下是否可以commit(提交)、或是需要abort(终止)。同时由于Masstree中Trie node中存在版本记录,Masstree的结构性改动(insert/delete,插入/删除)会更改相关Trie node(节点)上面的版本号。因此维护一个Range query(范围查询)涉及的node set(节点集),并在validation(检查验证)阶段对其进行对比校验,可以比较容易地在事务提交阶段检查此Range query所涉及的子集是否有过变化,从而能够检测到Phantom(幻读)的存在,是一个时间复杂度很低的操作。

 

4. 内存引擎的内存管控

由于内存引擎的数据是全内存态的,因此可以按照记录来组织数据,不需要遵从页面的数据组织形式,从而从数据操作的冲突粒度这一点上有着很大优势。摆脱了段页式的限制,不再需要共享缓存区进行缓存以及与磁盘间的交互淘汰,设计上不需要考虑IO以及磁盘性能的优化(比如索引B+ 树的高度以及HDD(Hard Disk Driv,磁盘)对应的随机读写问题),数据读取和运算就可以进行大量的优化和并发改良。

由于是全内存的数据形态,内存资源的管控就显得尤为重要,内存分配机制及实现会很大程度上影响到内存引擎的计算吞吐能力。内存引擎的内存管理主要分为3层,如图所示。

内存引擎的内存管理示意图.png

内存引擎的内存管理示意图

下面分别对3层设计进行介绍:

(1) 第一层为内存引擎自身,包含了临时的内存使用以及长期的内存使用(数据存储)。

(2) 第二层为对象的内存池,主要负责为第一层对象如表、索引、行记录、Key值、以及Sentinel(行指针)提供内存。该层从底层索取大块内存,再进行细粒度的分配。

(3) 第三层为资源管理层,主要负责与操作系统之间的交互,以及实际的内存申请。为降低内存申请的调用开销,交互单位一般在2 MB左右。此层同时也有内存预取和预占用的功能。

第三层实际上是非常重要的,主要因为:

(1) 内存预取可以非常有效的降低内存分配开销,提高吞吐量。

(2) 与NUMA库进行交互的性能成本非常高,如果直接放在交互层会对性能产生很大影响。

内存引擎对短期与长期的内存使用针对NUMA结构适配的角度也是不同的。短期使用,一般为事务或session(会话)本身,那么此时一般需要在处理该session的CPU核对应的NUMA节点上获取本地内存,使得transaction(交易)本身的内存使用有着较小的开销;而长期的内存使用,如表、索引、记录的存储,则需要NUMA概念中interleaved内存,并且要尽量平均分配在各个NUMA节点上,来防止单个NUMA节点内存消耗过多带来的性能下降。

短期的内存使用,也就是NUMA角度的本地内存,也有一个很重要的特性,就是这部分内存仅供本事务自身使用(比如复制的读取数据以及做出的更新数据),因此也就避免了这部分内存上的并发管控。

 

5. 内存引擎的持久化

内存引擎基于同步的WAL机制以及checkpoint(检查点)来保证数据的持久化,并且此处通过兼容openGauss的WAL机制(即Transaction log,事务日志),在数据持久化的同时,也可以保证数据能够在主备节点之间进行同步,从而提供RPO=0的高可靠以及较小RTO的高可用能力。

内存引擎的持久化机制如图所示。

内存引擎的持久化机制.png

内存引擎的持久化机制

可以看到,openGauss的Xlog模块被内存引擎对应的manager(管理器)所调用,持久化日志通过WAL的writer线程(刷新磁盘线程)写至磁盘,同时被walsender(事务日志发送线程)调起发往备机,并在备机walreceiver(事务日志接收线程)处接收、落盘与恢复。

内存引擎的Checkpoint也是根据openGauss自身的checkpointer机制被调起。

openGauss中的checkpoint机制是通过在做checkpoint时进行shared_buffer(共享缓冲区)中脏页的刷盘,以及一条特殊checkpoint日志来实现的。内存引擎由于是全内存存储,没有脏页的概念,因此实现了基于CALC的Checkpoint机制。

这里主要涉及一个部分多版本(partial multi-versioning)的概念:当一个checkpoint指令被下发,使用两个版本来追踪一个记录:活跃(live)版本,也就是该记录的最新版本;稳定(stable)版本,也就是在checkpoint被下发、形成虚拟一致性点时此记录对应的版本。在一致性点之前提交的事务需要更新活跃(live)和稳定(stable)两个版本,而在一致性点之后的事务仅更新活跃(live)版本本保持stable版本不变。在无checkpoint状态的时候,实际上稳定(stable)版本是空的,代表着stable与live版本在此时实际是相同的值;仅有在checkpoint过程中,在一致性点后有事务对记录进行更新,此时才会需要根据双版本来保证checkpoint与其他正常事务流程的并行运作。

CALC(Checkpointing Asynchronously using Logical Consistency,逻辑一致性异步检查点)的实现有5个阶段:

(1) rest(休息)阶段:这个阶段内,没有checkpoint(检查点)的流程,每个记录仅存储live版本。

(2) prepare(准备)阶段:整个系统触发checkpoint后,会马上进入这个阶段。在这个阶段中事务对读写的更改,也会更新live版本;但是在更新前,如果stable版本不存在,那么在更新live版本前,live版本的数据会被存入stable版本。在此事务的更新结束,在放锁前,会进行检查:如果此时系统仍然处于prepare阶段,那么刚刚生成的stable版本可以被移除;反之,如果整个系统已经脱离prepare阶段进入下一阶段,那么stable版本就会被保留下来。

(3) resolve(解析)阶段:在进入prepare阶段前发生的所有事务都已提交或回滚后,系统就会进入resolve阶段,进入这个阶段也就代表着一个虚拟一致性点已经产生,在此阶段前提交的事务相关的改动都会被反映到此次checkpoint中。

(4) capture(捕获)阶段:在prepare阶段所有事务都结束后,系统就会进入capture阶段。此时后台线程会开始将checkpoint对应的版本(如果没有stable版本的记录即则为live版本)写入磁盘,并删除stable版本。

(5) complete(完成)阶段:在checkpoint写入过程结束后,并且capture阶段中进行的所有事务都结束后,系统进入complete阶段,系统事务的写操作的表现会恢复和rest阶段相同的默认状态。

CALC有着以下优点:

(1) 低内存消耗:每个记录至多在checkpoint时形成两份数据。在checkpoint进行中如果该记录stable版本和live版本相同,或在没有checkpoint的情况下,内存中只会有数据自身的物理存储。

(2) 较低的实现代价:相对其他内存库checkpoint机制,对整个系统的影响较小。

(3) 使用虚拟一致性点:不需要阻断整个数据库的业务以及处理流程来达到一份物理一致性点,而是通过部分多版本来达到一个虚拟一致性点。

  6. 小结

openGauss整个系统设计是可插拔、自组装的, openGauss通过支持多个存储引擎来满足不同场景的业务诉求,目前支持行存储引擎、列存储引擎和内存引擎。其中面向OLTP不同的时延要求,需要的存储引擎技术是不同的。例如在银行的风控场景里,对时延的要求是非常苛刻,传统的行存引擎的时延很难满足业务要求。openGauss除了支持传统行存引擎外还支持内存引擎。在OLAP联机数据分析处理上openGauss提供了列存储引擎,有极高的压缩比和计算效率。另外一个事务里可以同时包含三种引擎的DML操作,且可以保证ACID。

历史上的今天...
    >> 2012-11-19文章:
    >> 2009-11-19文章:
    >> 2007-11-19文章:
           DBA警世录:where条件很重要
           儿子百天了
    >> 2006-11-19文章:
           知音如不赏 归卧故山秋
    >> 2005-11-19文章:
    >> 2004-11-19文章:
           Man Page Of gethrtime
           龙枪编年史

By enmotech on 2021-11-19 10:23 | Comments (0) | | 3438 |


CopyRight © 2004~2020 云和恩墨,成就未来!, All rights reserved.
数据恢复·紧急救援·性能优化 云和恩墨 24x7 热线电话:400-600-8755 业务咨询:010-59007017-7040 or 7037 业务合作: marketing@enmotech.com