内存数据库解析与主流产品对比(二)

做者:实验室小陈 / 大数开放实验室前端

在上一篇文章《内存数据库解析与主流产品对比(一)》中,咱们介绍了基于磁盘的数据库管理系统相关知识,并简述了内存数据库的技术发展。本篇文章将从数据组织和索引的角度来介绍内存数据库的特色,并介绍几款产品实际的技术实现。数据库

— 数据库管理系统中的数据组织编程

定长Block VS 变长Blocksegmentfault

内存数据库在内存中对数据进行管理时,虽然再也不须要经过Slotted Page的形式对数据进行组织,但也不能在内存中任意为数据分配地址空间,依然须要把数据组织成块(Block/Page)的形式。传统基于磁盘的DBMS采用Slotted Page的形式组织数据是为了读写性能的考虑,由于磁盘接口是以Block/Page为读写单位。而内存数据库采用块的方式组织数据是为了便于寻址和管理,一般会将数据块分为定长数据块(Fixed-Length Data Block)和变长数据块(Variable-Length Data Block)两种。后端

假设一个数据集已经所有被加载进内存,为了使用方便,内存数据库在进行数据组织时会把记录的定长的属性所有分出来,放到定长数据块;全部变长的属性保存在另外的变长数据块中。例如,一般将数据表中全部小于8个字节的属性都放在定长数据块中,将变长属性和超过8个字节的属性单独放在变长数据块中,并在定长数据块中放一个指向其地址的指针。采用定长数据块管理数据的好处是寻址快,能够经过记录长度和编号肯定记录在数据块中存储的位置;记录地址指针所须要的空间少,使得索引结构或其余结构中存放这条记录的内存地址最为精简,而且CPU作Pre-Fetch时预测较准。缓存

在传统基于磁盘的DBMS中,索引叶子节点保存的记录地址是Page ID + Offset,Page Table负责将Page ID映射到Buffer的Frame;内存数据库中,索引的叶子节点保存的记录地址则是直接的内存地址。在传统基于磁盘的DBMS中,访问Buffer中的Page时须要对Page进行加锁/解锁/修改锁的操做,因为现实系统中锁(Latch)的类型可能会不少,一个线程若是要访问一个Page,每每要加好几种类型的Latch。如今内存数据库中没有了Buffer,所以就省去了Latch的开销,性能上有很大提高。网络

数据组织:数据分区、多版本、行/列存储数据结构

在多核或多CPU共享内存的系统中,对数据的并发访问冲突是始终存在的。目前的内存数据库系统能够分为Partition SystemNon-Partition System两种。Partition System是把全部的数据切分红互不相交的多个Partition,每个Partition被分配给一个核(或分布式系统中的一个节点),全部操做都是串行执行,没有并发的数据访问,理想状况下能够得到最好的性能。但这类系统的缺点也很明显,例如如何划分Partition以及跨Partition的事务怎么处理等。对于Non-Partition System,全部的核以及全部的线程均可以访问全部的数据,所以必定会存在并发访问冲突,必须采用支持并发访问的数据结构。目前,通用数据库更多的是采用Non-Partition System设计,之因此不采用Partition设计的主要缘由是:通用场景下很难对数据进行有效分区,Partition数据库没法使用。多线程

在Non-Partition System中,若是两个线程访问同一个数据项会发生冲突,这时能够考虑Multi-Version的解决方案。Multi-Version的优点在于能够提升并发程度,其基本的思想是经过多版本的数据让全部的读操做不阻塞写操做,从而提升整个系统的性能。对于那些读多写少的系统,Multi-Version性能会很好,但对于一些Write Heavy的系统,性能并不理想。架构

