Leveraging Lock Contention to Improve OLTP Application Performance

摘要
为了提高事务处理性能,大多数工作都关注于如何设计高效的并发控制机制,很少有工作关注查询负载和应用语义来提高性能。本文展示一款面向查询的编译器QURO,能够自动对事务里的负载重新排列以提高性能。我们观察到并发事务之间某些负载会对相同的元组进行上锁,这些查询称为争夺性负载,QURO使得执行争夺性负载尽可能晚。通过QURO事务吞吐量提高6.53倍,事务延迟下降了85%。

1 介绍
介绍了一下2PL,有些数据是热点数据,一个事务访问,其余所有事务都被阻塞,将影响性能。
一个避免2PL带来的问题的方法是重新排列事务里的负载,让操作最热点的数据最后执行。
高级编程语言把查询当成黑盒调用,编译过程中不会统筹考虑。DBMS不清楚查询之间的语义关联,因此让DBMS在应用执行过程中重新排序查询很困难。人工方式又会使得难以理解,开发者又需要保存依赖于查询的数据,重新排列容易出错和混乱。
QURO首先给出负载之间的争夺数量,据此将重新排列问题抽象为ILP(整数线性规划问题),根据解来重新排列负载,通过标准编译器编译重排码来产生应用二进制。
本文贡献如下:
1.我们注意到事务码中负载的顺序能够严重影响OLTP应用的性能,现有的编译框架和DBMS对于这方面没有优势,不能提高性能。
2.我们用ILP来规划负载重排问题,求出一个最优值让这一过程扩展到真实的事务码
3.我们实现了QURO原型并在主流的OLTP benchmark上测试,在主存DBMS上,事务吞吐量提高6.53倍,单个事务的平均延迟下降了85%
本文组织如下:section2首先用例子解释负载重排,然后给出QURO的概览,section3说明QURO的预处理步骤,section4是重排算法的细节,section5给出profile,section6展示三个OLTP benchmark的实验结果,section7讨论相关工作和结论。

2 概况

这是TPC-C的payment事务码和重排后的事务码:





这是原始payment和重排后的payment的执行过程的对比,颜色越深则越容易争夺锁:


看起来简单,但是这个工作并不简单,特别是需要保证重排后的事务码不能和原始事务码在数据依赖上的语义产生冲突。举个例子,L1的line15只能在line1和line3执行后,因为需要用到1和3的结果(程序变量),同理line3和line4也是有数据依赖的。
QURO的优化是根据每个负载的锁争夺来重排事务码实现的。用BEGIN_TRANSACTION和END_TRANSACTION来区分每个事务。
profile:QURO首先根据原始事务码生成每个负载运行期间的instrumented version,收集负载争夺信息。QURO部署instrumented version,设置和原始应用一样,运行一段指定的时间。QURO为每个负载设置一个争夺标志(contention index),这将在后面的重排步骤中用到,QURO还收集schema信息,将在生成排列约束的时候用到。
预处理:首先为每个事务求reaching definition analysis(到达定值,编译原理),这用在推断不同的程序变量间的数据依赖,然后QURO将循环拆成小块,称之为重排单元(reorder units),这样才能有更多重排机会。
接着在重排前根据profile和预处理信息来发现排列约束(order constraints)。程序变量或者元组间的数据依赖可能产生排列约束。首先基于程序变量用reaching definition analysis生成排列约束,然后用负载和schema信息推断元组间的排列约束,如果两条负载更新同张表的同个元组则这两条负载的顺序不能变。基于排列约束抽象成ILP问题,简单的方法是将一个重排单元当成ILP的一个变量,但是求解可能很费时间,所以在section4.4给出一个优化方法。QURO根据ILP的解来重构程序,然后用一个通用的编译器编译应用(application)的最终二进制代码。

3 预处理
预处理将输入的事务代码解析成一棵抽象语法树,再做两个步骤:1.将事务代码拆成小块;2.分析程序变量间的数据依赖。
3.1 分解循环和条件语句
循环和条件块内部的语句顺序很难调,可能是嵌套的。如果将整个循环或条件语句当成一个单元则限制了重排的可能解,section6将证明分解循环和条件语句对于提高许多事务benchmark的性能是必要的。
对于循环语句,QURO应用循环分裂[22],将嵌套循环分解成多个单循环,一个循环体中S1和S2两条语句,如果没有循环依赖、数据依赖、语句不影响循环条件,则S1和S2可以分解成两个循环。

