翻译的草稿,暂时保存,还有很多错误和不理解的地方,以后再修改吧。
CPU and Cache Efficient Management of Memory-Resident Databases
内存数据库的CPU和缓存之高效管理
摘要:内存数据库管理系统(MRDBMS)必须针对CPU周期和内存带宽两类资源进行优化。部分(混合)分解存储模型(PDSM)的提出就是为优化内存带宽,为了满足OLTP和OLAP业务场景的需要。然而,从当前部分分解模型的实现来看,节省带宽的同时却增加了CPU的成本。为了节省带宽同时又不增加CPU负担,我们消除了导致CPU效率低下的函数调用,提出了部分分解存储与查询的即时(JiT)编译相结合的方案。因为现有的基于成本的优化组件不是专门为jit编译查询执行而设计的,所以我们开发了一种新颖的方法来对查询成本和后续存储布局的优化进行建模。实验评估显示,jit的处理器保持了先前混合查询处理器所节省的带宽,并且由于提高了CPU效率,优于他们两个数量级。
一、 简介
RAM容量的增加以及成本的降低,使内存数据库管理系统(MRDBMS)成了磁盘数据库的理想的替代方案[30]。RAM优越的低延迟特性和带宽优势促进了在线分析处理(OLAP)和在线事务处理(OLTP)等许多数据库应用的发展。但是,为大多数磁盘数据库而设计的Volcano-style 处理模型[14],却不再适合这样的快速存储设备。为了能够处理任意长度的元组运算, Volcano-style查询处理器通过使用执行时捕获的函数指针来“配置”运算。尽管这里的函数指针捕捉机制会导致CPU效率极其低下[2],[6],但是,这在磁盘数据库管理系统里还是可以接受的,因为磁盘I/O的成本远远超过了函数调用的成本。由于Volcano-style的CPU效率低下,直接应用到快速存储设备上往往达到满足预期的高性能。 MRDBMS性能的关键指标除了I/O(缓存)效率之外还得考虑CPU效率(见图1)。
为了提高MRDBMS的CPU效率,文献【23】提出了Bulk处理模型。使用这个模型,数据按照一次一列的方式处理,提高了CPU效率。但是缓存效率,只是针对那些根据DSM模型存储的数据才有所提高【12】。由于重构元组时cache效率低下,DSM只具有良好的OLAP性能,但是不具有OLTP性能。为兼顾OLTP和OLAP两种工作负载都具有良好的性能,文献[15]提出了一种混合的存储模型,更准确地说,应该叫部分分解存储模型(PDSM)。在当前实现里,即使是按照一次一分区的模式来处理数据的,一个分区内也还是不能避免处理任意宽度的元组。就像Volcano-style里一样,他们使用函数指针[15]来实现对应的操作。所以,PDSM 处理器虽然比DSM产生更好的缓存效率,但CPU效率依然是很低的 (图1)。
为了解决CPU和缓存效率之间的矛盾,我们提出,在查询处理过程中消除函数指针调用,把PDSM模型和即时(JiT)编译结合起来使用。具体来说,我们做出以下贡献:
l 在Hyper中设计实现了一个基于PDSM的存储组件,Hyper是我们开发的一个基于JIT的数据库管理系统。
l 引入了一种新的基于JIT编译的查询成本估算方法,以及一种后续的存储布局优化方法。该方法将通用的成本模型看成是“可编程”的机器,通过合适的指令集,处理整体查询成本的估计。
l 根据现有的基准进行了广泛的实验,并和以前的系统进行结果比较。
本文的其余部分组织如下: 第二节我们介绍内存数据库CPU和缓存的效率的相关研究工作。在第三节,我们分析CPU和缓存效率的矛盾带来的影响,描述我们在Hyper系统中是如何通过JIT解决它的 [21]。第四节里我们描述了成本模型,并介绍在布局优化中如何使用它。第六节,介绍我们的实验相关工作。第七节是结论部分。
二、 相关研究
在说明我们的方法之前,先介绍一下内存数据库的CPU和缓存的效率相关的研究工作,看看以前的方法所遇到问题和必须考虑权衡的细节。
A、CPU高效处理
在volcano模型中,关系查询计划是基于灵活的运算(算子)构建的。在构建物理查询计划时,通过注入函数指针(选择谓词,聚合函数,等等)把这些运算(算子)“配置”和连接在一起。尽管该模型有很多种变换形式,但都有一个基本的问题: 从CPU的执行的角度来看,运算符可以在运行时改变自己的行为,而这是不可预测的。这就麻烦了,因为许多现代CPU和编译器的性能优化都必须依赖于可预测的行为[19]。不可预测的行为肯定会影响优化,引发管道阻塞、本地指令缓存不足和指令级并发受限等一些问题[2]。因此,类似 Volcano-style这样的处理方式,运算很灵活但是CPU效率通常很低。
为了提高MRDBMS数据库的CPU效率,研究人员提出了一系列技术[5,35,20,17]。最突出的是Bulk-processing处理和A-priory查询编译。前者是主要面向OLAP ,而后者主要针对OLTP应用。从下面的分析不难看出,在处理混合工作负载的时候两者都有缺点。
l Bulk-processing处理
以MonetDB项目为代表,侧重于分析型应用[23],[5]。像Volcano模型中那样,复杂的查询首先分解为预编译原语。然而,Bulk处理原语是没有函数调用的静态循环,避免了中间结果的物化[5]。对分析型应用来说,提高CPU的效率要比节省物化成本更重要。为了减少物化成本,文献【35】提出了矢量化查询处理模型,把物化操作限制到了CPU缓存。然而,由于原语缺乏灵活性,批量处理只能适合于完全分解的关系,显然,这在处理OLTP应用时又受制于缓存的局部性。虽然使用元组聚类,压缩和银行包装[20]等方法可以像批处理那样求解多个属性上的选择操作。但是压缩会使得更新操作性能降低,而且解压缩也会增加元组重构的成本。
l A-priory查询编译
此方法出现在VoltDB[17]之类的系统中,支持系统在任何存储模型上进行高性能的事务处理。它把查询静态编译成机器代码和内联函数,从而避免函数调用,提高CPU的效率。这种处理模型支持SQL来实现查询公式化,但是无论何时,只要查询被修改或添加,就需要重新组装和重启系统。它还使复杂查询的优化问题更加复杂,因为所有的查询计划必须在没有查询相关的数据或参数下生成。这两个因素使得它不适合复杂的或即席查询的OLAP类应用。
B. Cache 高效存储
通过上述这些技术,虽然通消除了函数调用的开销,但是内存带宽依然是一个瓶颈[5]。减少带宽消耗的一个方法是各种形态的压缩方法 [33](字典压缩,行程编码,等等)。不过,这已经超出了本文的关注范围。在本文中,我们研究的是通过(部分)分解数据库表后,各属性之间的非最优协同定位的来减少带宽浪费。
完全的分解,比如使用文献[12]提出的DSM存储模型,能够为磁盘数据库上的分析型应用显著的节约带宽。但是,完全分解对元组内(intra-tuple)的缓存本地化产生负面影响,进而影响事务处理的性能[1]。因此,它对于OLTP和OLAP混合应用的工作负载不是最优的。为提高此类应用的缓存效率,文献[15]提出了一种混合的存储模型,更准确地说,应该叫部分分解存储模型(PDSM)。在这个模型中,数据库模式分解为分区,使得一个给定的工作负载从对应的分区可以获得最优的支持。各种OLTP和OLAP相混合的应用都可以受益于这种技术,比如事务数据的实时报表,无索引搜索,或复杂表的模式管理。这类复杂的表,一个元组可能对应几个实体实物。比如使用对象关系映射技术(ORM)把复杂的类层次结构映射到关系时得到的表。
三、 基于CPU和Cache的高效数据管理
根据定义,数据的部分分解至少部分包括一个n-元分区(否则我们会称它为完全分解)。如上所述,MRDBMS需要灵活运算来一次扫描处理任意宽度的元组。为了达到这种灵活性,当前查询处理器[15]的借助函数指针来实现,但是却导致CPU效率低下。在这节中,我们首先通过实际的例子加以证实。然后,我们描述如何使用JiT编译来克服这个问题。
A. The Impact of Storage and Processing Model
为了评估存储和处理模型带来的影响,我们用C语言设计实现了一个典型select-and-aggregate查询(图2),根据不同的处理和存储策略,尽可能的实现这些查询,并测量对比了选择谓词的选择率发生变化时所花费的时间 (图3)。
在Bulk实现方式中,第一个运算是扫描列A并物化所有匹配的目标位置。然后扫描B,E并物化所有的匹配位置。最后,针对每一个物化的缓冲区做聚合运算。这种方式下,在高选择率时CPU效率较高,但缓存效率较低。在Volcano-style实现中,由三个函数(扫描、选择、聚合) 依次重复调用底层函数,来产生上层函数的输入。Volcano模型产生的性能(独立于存储模型)表明,它确实不适合常驻内存数据库中的这类查询。图2c中显示的是根据Hyper中的编辑模型实现的JiT编译查询在PDSM数据模型上执行的版本。
Bulk-和JiT-两种处理方式在选择率上的优势和文献【32】中研究的结论一致。图中还可以看出,基于部分分解数据模型实现的JiT编译查询方法优于其他方法。这个结果说明:
即时编译查询(JiT-compiled queries)对内存数据库来说是一项重要的技术,在n元分解或部分分解数据模型(PDSM)下,它可以支持高效的查询处理。
接下来,我们研究在关系数据库管理系统(DBMS)中,如何组合并发挥PDSM和JiT编译查询的优势。
B. Partially Decomposed Storage in HyPer
HyPer是我们的一个MRDBMS研究原型。为了在CPU 效率上和Bulk进行对比,HyPer依赖JiT编译查询【27】。DBMS编译器的研究由来已久[3],[8],最近[27],[24],[31]的工作更多关注的是灵活性和可扩展性,而仅仅是性能。这里的核心思想是直接生成可以在目标主机CPU上执行的代码。这样就可以消除函数调用的开销,从而实现代码高度优化和CPU高效的执行。
代码生成过程不属于本文的讨论的范围,在文献[27]中有详细描述。为了说明问题,图2形象的展示了图2a中的关系代数查询计划转换为C99代码的过程。该程序在基于PDSM的关系R上求解给定的查询。目标关系R、输出缓冲区sums和选择条件C都是该函数的输入参数。在这个例子中,每一个运算对应一行代码(四个聚合操作使用固有向量类型v4si在一行执行)。通过一趟循环扫描枚举所有元组的id(第5行)。选择的求解是通过在if语句(第6行)访问第一个分区的对应值来实现的。如果条件满足,聚合属性的值会被累加到全局总和变量sums中(第7行)。很明显,上述过程没有存储开销也没有代码生成。所有的运算都并入一个FOR循环。数据只需要装入CPU寄存器一次,直到他们不再需要才离开。实际上,编译器产生的不是C代码,而是可以被LLVM编译库编译成机器代码的等效的LLVM汇编程序 [25]。
如文献[27]所述,生成的代码不需要昂贵的中间物化过程,提高了批处理模型的CPU效率。更重要的是,在我们的案例中, JiT-compilation 使n 元存储模型适用于内存数据库。由于生成的代码是静态的,它是可预测的,并允许从CPU和编译器分别进行优化。因为HyPer已经有一个n元存储的后台,所以,为PDSM开发后台就变得很简单。我们扩展了目录以支持在一个关系进行多种垂直分区,于是编译器就可以访问各自的分区,而不是访问原关系。和以前系统一样,这里所面对的主要挑战是对于一个给定的模式和工作负载确定适当的分解。在接下来的章节,我们将讨论针对这个问题的解决方法。
四、 查询成本估算
部分分解数据模型上的查询性能除了依赖于处理模型,也取决于的分解布局的选择。像[15]中提到的方法,我们为给定的工作负载找到一种合适的分解布局来关注基于成本的优化。因为在内存数据库中,没有明显的因素处于决定地位,所以需要一个基于硬件感知的成本模型。因为常驻内存批量处理器面临类似的挑战,所以针对基于主存数据库的硬件感知成本模型已经有大量研究[26],[15]。然而,对于JiT编译查询,查询求解更为复杂:像第III节-B里边描述的那样,查询运算交错执行,导致复杂和不规则的内存访问。简单地增加运算的成本[26]或忽视非扫描运算[15]将产生不准确的估计。为了达到更精确的估计, 我们以现有的通用的成本模型(Generic Cost Model)[26]为基础,用它的原子操作当做指令,开发了“可编程”的整体成本模型。
在本节的剩余的部分,我们简要地介绍了一下设计一个复杂模型的必要性,然后介绍通用的成本模型[26]以及我们对的扩展工作,最后讨论基于硬件和存储布局感知的查询成本估算。
A. Cost Factors on modern CPUs
为了实现较高的内存访问性能,现代的CPU包含了一个复杂的内存层次结构(见图4)。多级缓存和多个TLB提高了内存数据项重复访问的速率 (或位于同一缓存行或TLB-Block的数据项)。此外,许多CPU具备了预加载单元,在数据项被访问之前,把他们推测式预加载到内存中。
l 预加载:
当CPU获取一个缓存行进行传输和处理时,预加载单元就开始预约下一个要访问的缓存行。如果置信度比较高,读取指令就会传递给系统,下一条缓存行就会被加载到一个LLC的(时间)槽位里。正确实施缓存行的预加载可以降低内存访问延迟,而错误的预加载会:
a)导致内存总线上流量浪费;
b)将一个本应该驻留Cache的缓存行错误地丢弃。
由于这些潜在的有害的影响,预加载单元在发出预取指令时,通常非常谨慎。
l 预加载策略:
预取策略常常因为CPU不同而不同,并且往往是比较复杂,甚至为了自我保护而不发出任何预加载指令。
在我们的模型中,我们假设的策略是:基于跨步检测的相邻缓存行预加载策略。例如:英特尔酷睿微体系结构中实现的方式[18]。在这种策略下,每当预加载器获得一个固定跨步,缓存行就会被预加载。虽然这个策略非常简单,但正是它的简单性和确定性使它更容易建模和实现。当然也有更复杂的策略,但通常依赖于(部分) 程序执行期间对数据访问的历史。这些主要为了处理更复杂的运算(如高维度数据处理或交叉访问模式),这些运算的执行像简单的关系查询处理,然而执行操作还是和相邻缓存行跨步检测预加载策略类似。
B. The Generic Cost Model
内存访问模式(MAP)对算法所暴露出来的内存访问行为进行正规且抽象的描述,通用的成本模型(GCM)就是建立在内存访问模式的基础之上的。GCM模型提供代数形式的原子访问模式,用来构建更为复杂的模式和公式来估算查询诱发的成本。该模型太复杂了,不适合在这里深入讨论,所以表1只是提供了一个简要的描述。感兴趣的读者可以阅读文献[26]里的详细描述。
为了说明模型的作用,考虑表1b中的示例,这是图2a中查询的内存访问模式(见图2),此查询是在部分分解的数据上展开的,这里的选择率是1%。
为了求解给定的查询,DBMS通过顺序遍历内存区域,扫描满足条件 (s_trav(A)=s_trav(26214400,4))的列A上的数据。然后,一旦条件满足,就访问列B,C,D,E。这在模型中定义为rr_acc,用r反映访问的次数,可以从选择率推导而来。对于每一个匹配的元组,输出区域都得更新。输出区域只有一个原子rr_acc,但是每一个满足匹配的元组都可以访问。已经证明,这种代数形式描述程序的执行对于预测基于内存的连接运算的性能是很有用的。
然而,你也许会注意到,表Ib的访问模式并没有完全准确的描述实际的运算: 由于事先假定好了访问顺序,访问B,C,D,E几个列所参考的rr_acc数据并不是完全随机的。在下一节中,我们将介绍可以对这类行文进行建模的扩展模型。
C. Extensions to the Generic Cost Model
在现代CPU上针对JiT编译查询进行建模时,原始模型会暴露一定的不足。首先,正如在上一节提到的,对选择投影建模考虑不足。我们通过引入一个新的访问模式,即顺序遍历/有条件读取模式来克服这个问题。然后介绍一下我们针对模型作出的两个改进,一个是针对随机访问成本估算模型的改进;另外一个是针对预加载的影响模型做出的改进。对于后文的理解来说,对新的访问模式的理解是至关重要,两个扩展改进并没有改变模型的本质。
1)顺序遍历/条件读取
表Ib中的例子已经暗示了成本模型的一个问题:如果内存区域是被顺序扫描的,但并不是所有的元组都被访问,那么大多数DBMS暴露的访问模式不能用表Ia中的atoms进行精确的建模。在这个例子中,我们根据rr_acc进行对这类操作进行建模是不合适的,因为:
a)内存区域是从头到尾遍历的,没有任何回退;
b)内存区域内所有的元素都最多只访问了一次。
而这不是构建成本模型最初的目的——优化连接操作的性能。这严重限制了对整体查询成本的估计以及对后续存储布局的优化。
我们开发了一个扩展模型来对这种行为进行准确建模。一个新的原子操作,有条件的顺序遍历和读取(s_trav_cr),该操作与s_trav具有相同的参数,结合特定条件s下的选择率来捕获选择性投影操作。
图5给出了这种访问模式的示意图,通过R.n个步骤顺序遍历该内存区域。在每个步骤中, 依概率s读取u个字节,迭代器无条件的向前读取w字节。这个扩展提供了一种原子访问模式,正好用来对表Ib中的查询评估进行建模:在选择率是s= 0.01的前提下,rr_acc([B,C,D,E])被转换成了s_trav_cr([B,C,D,E],s)。
为了估计从这些参数造成的缓存缺失量,我们必须先估计遍历缓存过程中访问缓存行的概率Pi。Pi是缓存行的所有项被访问的概率。它独立于缓存的容量,但是取决于缓存行的宽度(也就是块的大小,这里用Bi表示)。假设在内存区域内服从正态分布,Pi就可以借助公式(1)来估计取值。对于非正态分布,该模型可以根据其他的公式进行扩展。
(1)
然而,为了估计诱导成本,我们必须区别随机缺失和连续缺失。尽管没有明确指明,我们还是假设最初模型里的随机缓存缺失和连续缓存缺失是可以用来区分非预加载缺失和预加载缺失的。所以,我们对他们进行上述建模。
假设在一个相邻的高速缓存行预加载器中,一个缓存行被加载的概率是连续的,也就是预加载的。正在访问的缓存行的缺失概率和先前缓存行的缺失概率是一样的。因为这两个事件在统计学上来讲是独立事件。两者的概率可以像公式(2)里这样直接相乘,从而得到连续缺失的概率:
因为任何缓存缺失要么是连续缺失,要么就是随机缺失,一个缓存行的随机缺失概率就可以用公式(3)推导得到:
有了缓存行的访问概率,我们就可以用公式(4)来估计随机缺失和连续缺失两种情况下的缓存缺失数量:
预测精度:图6给出了不同选择率s下,随机缺失概率和连续缺失概率的示意图。随机缺失和连续缺失的百分比在选择率0<s<0.05时,急剧增加。从这个点往后,随机缺失的数量减少,而连续缺失继续增加。
为了评估我们预测的质量,我们用C语言实现了一个选择投影,并用CPU性能计数器测量了诱导缓存缺失。CPU微架构性能计数器只把需要的L3缓存缺失计入缺失统计结果。预加载器触发的缺失量没有被统计,这便于后面测量时加以区别。顺序缺失直接通过统计的L3访问数量减去L3缺失数量。随机缺失就是统计得到的L3缺失量。
另外,除了预测的缺失,图6也给出了测量到的缺失。如图所示,当选择率在中等范围时,预测缺失量高估了随机缺失量;而在选择率较高时,则又低估了随机缺失。不过,预测的总体趋势还是比较接近测量值的。
为了说明新模式带来的性能提高,图中也给出了缓存行的预测访问数量,这里的建模采用了rr_acc而不是s_trav_cr。可以看出,a) rr_acc远远低于总的缺失量(也就是说,访问量低于缺失量,不用担心缺失?);b)随机缺失和连续缺失不好区分。尤其在选择率较低的时候,模型的精确度得到了巨大的提升。
2)基于预加载感知的成本函数
在通用成本模型的原型中,是区分随机缺失和顺序缺失的(分别记作);并分别赋予不同的固定的权重(相对成本)来确定最终成本。这些权重用经验校准而不是细致的观察和适当的建模。这对于原始版本的模型就足够了,毕竟那只包含纯粹的随机或者纯粹的连续缺失。然而,在我们的原子操作s_trav_cr里,既包含了随机缺失,也包含了连续缺失,就必须仔细地对两种缺失的成本加以区别。
基于这种考虑,我们提出了一种替代的成本函数模型。与Manegold[26]等人简单的增加不同内存层次上的加权成本不同,我们用更为复杂的成本函数来反映预加载对成本的影响。由于最重要的预加载是在LLC,我们将单独为此建模计算成本函数。
预加载的基本作用是降低高层存储和处理背后的内存访问延迟。所以,它的性能极大地取决于在快速内存层处理一个缓存行所花费的时间。根据内存访问速度决定执行时间的基本原理,用更快的缓存和处理器寄存器(可以看做是另外一层内存)可以降低 LLC诱发的成本。
如果处理数据的时间比预加载的时间长很多,整个的成本就取决于处理数据的成本,并且LLC诱发的成本是0——这里CPU就是瓶颈了。在LLC连续缺失的情况下,总体的成本可以用公式(5)来计算(在我们的Nehalem系统中,是第3层Cache,所以):
根据文献[26], 访问第i层的成本(CPU周期)可以记作li(也可以看作是第i-1层的缺失)。因为我们把CPU的寄存器看作是另外一层内存,L1来表示加载和处理一个数据花费的时间,M0表示要处理的寄存器数据的数量。
整体的成本TMem通过对除了LLC之外的所有Cache层加权缺失求和来计算。LLC的预加载缺失通过公式(5)计算,然后添加到公式(6)来作为整体成本的一部分。
3)随机访问估计
为了估计重复随机访问(rr_acc)引发的缓存缺失数量,Manegold等人根据访问操作r和区域内的元组数量R.n对缓存行的访问数量进行估计,并提出了对应的公式。虽然从数学上来讲,这个公式是正确的,但是由于需要计算大量的二项式系数,导致该公式的计算变得非常困难。这对于那些涉及大量表格的运算来说,用该模型进行估计变得不切实际。处于完备性的考虑,我们提出了另外一个公式。
关于选择记录去重的研究已经非常广泛,但是也颇有争议。Cardenas[7]等人提出并使用公式(7)来估计当R.n个记录中的一个被访问r次时的不同的访问次数。
由于不断面对各种情况的挑战[13][34][9],我们发现公式(7)和原来的模型具有完全一样的结果,计算成本却更低。有了根据内存访问模式来估算成本的这一模型,我们可以通过把查询转换为内存访问模式的代数形式,来估计一个关系查询的代价。在后续章节,我们将介绍这一过程。
D. Modeling JiT Query Execution
根据内存访问模式代数的指令化特点,我们可以把它看成一个编程的机器,而一个个原子访问模式就组成了一条指令。所以,为一个物理查询计划生成访问模式就类似于为查询的实际执行生成实际代码(详见文献【27】)。
为了生成访问模式,需要从根节点开始,先序遍历关系查询计划,见图7。而对应的模式则按照表II生成。就像程序里的语句一样,产生的模式追加到整体模式上。
注意当运算符节点第一次输入时,都不会产生一个模式。所有的运算都要等数据流进运算符节点才开始。也就是说,运算发生在遍历完且离开这个节点的时候。连接运算稍微不同,因为数据会流经这个节点两次,他的孩子节点都会分别经过一次。这样一来,他们会产生两次模式,一次是构建内部哈希表,一次是探查他。当产生模式的时候,我们将以pulling和non-pulling运算来加以区分。那些在流水线中,底层有连接运算的运算符都是non-pulling运算,其余的是pulling运算。这背后的原因是:连接运算之上的运算符,不需要显式地提取他们的输入,因为连接运算会把元组当做哈希探查的结果放进一个流水线片段中,详见文献[27]。像图7中的选择运算,属于pull方式的运算符,都必须显式地读取他们的数据。
例如图7中的连接运算符。当输入是当前节点的一颗子树,且目前没有模式生成,正要遍历当前节点的左孩子时。当离开左子树(标记为1)时,生成一个模式反映了为哈希连接构建哈希表的过程。因为他的左孩子是基本表,所以必须拉元组进入哈希表。所以,产生一个s_trav(R1),和一个关于哈希表的并发操作r_trav(ht)。因为哈希表导致物化过程,所以打断了流水线,就需要用顺序运算来标记模式。然后,继续处理它的右孩子。当离开右子树时(标记为2),哈希表探查完毕,元组压进另外一个哈希表。在标记2的地方,生成的模式是。
通过上述过程,我们根据访问模式把通用的成本模型编辑成一组指令。这是不仅一种简单而且高雅的JiT编译的成本估算方式,而且还允许我们估计存储布局可能带来的影响。在后续章节,我们介绍如何针对给定的工作负载来优化存储布局。
五、 模式分解
寻找最优的模式分解是在所有的垂直分区空间内,用估计得到的成本函数作为目标函数的最优化问题。因为模式的垂直分区的数量随着表格的大小指数增长,基于属性的分解算法在这里就不可行了,比如文献[16]里Data Morphing Approach用到的算法,因为要花大力气去优化它(随着分区的数量线性增长,所以在属性上就是指数级增长的)。作为替代方案,我们使用的算法采用工作负载上的查询作为模式上潜在分区的提示。Chu等人[10]提供了两种这样的算法。第一种是OBP算法,考虑到查询的数量,要花费指数级的努力才能得到优化方案;另外一种方法,BPi只是能找到优化解,但是却减少了成本(如果选择的阈值合适,可以到线性级)。下面我们将采用BPi算法来达到优化的解决方案。
Binary Partitioning(二元分区)
OBP和BPi通过一种合理切割(reasonable cut)方法,迭代式地分割一个表以产生潜在的优化解。在原来的算法里,每一个查询为每一个被访问的表的表产生一个合理分割。这种分割是两个不相交的属性集合:一个集合查询涉及到的集合,另外一个是不在查询访问范围内的属性集合。在图2a中,我们最初的的例子里,会产生一个分割如{{A,B,C,D,E}, {F,…,P }}。对于多种类型的查询负载,合理分割的结果集会包含对关系进行多次分割得到的所有的结果。所以,解空间会按查询数量的指数级增长。
然而,这种合理分割的定义似乎忽略了查询的实际访问模式。他没有反映一个事实,那就是一个查询涉及的不同属性可能访问的方式也不同。例如,上述例子里{{A},{B,C,D,E},…}就不会认定为一个合理分割。所以就不会被分解,因为这些属性都出现在同一个查询里。
然而,如果选择度是0的话,{B,C,D,E}将永远不会被访问;而把他们和A放在一个分区里,会影响扫描的性能。所以,也要考虑对那些出现在同一个查询,但是访问方式不同的属性进行分割。这样以来,我们就把扩展的合理分割(Extended Reasonable Cuts)当作潜在的解。这些解是从访问模式产生的而不是依据查询产生的。
一个合理分割是由这样一些属性组成的,这些属性一起被一个原子访问模式访问到或者在一个并发的执行同种访问的原子模式内被访问。
例如:可以产生一个分割,而就会产生两个分割。对于并发操作s_trav_cr,情况就稍微复杂一些。里边的a和b可能也或许不会在一起访问,这得视选择度s1和s2的取值而定。例如,如果s1=s2=1,被遍历的属性一直都是一起被访问的,不会被分解。如果选择度小于1,我们必须考虑所有可能的分解:{{{A},…},{{B},…},{{A,B},…}}。
以所有合理分割的解空间为基础,BPi使用分支界限方法来降低搜索空间。根据随机选择的一个分割,模式被递归分解。这个分割根据成本增幅来决定是否被包含在解空间里。如果增幅超过一个用户定义的阈值,这个分割就会被是否包含在解里;算法就分为两个分支:包含或者不包含这个分割。如果低于那个阈值,就不需要考虑分割是否被包含在解里了。子树就被剪掉。这样牺牲最优性来降低搜索空间和优化成本。
六、 总结
在MRDBMS内,用部分分解来降低数据访问成本是允许的。但是,为了充分发挥其潜能,有一点很重要,那就是不要为了节约带宽而降低CPU的性能。JiT编译查询自然避免了CPU的负担,而且很自然的满足了部分分解存储模型的需要(PDSM)。通过上述方法的组合使用,我们不仅节约了带宽,而且查询求解并没有增加CPU的额外负担。我们发现用JIT编译查询来代替混合的DBMS,会获得好几个数量级的收益。
而且,部分分解不会降低性能,收益依赖于数据库的模式和工作负载。从大概的情况来看,扫描和投影的工作负载类型越重,部分分解算法的效益就越好。在通用数据集上的测试结果会更明显,例如CNET数据集,SAP SD数据集。我们有理由相信,工作负载感知的存储优化将是未来值得研究的一个有趣的方向。
除了模式分解,还有其他的负载感知存储优化方案。尤其是当把稀疏数据的存储转换为稠密的键值对存储时,会同时降低存储空间和处理难度。我们也希望这样的键值对存储能够更容易的集成到现有的列存储而不是类似JIT这样的模型。当数据不是很稀疏但属性较少的时候,部分分解还是很奏效的,对我们这样的硬件感知的模型来说,也很好用。另外可能使用的场景比如在线自适应重构分解策略和查询布局协同优化。