数据组织还有一个须要考虑的是Row和Column的组织形式。传统数据库系统在磁盘上维护数据时,分为行式存储和列式存储。顾名思义,行式存储是按行存储数据,列式存储是按列存储数据。若是对少许记录的全部属性进行操做,行式存储更加合适,若是只读大量记录的部分列数据,则列式存储性能比较好。好比一条记录有100个属性,本次读操做须要读取全部记录的其中一个属性,若是按行存储,Block读进来后还须要再筛选列;若是按列存储,能够只读取这列数据所对应的Block,因此性能会比较好,适合去作统计分析。但内存数据库不会有这个问题,全部数据都放在内存,不管行存仍是列存,访问的代价是差很少的。因此在内存数据库中,行存/列存是能够作交换或任意选择的。固然对于TP应用而言,更多的仍是用行存,由于能够一次性把全部属性都读出来。但即便是列存,性能也并无在基于磁盘的数据库系统中那么糟糕。好比SAP HANA就是一个行列混合的存储,前端的事务引擎是行存储,经过合并整合之后,后端转为了列存储。

— 内存数据库系统对比

接下来从数据组织的角度,简要介绍一下4个具备表明性的系统:SQL Server的内存数据库引擎Hekaton、慕尼黑工业大学的内存数据库系统HyPer、SAP的HANA、图灵奖得到者Michael Stonebraker的H-Store/VoltDB。

Hekaton

Hekaton是一个Non-Partition的系统,全部线程均可以访问任意数据。Hekaton的并发控制不采用基于锁的协议,而是利用多版本机制实现,每条记录的每一个版本都有开始时间戳和结束时间戳,用于肯定该版本的可见范围。

Hekaton中每一张表最多有8个索引,能够是Hash或者Range索引。同时,全部记录版本在内存中不要求连续存储,能够是非连续存储(No-Clustering),经过指针(Pointer Link)将同一记录的不一样版本关联起来。

上图所示,图中有一个包含姓名、城市和金额字段的表,姓名字段上有一个Hash索引,城市字段上有一个B-Tree索引。黑色箭头表明姓名索引对应的指针,名字John对应的第一条记录,指向下一个具备相同开头字母名字的记录。每条记录包含有开始和结束时间戳,红色表示存在一个事务正在更新记录,事务提交后会替换结束的时间戳。B-Tree索引也是同理,蓝色箭头指针按照城市值串联。

H-Store/VoltDB

H-Store/VoltDB是Partition System,每一个Partition部署在一个节点,每一个节点上的任务串行执行。H-Store/VoltDB没有并发控制,但有简单的锁控制。一个Partition对应一把锁,若是某事务要在一个Partition上执行,须要先拿到这个Partition的锁,才能开始执行。为了解决跨Partition执行问题,H-Store/VoltDB要求Transaction必须同时拿到全部相关Partition的锁才能开始执行,至关于同时锁住全部与事务相关的Partition。

H-Store/VoltDB采用两层架构:上层是Transaction Coordinator,肯定Transaction是否须要跨Partition执行;下层是执行引擎负责数据的存储、索引和事务执行,采用的是单版本的行存结构。

H-Store/VoltDB中的数据块分为定长和变长两类:定长数据块的每条记录长度都相同,索引中采用8字节地址指向每条记录在定长数据块中的位置;变长属性被保存在变长数据块中,在定长数据块的记录中对应一个指针(Non-Inline Data),指向其在变长数据块中具体的位置。在这种数据组织方式下,能够用一个压缩过的Block Look-Up Table来对数据记录进行寻址。

HyPer

HyPer是多版本的Non-Partition System,每一个Transaction能够访问任何数据。同时HyPer是针对于HTAP业务创建的TP和AP混合处理系统。HyPer经过Copy on Write机制实现TP和AP混合处理。假设当前系统正在对数据集作事务处理,此时若是出现AP请求,HyPer会经过操做系统的Fork功能对数据集作Snapshot,随后在快照上面作分析。Copy on Write机制并不会对内存中的全部数据进行复制,只有因OLTP业务致使数据发生变化时,快照才会真正拷贝出原数据,而没有变化的数据则经过虚拟地址引用到相同的物理内存地址。

此外,Hyper采用多版本控制,全部更新都是基于原记录的,每条记录都会维护一个Undo Buffer存储增量更新数据,并经过Version Vector指出当前最新版本。所以,能够经过Transaction找到被修改过的记录,同时能够经过反向应用增量数据来找回修改前的版本,固然也能够对数据版本进行按期融合或恢复等操做。

