文章信息
- VLDB 2012
- Northwestern Polytechnical University, ChinaYale University
- Kun Ren,Alexander Thomson,Daniel J. Abadi
Abstract
在数据库系统中,锁是一种被广泛使用的并发控制机制。由于越来越多的OLTP数据库部分或者全部放到内存中,disk io对事务吞吐量的限制没那么明显了,这个时候锁管理成为主要的性能瓶颈。
在这篇论文中,作者提出了VLL和SCA。
对主存数据库系统里的悲观并发控制来说,VLL算是对它的一种替换方案,它几乎可以避免所有和传统锁管理器操作相关的开销。
SCA(selective contention analysis),选择性争用分析,他让系统实现VLL,达到在高争用工作负载下的高事务吞吐量。
实验:
1)传统单机多核数据库服务器
2)分布式数据库,数据分布在无共享集群的多个商用机器上
1. Introduction
随着机器核数的增加,锁管理器愈发成为性能瓶颈。(高争用的工作负载
尽管在所有数据库系统里锁协议的实现方式不尽相同,但最常见的方式还是通过hash表来实现数据的锁管理。
每个锁释放的时候,会触发链表的遍历,用来确定下一个继承锁的请求request。
哈希表的查找、锁存器获取、链表操作都是主存操作,如果访问的数据放在disk上,那么执行事务时这些代价可以忽略不计,但是对于主存数据库系统来说,这些代价不可忽视。
服务器核数和处理器数目的增加,也会提高并发度(因此导致锁的争用),每个锁对应的事务请求链表规模也会变大,还要考虑释放时遍历链表寻找下一个继承锁的请求。
因此对于主存数据库系统里锁管理器的设计,作者认为有必要重新审视。在本文中,探讨了锁管理器两个主要改变:
- 将所有锁的信息从中心锁数据结构中移除,而把锁信息绑定在会被锁定的原始数据上;(remove all lock information away from a central locking data structure, instead co-locating lock information )
- 删除未完成的从锁数据结构中请求特定锁的所有信息,用信号量来取代请求列表。(remove all information about which transactions have outstanding requests for particular locks from the lock data structures. Therefore, instead of a linked list of requests per lock, we use a simple semaphore.)
两个信号量,一个表示读请求,一个表示写请求
移除锁管理器的数据结构之后,当一个事务完成后释放锁,哪个事务来继承这个锁,就成了需要考虑的问题。
我们工作的一个主要贡献就是解决了上述问题。
- 基本技术:强制事务一次性/同时请求所有锁,然后按事务请求锁的顺序对事务进行排序。使用全局的事务排序来计算接下来哪个事务可以被解封,然后继承最近释放的锁。
- VLL(very lighting lock):开销小(和传统锁管理器相比)但只能追踪竞争事务的较少信息,高竞争负载下降低并发性和较低的CPU利用率
- SCA(selective contention analysis):VLL在高争用负载下会降低并发性和CPU利用率,为了改善这一问题,提出SCA,SCA能高效地计算出竞争信息中最有用的子集(仅在需要的时候),传统锁管理器在任何时候/始终都会跟踪这些竞争信息。
2. VLL
主存数据库系统这一类别包含不同的数据库架构,包括单个服务器(多处理器)架构和很多新型的分区系统设计。
VLL协议在设计上尽可能通用,针对以下体系结构进行了特定优化:
- 多线程在单个服务器、无共享内存系统中执行事务
- 数据跨处理器分区(也可能跨越多个独立的服务器),在每个分区,单个线程以串行方式执行事务数据跨处理器分区(也可能跨越多个独立的服务器),在每个分区,单个线程以串行方式执行事务
- 数据被任意划分(跨集群中多台机器),在每个分区,多个工作线程对数据进行操作数据被任意划分(跨集群中多台机器),在每个分区,多个工作线程对数据进行操作
第三种架构(多个分区,每个分区有多个工作线程)是最常见的情况。为了讨论的一般性,下面内容我们讨论的是最常见的情况。但我们也会指出在另外两个架构中运行VLL的优点和权衡。
2.1 VLL算法
VLL与传统的锁管理器的实现区别:VLL下每个记录的“锁表条目”不是以链表形式,而是以一对整数值(CX,CS)进行存储,CX和CS分别表示在这条记录上请求排它锁和共享锁的事务数目。
在每个分区中保存事务请求的全局队列,称之为TxnQueue。TxnQueue(事务处理请求的全局队列) 存在在每个分区,跟踪所有活动active事务(按照请求锁的顺序)
当事务到达相应分区,它会在该分区尝试请求在整个生命周期内将会访问到记录的全部锁。每个锁请求采用增加相应记录的CX、CS值的形式,取决于是否需要排它锁或共享锁。
如果CX=0,认为请求事务获得(被授权)了共享锁;
如果CX=1,CS=0,认为请求事务获得(被授权)了排它锁。
当一个事务请求了它需要的锁,它就被添加到TxnQueue。请求锁、把事务添加到队列这两者都发生在同一个临界区,这样在分区里面,一次只有一个事务可以执行。为了减少临界区的大小,事务尝试在进入临界区之前计算出它全部的read set和write set。这个过程不总是那么简单的,有时候可能需要一些额外的工作。此外,还必须协调多分区事务的锁请求。这个过程将在3.1进一步讨论。
离开临界区后,VLL基于以下两个因素决定如何进行下面的工作:
- 事务是local还是distributed的
Local transaction的read set和write set包含的记录都在同一个分区上;
Distributed transaction的可能需要跨多个数据分区来访问记录。 - 事务是free还是blocked的
事务是否能够在请求后立刻成功获取所需要的全部锁。能够立刻获得的称为free transaction,至少有一个锁获取不到的称为blocked。
基于事务是free还是blocked的,VLL以不同的方式来处理事务。
- Free transaction被立即执行。一旦完成,事务就会释放锁(把原先增加的每个CX和CS值再减掉),并把自己从事务请求的全局队列TxnQueue中删除。但如果事务是distributed的,它可能需要等待远程的读取结果,可能不会立刻完成。
- Block transaction因为还没获得所有锁,无法完全执行,它们在TxnQueue队列中标记为阻塞的(blocked),直到被VLL算法显式解除(unblocked)之后,这些blocked transaction才允许开始执行。
简而言之,不管事务是free的还是blocked的,是local的还是distributed的,所有事务都在TxnQueue这一全局队列中。但只有free transaction可以立刻执行。
因为现在没有锁管理数据结构来记录哪些事务在等待被其他事务锁定的数据,因此当某个事务完成后,就没有办法直接把锁转交给别的事务(无法确定转交给谁)。因此当阻塞(blocked)的transaction可以被解除和执行的时候,需要一个替换机制来决定谁来继承释放的锁。实现这个目的的一种可能方法,是说用一个后台进程来检查TxnQueue中每个阻塞的事务,检查事务请求锁定的每个数据项的CX、CS值。如果某事务增加了特定项的CX值,现在该项CX的值降为1且CS=0(表明在该项上已经没有其他的活跃事务了),那么该事务显然对该数据项有一个排它锁。相似地,如果某事务增加了CS的值,现在CX=0,表明事务获得了该数据项的共享锁。如果事务请求的所有数据项现在都可用,那么该事务可以被解除阻塞和执行。
这种方法的问题是,如果另一个事务进入TxnQueue并为TxnQueue中阻塞(blocked)事务已经增加了CX的相同数据项增加CX,那么这两个事务将永远阻塞,因为CX的值总是至少为2。
幸运的是,这个问题可以通过一个简单的观察来解决:到达TxnQueue全局队列前面的阻塞事务将始终都可以解除阻塞并执行,无论它访问的数据对应的CX和CS值有多大。要了解为什么会出现这种情况,注意每个事务请求所有锁和进入队列都发生在临界区。因此当一个事务处于队列前面的时候,意味着在它之前的请求锁的其他事务都做完了。此外,对于所有排在该事务后面的请求锁的事务,如果该事务和后续事务们的读写集合冲突,这些后续事务将会被阻塞(blocked)。
因为处于TxnQueue全局队列前面的事务总能被解除阻塞和执行,每个事务最终都能被解除阻塞。因此除了减少所管理其开销之外,该技术也可以保证在分区内不会出现死锁。(我们将在3.1介绍如何避免分布式死锁)
注意到一个阻塞事务现在有两种方式解除阻塞:1)处于TxnQueue队列的前面,即在它前面请求锁的所有事务都做完了。2)队列中只剩它一个事务。我们在2.5节讨论一种更复杂的解锁事务(unblocking transactions)的技术。
VLL有时会面临的一个问题是:随着TxnQueue大小的增长,一个新事务能够立即获得其所有的锁的可能性降低,因为事务只有不与整个TxnQueue中的其他事务冲突才可获得它的锁。
因此我们提出的解决方法:人为限制进入TxnQueue的事务数量,若超出给定范围停止处理新事务,把处理资源放在寻找TxnQueue中可以被解除阻塞(unblocked)的事务(见2.5)。
实践中我们发现阈值应该根据工作负载的争用率进行调整。当TxnQueue的阈值较小时,新事务与TxnQueue中的任何元素不冲突的可能性较大,因而较高争用工作负载此时运行得比较好。对于较低的争用工作负载,阈值可以更大。
为了自动考虑此调整参数,我们设置阈值不是通过TxnQueue的大小,而是通过TxnQueue中阻塞事务的数量,因为高争用工作负载将比低争用工作负载更快地达到这个阈值。
图1显示了基本VLL算法的伪代码。系统中的每个工作线程都执行VLLMainLoop函数。
图2描述了一事务序列的执行跟踪示例。
Figure 2: Example execution of a sequence of transactions {A,B,C,D,E} using VLL. Each transaction’s read
and write set is shown in the top left box. Free transactions are shown with white backgrounds in the
TxnQueue, and blocked transactions are shown as black. Transaction logic and record values are omitted, since
VLL depends only on the keys of the records in transactions’ read and write sets.
2.2 Single-threaded VLL
之前一直在讨论VLL的一般情况,即在一个分区内有多个线程处理多个事务,但也可以以单线程的模式运行VLL,每个分区只分配一个线程。除非一个事务跨越多个分区,否则每个分区都是独立执行的。如果事务跨越多个分区,需要考虑分区的协调处理。
在一般版本的VLL,一旦进程开始执行一个事务,直到事务完成期间它什么也不做。如果我们根据上面的规范,仅运行一个线程来实现single-threaded VLL,最终的结果将会是事务的一个串行化序列,因为一个事务无法打断另一个事务,只能等待它执行完成。
为了提高并发性在单线程VLL的实现,我们允许事务进入第三状态(除了“blockd”和“free”)"waiting”。这一状态表明该事务之前执行过,但是只有获取到远程的读取结果才能往下执行完成。当一个事务触发该条件并进入“waiting”状态时,主执行线程把它放到一边然后寻找&执行别的新事务。相应地,当主线程寻找要执行的线程时,除了考虑TxnQueue队列的队首事务,以及新事务请求,还有可能恢复执行处于“waiting”状态的事务(如果它已经获取到所需的远程读取结果)。
2.3 Impediments to acquire all locks at once
TxnQueue的队首要获得全部锁有两个问题:事务运行前其读、写操作集合未知;不同分区有自己的TxnQueue事务运行的顺序可能不同,有可能导致死锁。
解决问题一:进入临界区前允许事务读任何需要的数据(在GetNewTxnRequest函数中实现),来计算将会访问的数据
解决问题二:(1)允许产生死锁,使用死锁检测协议取消死锁事务;(2)协调分区使每个分区事务顺序一致
在本文选择第二种解决方案来实施。因为提交协议(比如two-phrase commit)通常是多分区事务的瓶颈,需要运行提交协议来确保事务执行遵守ACID(原子性、一致性、隔离性、持久性)四大原则。所以在这个更大的瓶颈勉强,减少锁管理器的开销显得没什么帮助。然而,最近在确定性数据库系统上的一些研究表明,在事务执行之前通过执行多分区协调可以消除提交协议。
简而言之,对于确定性数据库系统,比如像Calvin把分区里的事务进行排序,VLL可以利用这个排序规避分布式死锁。
2.4 Tradeoffs of VLL
VLL的主要缺点是在并发性的损失。VLL only test more selective predicates on the state: (a) whether this is the only lock in the queue, or (b) whether it is so old that it is impossible for any other transaction to precede it in any lock
queue.
因此在VLL下出现下面的场景是很常见的,当C可以安全执行时,VLL没有办法做出这一决定。
当争用率较低时,VLL无法立即确定有可能被解锁的可能事务(这句翻译比较拗口,大概是那个意思)。但是在高争用工作负载下,特别是有分布式事务时,VLL的资源利用率会受损,一些额外的优化显得很有必要。
3、SCA
标准锁管理器可以获知有哪些可以安全执行的事务,但是VLL可能还没找到。
为了最大化CPU资源利用率引入了SCA(selective contention analysis)选择争用分析。
SCA模拟标准锁管理器检测哪些事务可以继承已经被释放的锁这一能力。它通过进行检查争用的工作(spending work examining contention)来实现,仅当CPU空闲(即TxnQueue队列已满,同时队列没有明显可以被解锁的事务)才去做这一工作。因此SCA可以使VLL选择性地增加它的所管理开销,当且仅当该工作是beneficial的时候。
在TxnQueue队列中的事务如果处于“blocked”状态,说明在添加该事务时,它和在它前面的事务有冲突。从那开始,导致它变成“blocked”状态的事务可能已经完成并且释放了相应的锁,随着该事务越来越接近队首,它实际上是“blocked”状态的可能性大大降低。
SCA从队列前面开始,以它的方式从队列中寻找一个事务来执行。SCA持有两个大小均为100KB位阵列, DX和DS 初始化都为0。
- DX[j] = 1 if an element of one of the scanned transactions’s write-sets hashes to j
- DS[k] = 1 if an element of one of the scanned trans-actions’s read-sets hashes to k
因此不管在什么时候扫描到的下个事务Tnext如果具有以下属性:
- DX[hash(key)] = 0 for all keys in Tnext’s read-set
- DX[hash(key)] = 0 for all keys in Tnext’s write-set
- DS[hash(key)] = 0 for all keys in Tnext’s write-set
说明Tnext跟之前扫描过的事务没有冲突,可以安全执行。
换句话说,SCA就是从最老的事务开始,遍历RxnQueue,找到一个准备好运行且和比它老的事务不冲突的事务。伪代码如下:
SCA是有”选择性”且有 两种不同的方式:
First , it only gets activated when it is really needed ;
Second, rather than doing an expensive all-to-all conflict analysis between active transactions SCA is able to limit its analysis to those transactions that are (a) most likely to be able to run immediately and (b) least expensive to check。
为了提高SCA实现的性能,我们做了一个次要的优化,可以减少运行SCA带来的CPU开销。
改进:缓存第一次hash函数的结果
3. VLL算法
4. 相关工作(对比一些方法)
System R 锁管理器
LIL lightweight Intent Lock
多版本并发控制机制
乐观并发控制方案
……
5. 结论+未来工作
结论:提出了VLL,在主存数据库系统中可以避免传统锁管理器维护数据结构的代价。VLL把数据和锁信息(两个简单的信号量)绑定在一起,强制事务立刻获得需要的所有锁,尽管减少的锁信息可能会导致难以决定什么时候释放阻塞事务、什么时候执行,但提出SCA,可以在需要的时候创建事务依赖信息,用来解锁事务。该优化使得VLL协议在高争用情况下也能实现高并发。
实验结果挺好的。VLL性能明显超过two-phase locking, deterministic locking, and H-Store style serial execution schemes。同时高度兼容标准(非确定)数据库系统和确定性数据库系统。
我们在本文中的重点是更新现有数据的数据库系统。在未来的工作中,我们打算研究VLL协议的多版本变体,并进行集成VLL中的分层锁定方法。