L3和L4是分解前后的例子


对于条件语句,同一个条件下的语句可以分解为多个条件语句,L5是分解L1中line9-line14的例子


3.2 Analyzing Reaching Definitions(分析程序变量间的数据依赖)
到达定值(reaching definitions)的定义:语句1定义的变量v能够到达语句2且这个过程中v没有被更改。我们用标准数据流算法[19]为每条语句计算到达定值。对于大部分语句,处理过程很简单。对于函数调用我们区别对待,对于数据库调用,我们对函数参数和返回值建立一个精确的定义-使用模型;对于其他调用,我们适当假设所有指针或引用参数都被函数定义和使用。

4 重排语句
4.1 生成排列约束
重排目的是使得查询以锁争夺次数的递增排序。要保护两种数据依赖,第一种是程序变量间,第二种是数据元组间。
3种程序变量间的依赖:
1.写后读(RAW):Ui用了Uj定义的变量,则Ui应该出现在Uj之后。
2.读后写(WAR):Uj用了一个之后会被Uk更新的变量,如果Ui和Uk都定义了同个变量v,且Uj用的v是Ui定义的,则Uk不能出现在Ui和Uj之间,如果不存在Ui,则Uk应在Uj之后出现。
3.写后写(WAW):v是一个全局变量或函数的引用参数,Ui和Ul都定义v,Ul是函数体中v的最后一次定义。则Ui应该在Ul之前。如果v是全局变量,我们假设程序自己的锁会阻止竞态条件,不用我们关心。
接着用L1举例子说明上面1和2依赖。

4种数据元组间的依赖:
1.同张表上的操作:查询Qi和Qj作用于相同的表,至少有一个查询是写操作(如更新,插入,删除)。
2.视图(View):Qi对表Tj的一个视图Ti进行操作,Qj对表Tj进行操作,至少有一个query是写操作。
3.外键约束:Qi对表Ti进行插入、删除,或者更改Ti表中Ci列的值,Qj对表Tj上的Cj列操作,列Cj是外键,参照表Ti的Ci列。
4.触发器(Trigger):Qi对表Ti进行插入或删除触发一系列对表Tj的操作,Qj对表Tj操作。
以上4种情况,Qi和Qj的相对顺序与重排前一致。
4.2 规划ILP问题
将重排问题抽象成ILP问题。profile过程为每条查询生成一个冲突标志(conflict index),标志值越大,此查询与其他事务发生数据冲突的可能性越大,非查询(non-query)语句的冲突标志为0。

假设有n个重排单元U1到Un,每个单元对应一个冲突标志ci,pi代表单元Ui的最终位置。


接着用L1举了个例子。
4.3 去除不必要的依赖
介绍两种优化ILP问题的方法。第一种是重命名方法去除不必要的依赖,第二种是缩小问题规模。通常WAR和WAW称为命名依赖(与数据依赖形成对照,在RAW中出现)是可以通过重命名被去除的,但如果WAR或WAW中的某个变量v满足以下任一个条件,那么去除将变得复杂。
1.v不是基本数据类型(例如指针或类),因为重命名是要复制原始数据的,对于非基本类型,可能有私有域,且复制对象会拖慢程序速度。
2.v在同一重排单元中被使用和定义。例如f(v)中v通过引用传递,用v_r代替v将会传递未初始化的值,因此需要先将v的值复制到v_r中。
3.多个定义到达v的同个用法。v的任一个定义被重命名则v的其他所有定义也需要重命名,用L8和L9做例子说明。
4.4 缩小问题规模

这节描述一个减少ILP问题变量数目的优化方法。直观想法是去除ILP中的不包含查询的重排单元的变量,但会导致错误去除了查询之间的数据依赖。但如果我们将非查询变量去除之后补上额外的约束就行。首先,定义一个辅助bool变量xij来表示i<j时pi<pj,将原始ILP的约束重写为辅助bool变量的形式:


将重写后的约束子句写成合取式E


满足约束的任何一个排序都将使E为true,现在使用E中已知的子句推导新的子句,规则如下:


直到无法产生新的子句,用所有的子句组成一个新的合取式E',之后将E'中的子句再转回为ILP约束,但只选择包含query的子句,并以下面的规则转变:


这样ILP约束就只涉及有query的单元,解这个约束产生最优排序。我们证明以上的迭代推导过程时间复杂度将收敛为n的多项式,n是重排单元的个数。新的子句的形式只有三种,且xij的个数是O(n^2),可能子句的个数为n的多项式。总是存在满足所有约束的一个重排顺序,在[23]中有证明。
4.5 重构事务代码
如果依赖4.2的方法则生成最后的代码很容易,如果应用4.4的优化则需要考虑非查询重排单元的顺序问题。此节介绍重构过程。
基本思想是根据新次序遍历query,试着对其他与当前query所在单元有关的重排单元进行放置。算法1开始时U_list为空,用来存储重排单元的次序;Us是所有重排单元的集合,当U使用的所有变量来自的重排单元都已经在U_list中,则将U也加入U_list;当Defs(Ui)返回重排单元集合,这些单元定义的变量被Ui使用;Uses(Ui)返回重排单元集合,这些单元使用了Ui定义的变量;Checkvalid(U)检查U是否满足数据依赖;Rej[Qi]记录Qi所有失败的重排单元。对于每个query Qi,先插入Defs(Qi)到U_list,接下来插入Qi,最后插入Uses(Qi)。

5 Profiling
已经有工作研究如何估算锁争夺程度,Johnson[15]使用Sun公司的profiling tools计算数据库事务的分类时间,分析有效工作的时间和等待锁的时间,用于确定争夺级别。syncchar[20]通过跑一个经典的负载样例,计算冲突密度并推断锁争夺的程度。QURO可以选这些技术,但选了一个更简单的方法,这个方法计算每条query的运行时间和标准误差。目前我们的原型系统,大多数事务的时间花在锁等待上,如果query访问热点数据,锁等待时间将会变化巨大,因此误差越大,冲突的可能性越高。
为了搜集query的运行时间,QURO在每条query前后加了代码,在profiling结束后计算标准误差。目前的原型系统中,我们假设profiler运行时的设置与真实应用部署环境一样。