SAP HANA

SAP HANA是一个Non-Partition的混合存储系统,物理记录在存储介质中会通过三个阶段:1. 事务处理的记录存储在L1-Delta(行存方式);2. 随后记录转化为列式并存储在L2-Delta(列式存储、未排序字典编码);3. SAP HANA的主存是列存(高度压缩并采用排序字典编码)。每条记录经历着从行存到列存的映射合并,至关于一个多版本设计。

— 数据库管理系统中的索引技术—

内存数据库领域在设计索引时,主要是从面向缓存的索引技术(Cache-Awareness)和多核多CPU的并行处理(Multi-Core and Multi-Socket Parallelism)两方面进行考虑。

因为内存数据库再也不有磁盘的I/O限制,所以索引目的转变为加速CPU和内存之间的访问速度。虽然如今内存价格较低,可是内存速度的增速与CPU主频的增速相差依然较多,所以对于内存数据库,索引技术的目的是及时给CPU供数,以尽可能快的速度将所需数据放入CPU的Cache中。

对于多核多CPU的并行处理,80年代就开始考虑若是数据结构和数据都放在内存中,应该如何合理的构造索引。其中,1986年威斯康星大学的MM-DBMS项目提出了自平衡的二叉搜索树T-Tree索引,每一个二叉节点中存储取值范围内的数据记录,同时有两个指针指向它的两个子节点。T-Tree索引结构内存开销较小,由于在80年代内存昂贵,因此主要的度量不在于性能是否最优,而是是否占用最小内存空间。T-Tree的缺点是性能问题,须要按期地作负载平衡,而且扫描和指针也会对其性能产生影响。早期商业系统如Times Ten中,采用的即是T-Tree的数据结构。

那么索引的设计为何须要考虑Cache-Awareness呢?1999年有研究发现内存访问中的Cache Stall或者Cache Miss是内存系统最主要的性能瓶颈。该研究进行了一项性能测试,经过对A/B/C/D 4个系统评测,测试如下过程的时间占比:Computation、Memory Stalls、Branch Mispredicitons和Resource Stalls。Computation表示真正用于计算的时间;Memory Stall是等待内存访问的时间;Branch Mispredicitons是指CPU指令分支预测失败的开销;Resource Stalls是指等待其余资源的时间开源,如网络、磁盘等。

能够看到Memory Stall在不一样的测试场景都会占据较大比例开销。所以对于内存索引结构来讲,发展面向缓存的索引的主要目的就是为了减小Memory Stall的开销。

CSB+-Tree

这里介绍几个典型的内存索引结构例子。第一个是CSB+-Tree,它在逻辑上仍然是B+-Tree,可是作了一些变化。首先每一个Node的大小是一个Cache Line长度的倍数;同时CSB+-Tree将一个节点的全部的子节点组织成Children Group,一个父节点经过一个指针指向它的Children Group,目的是减小数据结构中的指针数量。由于CSB+-Tree的节点与Cache Line长度相匹配,只要依序读取,就能够达到较好的pre-fetche性能。当树分裂时,CSB+-Tree会对内存中的Group从新分配位置,由于CSB+-Tree节点在内存中不须要连续,排好后再建立新的指针连接就能够。

PB+-Trees

另外一个例子是PB+-Trees(Pre-fetching B+-Tree)。它并非新的结构,只是在内存中实现了B+-Tree,每一个节点的大小等于Cache Line的长度倍数。PB+-Trees比较特殊的是在整个系统实现过程当中,引入了Pre-fetching,经过加入一些额外信息帮助系统预取数据。

PB+-Trees倾向于采用扁平的树来组织数据,论文中给出了它Search和Scan的性能,其中Search性能提升1.5倍,Scan上提升了6倍。处理Search时的性能相比CSB+-Tree,PB+-Trees的Data Cache Stalls占比更小。

另一个性能对比是,当没有采用预取时,读取一个Node大小等于两个Cache Line的三级索引须要900个时钟周期,而加了预取后仅须要480个周期。PB+-Trees还有一个实现是,它会在每一个节点加上Jump Pointer Array,用来判断作扫描时要跳过多少Cache Line以预取下一个值。

Bw-Tree

Bw-Tree是Hekaton系统中使用的索引,基本思想是经过Compare-and-Swap指令级原子操做比较内存值,若是新旧值相等就更新,若是不等则不更新。好比原值为20(存储在磁盘),而内存地址对应30,那么要是把30更新成40就不会成功。这是一个原子操做,可用于在多线程编程中实现不被打断的数据交换操做。

Bw-Tree中存在Mapping Table,每个节点都在Mapping Table中有一个存储位置,Mapping Table会存储节点在内存中的地址。对于Bw-Tree来说,从父节点到子节点的指针并非物理指针,而是逻辑指针,也就是记录在Mapping Table中的位置并非真正的内存位置。

Bw-Tree采用的设计是节点的更新并非直接修改节点,而是经过增长Delta Record(增量记录)来保存修改的内容,而后在Mapping Table中指向Delta Record,若是有新的更新就继续指向新的Delta Record。在读取一个节点的内容时,其实是合并全部的Delta Record。由于对Bw-Tree的更新是经过一个原子操做来实现的,发生竞争时只有一个改动能成功,所以是一种Latch-Free结构,只须要靠Compare-and-Swap就可以解决竞争问题,再也不须要依赖锁机制。

Adaptive Radix Tree

Hyper的索引树的设计采用了Adaptive Radix Tree。传统Radix Tree是一个前缀树,其优点是树的深度不依赖于被索引的值的个数,而是靠Search Key的长度来决定。它的缺点是每个节点都要维护可能取值的子节点的信息,致使每一个节点的存储开销较大。

而在Adaptive Radix Tree中,为每一个节点提供了不一样类型长度的格式,分别能够保存4/16/48/256等不一样个数的子节点。Node4为最小的节点类型,最多可存储4个子节点指针, Key用来表示节点所存储的值,指针能够指向叶子节点,也能够指向下一层内部节点。Node16 和Node4 结构上一致,但 Node16 能够存放16个 unsigned char 和16个指针,在存放第17个key时则须要将其扩大为 Node48。Node48结构上和 Node4/16 有所不一样,有256个索引槽和48个指针,这256个索引槽对应 unsigned char 的0-255,每一个索引槽的值对应指针的位置,分别为 1-48,若是某个字节不存在的话,那么它的索引槽的值就是0。当存放第49个key byte 时须要将其扩大为 Node256。Node256结果较为简单,直接存放256个指针,每一个指针对应 unsigned char 的0-255 区间。

好比说在这个例子里,咱们要索引一个整数(+218237439),整数的二进制表示形式为32位,随后将32位bit转换为4个Byte,这4个byte十进制表示为1三、二、九、255,这就是它的Search Key。在索引树中,第一层为Node 4,13符合这一层的存储要求,因而就保留在第一层节点上,后面的位数则进入下一层存储,下一层为Node 48,用来存储2;接下来的每一位都存储到下面的每一层。因为本例子中整数为4个字节表示,故共有4层。能够看到,每一个节点的结构是不同的,根据字节位数和顺序逐一存储,数值在这一层目前有多少个不一样的值,就选择什么类型的节点。若是当前类型的不够用,能够再增长个数,每一个节点能够容纳的 key 是动态变化的,这样既能够节省空间,又能够提升缓存局部性。

另外Adaptive Radix还采用了路径压缩机制,若是一条路径的父节点只有一个子节点就会将之压缩合并。Adaptive Radix之因此采用这样的索引结构,是由于每一个节点的大小都等于一个Cache Line,全部操做能够在一个Cache Line的基础上实现。

OLFIT on B+-Trees