6 评估
我们用Clang处理事务代码,gurobi用来计算ILP,本节展示QURO和原始实现的对比实验。我们首先研究QURO生成的事务代码的性能,通过隔离磁盘操作的影响,这个占据了事务处理的大部分时间。为了达到隔离的目的,我们取消了提交时间刷磁盘,我们用的机器内存足够大,装得下实验的所有数据。以下实验基于MySQL5.5,所在机器为128核的2.8GHz处理器,有1056GB内存。
6.1 Benchmarks
1.TPC-C。我们使用其开源实现[3]的版本作为输入源代码,实验包含单独对每种类型的事务和混合类型的事务,由于空间的限制,只展示了单独跑new order和payment事务的实验结果,其他实验结果可以在技术报告[23]里找到。
2.TPC-E中与交易相关的事务。TPC-E模拟了一个金融经纪行,包含顾客、经纪行、股票交易三部分。我们使用了开源实现[4]作为输入。我们实验的事务包含交易更新、订单、结果和状态事务。我们只展示了交易更新事务的结果,因为其他事务不包含访问热点数据的query,重排对于这个性能提升有限。其他交易事务的结果可在技术报告中找到。
3.投标benchmark的事务。我们使用了开源实现[1]。这个benchmark模拟了用户投标的活动,这个benchmark只有一个类型的事务:读取现有出价,更新用户和投标信息,对投标历史表插入一条记录。
profiling阶段跑20分钟应用来收集query的运行时间。每个实验的头5分钟省略掉,为了能让线程开启和填满缓冲池,然后花15分钟测试吞吐量,每个实验跑3次得平均吞吐量,还省略了客户端的thinking time(考虑时间,为了模拟真实),我们假设有足够的事务使得系统饱和。
6.2 varying data contention(变换数据的冲突大小)
第一个实验,比较原始代码和重排后代码的性能,固定32线程,改变contention rate。变换数据的争夺性是通过改变一个事务访问的数据集大小实现的。对于TPCC,我们通过调整仓库数量从1到32改变数据大小,1仓库,则所有事务不论读写都访问同仓库的元组,32仓库,并发事务更可能访问不同的仓库由于他们之间数据争夺性较低。对于TPCE,我们调整每个事务访问的交易数量,由于交易更新事务是用来模拟交易的轻微修正的过程,更改交易的数量就是更改事务访问数据的大小,我们变换交易数量从1K到整个交易表的大小576K。当被更新的交易数量很小,并发事务将可能修改相同的交易记录,与此相反,当事务随机访问交易表的任何元组,他们冲突的可能性较小。对于投标benchmark,我们调整投标项目的数量,投标人给出一个更高的价格将会改变此投标项的当前价格,设定投标人中给更高价格的比例是75%,意味着有75%的事务将对投标项的元组进行写入。
图3a是TPCC只跑payment事务的结果,QURO达到了原始的6.53倍。图3b是TPCC只跑new order事务的结果,这个事务很难重排由于程序变量间的许多数据依赖,但仍然达到了原始的2.23倍。图3c是TPCC包含50%new order和50%payment事务的结果,在高数据争夺性下,重排速度是4.13倍。图3d是TPCC五种类型事务的混合的结果,事务类型越多则数据争夺性越高。对于TPCE,图3e是交易更新事务的结果,这个benchmark有一个平均20次迭代的循环,一次迭代里有多个读query,但只有一个update query,我们将这个结果归功于缓存。图3f是投标benchmark的结果。
结果显示,数据集减小,争夺性升高,则重排提高性能的机会也增高。
6.3 varying number of database threads(变换数据库线程数)
这个实验的实验数据相同,但是调整线程数量。相同的数据集下,线程增多则争夺性增高,接下来是结果随着线程增多,QURO表现如何。
图4a到图4d是TPCC的结果,固定仓库数为4。图4e是TPCE交易更新事务的结果,固定交易数量为4K。不像TPCC的new order和payment事务有明显的争夺性query,交易订单和交易结果事务里的query访问数据有相同的争夺级别,这几个详细结果和分析都在技术报告[23]里。图4f是投标事务的结果,固定投标项为4。
我们还对比了每个benchmark里,随着线程数的增多,自己的相对加速,两个实现的baseline是原始实现单线程的吞吐量,图5显示线程数增多吞吐量的变化。这里只展示了payment和trade update事务的结果,其他在技术报告里可以找到。线程数超过8,QURO的的自加速比就超过了原始实现,相对于原始实现,QURO允许通过scale up来提升并发数据库连接数目。
6.4 分析性能
查询执行时间:我们分析了每条query的执行时间,表2是TPCC payment事务的结果,列出了10K个事务的分解时间,32线程,1个仓库。
第3列的意思是,query的执行时间,query没有锁且每个事务顺序执行。通过对比每行中single-thread和其他列,可以推断出两种实现中每条query等待锁的时间。
对于原始实现,大部分执行时间都花在query2上,如L1所示,这条query是一个update操作,每个事务都更新同一个元组。除了Q2其他query重排后执行时间反而上升,这是由于在获得锁过程中,其他query被阻塞的几率增大。原始实现中,query能够高效访问争夺性数据,是因为串行化所有事务,由于每个线程都需要在开始时获得争夺性的锁,因此,后面query的执行时间就和运行在单线程上一样(例如没有锁了),但是重排将使得query并发运行去竞争锁,这增大了运行时间。
我们分析了new order事务,这个事务里重排的可能性被query间的数据依赖限制住了,特别是query2,是最争夺性的,但是Q2不能被重排到事务的末尾由于许多其他query都依赖这条query的结果,表3是结果。
上面的分析意味着重排是有妥协的。重排减少了花在冲突query上的锁等待时间,但是却增加了非高冲突query的锁等待时间,在大多数情景下,减少的时间远大于增加的时间,重排减少了query延迟除了一些query比原来执行时间更长一点点。
Abort rate:对于TPCE的交易更新事务,重排通过减少abort rate提升性能。一次交易更新事务平均随机查询20条交易记录并修改这20条记录,当交易数量小时,并发事务更可能访问相同的数据,但不同的顺序还可能导致死锁,表4是结果。
图6说明重排如何降低abort rate。在交易更新事务中只有一条query每个循环修改一次交易表,而其他query读其他没有正在被修改的表。假设有事务T1和T2,T1先开始,在一个时间范围内如果T2也启动则两者可能导致死锁,我们将这个时间范围称为死锁窗口,重排后死锁窗口变短了,因此abort变少,吞吐量提升。
6.5 worst-case implementations(最坏场景的实现)
为了验证我们的观察,即query的顺序影响事务性能,我们人为设定了一个最坏的场景,最容易冲突的query放在事务的最开头。将这个实现与原始实现和QURO做对比,图7是TPCC payment事务的结果(其他benchmark结果在技术报告里),如我们所预期,将最容易冲突的query放在事务最前面表现出最糟的性能随着线程数目增加,与图4a对比,重排获得的性能更好。
这个实验表明query的顺序对性能的影响巨大。但是QURO依赖于profiling估计锁争夺,如果负载变化,则事务需要重新profiling和重排,我们的未来工作是实现动态profiling和重新生成。
6.6 Disk-based DBMS
接下来我们考虑QURO在基于磁盘的DBMS系统上的性能,改变花在磁盘操作上的时间来测量性能,这个可以通过变换缓冲池的大小来实现。当缓冲池比数据库小时,脏数据被刷到磁盘,这个操作占到事务执行时间的一大部分,图8是在TPCC上的对比结果,当缓冲池小时,磁盘操作将决定事务处理时间,因此重排获得的性能提升也很小,增大缓冲池时则减少磁盘操作的时间,重排将增大吞吐量。这个实验表明即使不是内存数据库,重排仍能提高吞吐量。
6.7 带有存储过程的性能比较
前面的实验我们将数据库服务器和客户端程序都运行在同一台机器上来最小化网络传输的影响,这个实验加入存储过程[3]。通过一条query调用整个事务,客户端和数据库之间的网络传输才能最小。
图9是这次实验TPCC payment事务的结果,当数据争夺性小时,存储过程实现方案比其他的实现性能都好,但是随着数据争夺性增高,锁花费的时间远比网络传输的时间高,而且这时QURO性能也最好。
6.8 ILP Optimization
最后这个实验定量分析,我们选择两个事务:40条语句和9条query的new order;189条语句和21条query的trade order。预处理后两个事务分别被分成27和121个重排单元,我们用QURO来为每个事务构建成ILP,用到了4.2和4.4说的方法,然后用两个开源ILP solver,lpsolve[6]和gurobi[5]来解,限定2个小时,4.5说的重构代码只在用到优化时才用到,两种事务都只花了1秒。
表5是结果,原始方法(没有经过优化)下,两个solver在2小时内都没解出来,用了优化的,变量数量和约束数量分别减少了79%和98%。