OLFIT on B+-Trees(Optimistic Latch Free Index Access Protocol)是HANAP*Time采用的索引技术,可以在多核数据库上保证CPU的Cache Coherence。在多处理器计算机的体系结构中,多个CPU的Cache会缓存同一内存的数据。在内存数据库中,存储的数据会先读到对应Cache再处理;若是缓存数据处理过程当中内存数据发生变化,那Cache的数据会因与内存数据不一致而失效,Cache Coherence就是解决这个不一样步的问题。

考虑这样一个场景:以下图所示,内存中有一个树形数据结构,由4个CPU处理,每一个CPU有本身的Cache。假设CPU-P1先读了n一、n二、n4,Cache中便有了n一、n二、n4。随后CPU-P2读n一、n2和n5时,假设这个数据结构不是Latch-Free,若是在读的同时且容许修改,就须要一个Latch来在读的时候上锁,读完再释放。由于内存数据库中Latch和数据放在一块儿,数据虽然没有变化,可是Latch的状态发生了改变,计算机的硬件结构会认为内存发生了变化。因此,当多个CPU读一样的数据时,只有最后一次读取状态是有效的,前序的读取会被认为失效。这就会出现即便都是进行读操做,可是由于Latch状态改变致使CPU的Cache失效。所以OLFIT设计了一套机制,要求写操做申请Latch,读操做不须要。OLFIT经过版号维护读写事务,每一个CPU读前先把版本号拷贝到本地寄存器,而后等读完数据后,再判断此时版本号跟读前的是否同样。若是同样就继续正常执行,不同就说明Cache失效。所以,读请求不会引发其余CPU的Cache失效。

经过这个例子能够看到,内存数据库考虑的问题和基于磁盘的数据库是不同的,没有了磁盘I/O的因素,就须要考虑其余方面对性能的限制。

Skiplists

Skiplists是MemSQL的数据处理引擎所用到的技术,它的最底层是一个有序的列表,上层按照必定的几率(P-value)抽取数据,再作一层列表。进行大列表搜索时,从最上层开始一层层递进,相似于二分查找,粒度能够根据实际状况自定义。之因此这样设计是由于全部对列表的插入操做,都是能够经过Compare-and-Swap原子操做完成,从而省去了锁的开销。

— 本文小结—

本文首先介绍了内存数据库的数据组织,分别从数据划分,Partition/Non-Partition的系统差别和存储方式进行介绍,并对比了四款产品的实际实现。随后,介绍了六种内存数据库系统的索引技术,并经过例子简述了索引查询原理。下一篇文章将继续对内存数据库进行剖析,从并发控制、持久化存储和编译查询的角度,讨论内存数据库对于查询性能和可用性的优化设计。

注:本文相关内容参照如下资料:

1. Pavlo, Andrew & Curino, Carlo & Zdonik, Stan. (2012). Skew-aware automatic database partitioning in shared-nothing, parallel OLTP systems. Proceedings of the ACM SIGMOD International Conference on Management of Data. DOI: 10.1145/2213836.2213844. 

2. Kemper, Alfons & Neumann, Thomas. (2011). HyPer: A hybrid OLTP&OLAP main memory database system based on virtual memory snapshots. Proceedings - International Conference on Data Engineering. 195-206. DOI: 10.1109/ICDE.2011.5767867. 

3. Faerber, Frans & Kemper, Alfons & Larson, Per-Åke & Levandoski, Justin & Neumann, Tjomas & Pavlo, Andrew. (2017). Main Memory Database Systems. Foundations and Trends in Databases. 8. 1-130. DOI: 10.1561/1900000058. 

  1. Sikka, Vishal & Färber, Franz & Lehner, Wolfgang & Cha, Sang & Peh, Thomas & Bornhövd, Christof. (2012). Efficient Transaction Processing in SAP HANA Database –The End of a Column Store Myth. DOI: 10.1145/2213836.2213946. 

5. Diaconu, Cristian & Freedman, Craig & Ismert, Erik & Larson, Per-Åke & Mittal, Pravin & Stonecipher, Ryan & Verma, Nitin & Zwilling, Mike. (2013). Hekaton: SQL server's memory-optimized OLTP engine. 1243-1254. DOI: 10.1145/2463676.2463710.


image.png

相关文章
相关标签/搜索