7 相关工作
除了锁方法,还有各种各样的并发控制协议被提出,例如乐观并发控制、多版本并发控制研究内存DBMS在不同并发控制协议下的性能和可扩展性,在技术报告中我们对比了QURO实现的2PL和OCC和MVCC的性能。
有一些工作是提高基于锁的并发控制协议的效率。Shore-MT[16]在多核机器上运用2PL并提供许多系统级别的优化来达到高扩展性。Jung等人[17]在MySQL上实现一个锁管理,通过批量锁分配和回收提高扩展性。Horikawa[14]改造一个PostgreSQL的数据结构,latch-free(锁存器空闲),并实现一个锁存器空闲机制的锁管理器。然而这些系统没人检查应用是如何发送query的,QURO通过改变应用程序代码顺序提高性能,还支持其他锁实现,例如上面描述的。
还有从应用角度提升性能的工作。DBridge[9,13]是一个程序分析和转换工具,通过重写query优化性能。Sloth[11]和Pyxis[10]是工具,使用程序分析减少应用和DBMS的网络传输。
最后,有工作是使用数据库应用语义来设计并发控制协议。Faleiro等人[12]发现,懒惰事务处理提高缓存命中率,提高负载均衡,通过使用争夺性足迹来帮助决定哪个query延迟执行从而减小数据争夺,然而这项技术只被用在确定性DBMS,且事务开始前要知道所有要被执行的query。我们的工作结合了带有程序分析的并发控制,适用更广泛的DBMS。

8 结论
我们展示了QURO,一个编译事务代码的新工具。QURO利用query争夺信息来自动重排事务代码,以提高OLTP应用的性能。QURO将重排问题构建成ILP问题,使用不同的优化技术来高效减少solve时间。实验结果表明QURO相对开源实现,可以降低85%的延迟,提高6.53倍吞吐量。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值