凝望深渊,并发控制的尽头
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。
文章目录
引言
随着数据库领域的蓬勃发展,并发控制算法也有了长足的进步,从悲观的2PL到乐观的TimeStamp Order族的解决方案(OCC,MVCC),算法的每种不足也都有了对应解决方案,但是这些方案是否就已经是一种大一统的“定理”,未来基本不会在算法本身逻辑上做出改进呢?其实不是的,这些并发控制方案本身都有着自己的问题,你可能会说,当今所有流行的DBMS都是这样做的呀?诚然,这些并发控制算法虽然足够稳定与高效,但是基于的硬件条件是一般的多核机器,这其实从更高的视角来看是一个狭隘的观点,因为以目前的CPU架构来说,[1]中提到有如下问题:
We are entering the era of the many-core machines that are powered by a large number of these smaller, low-power cores on a single chip. Given the current power limits and the inefficiency of single-threaded processing, unless a disruptive technology comes along, increasing the number of cores is currently the only way that architects are able to increase computational power. This means that instruction-level parallelism and single-threaded performance will give way to massive thread-level parallelism.
这意味着我们的代码其实应该去适应这种一个芯片上有多个简单单核心的架构,意味着什么呢?这让我想起《C++并发编程实战(C++ Concurrency In Action)》中的一句话,即"对程序员来说,免费的午餐已经结束了",我们的指令级并行以及单线程的性能应该让位与多线程并行的系统,很遗憾目前已有的DBMS并不是为这种趋势所设计的。
这篇文章主要阐述对《Staring into the Abyss: An Evaluation of Concurrency Control with One Thousand Cores》这篇论文的学习思考,文中列出了七种主流的DBMS并发控制方案,并发现扩展至多核时这些方案都有各自的瓶颈,而这些瓶颈都是独立于实现的,这就意味着这些方案其实很难扩展到多核芯片上,所以得出结论如果想要在多核系统中得到好的性能,要么重新设计现有DBMS的架构来适应多核体系;要么添加硬件消除软件无法解决的瓶颈。
并发方案
首先明确一点,也是很多初学者不太清楚的地方(当然我也是一个十足的菜鸡),也就是DBMS中我们讨论的并发方案其实就是指如何满足事务中的隔离性与原子性[2],也就是如何保证一种在在并发访问中,每一个用户眼只有他一个人访问数据库的假象。
文中主要测试了其中并发控制方案:
- 2PL with Deadlock Detection (DL_DETE)
- 2PL with Non-waiting Deadlock Prevention (NO_WAIT)
- 2PL with Waiting Deadlock Prevention (WAIT_DIE):
- Basic T/O (TIMESTAMP)
- Multi-version Concurrency Control (MVCC)
- Optimistic Concurrency Control (OCC):
- T/O with Partition-level Locking (H-STORE)
其实明眼人都可以看出来这其实就是paper作者之一Andrew Pavlo
在CMU 15-445
lecture17,18,19中锁描述的三类并发控制方案(当然广义来看就是悲观和乐观这两种)。这七种方案不细谈了,从原理讲其实难度都不大,主要难的是工业级别的优化,所以迅速的理解它们的实现原理应该是很容易的。开始下面的内容前必须得了解它们,可以通过CMU 15-445的视频和讲义或者一些博客去了解。
多核的实验环境
目前物理CPU离千核这个目标还是比较遥远,所以论文中使用Graphite这种CPU模拟器搭建了实验的环境。数据库层面是重新实现了一个OLTP DBMS,并没有使用已有的DBMS,主要是为了不受其他额外的因素影响测试。我们注意到其实数据库是OLTP并不是OLAP,所以其实测试还是聚焦于事务实时性。OLTP负载类型中事务具有三个显著的特征:
- 短暂的,即事务的语句比较少。
- 只是接触到索引的一部分数据,不会涉及到全表扫描。
- 基本是重复的。
其实目标CPU架构也很有意思,基本是长这样子的:
其实就是一个芯片上的内核呈32*32的mesh排布,每个核心包含一个包含一个低功耗、有序的处理内核、32KB L1Cache(instruction/data)、512KB L2 Cache和网络路由器,其中每一hop需要两个时钟周期。
paper中提到了使用共享L2 Cache的方案,使用的是NUCA(Non-uniform cache access)架构,刚开始看到有点懵,最后发觉其实就是NUMA(Non-uniform memory access)的理念嘛。
最后值得一提的是实现的这个DBMS可以报告每一个事务在不同组件上花费的时间,这些重要的latency metric可以分为如下六种,记住这些,在本篇文章中非常重要:
USEFUL WORK
:事务实际执行应用逻辑并对系统中的tuple进行操作的时间。ABORT
:回滚做出所有更改的时间。TS ALLOCATION
:分配时间戳的时间。INDEX
:花费在哈希索引上的时间,包括这种并发的哈希索引中latches的消耗。WAIT
:事务等待的总时间。MANAGER
:在 lock manager 或者 timestamp manager上花费的时间。
相关优化
算法无关优化
此次测试的工作负载使用YSCB和TPC-C这两个经典的评估标准,基本上可以让所有的瓶颈无所遁形。其实也可以看出来这篇paper中比较麻烦的一点就是去优化每一种算法,尽可能的消除所有的扩展性瓶颈,然后确定最基本的瓶颈在哪里。首先paper阐述了以下几种算法无关的瓶颈:
- 内存分配,通过实现可根据负载自动调整每线程内存池大小的优化解决此问题。
- 为了提升事务的扩展性,将锁表替换为Tuple加锁,这增加了部分内存开销,但是对于大Tuple来说基本可以忽略不计。
- 在2PL的死锁检测器和T/O中的时间戳分配需要互斥锁,虽然现代操作系统引入了Futex机制这样的方案去使得无冲突时加锁不需要陷入内核,但是这其实这相比于直接内存访问来说仍旧是一个开销非常高的操作。可以参考Jeff Dean大神2012年这张经典的图[3],当然最新的指标可以在[4]中查看:
2PL算法相关
我们前面也提到死锁检测器是2PL算法的主要瓶颈。
-
Deadlock Detection for DL_DETECT
在死锁检测中我们需要构建一个waits-for graph,为了扩展性引入了一种lock-free的算法进行优化,具体的细节文中描述的比较简洁,需要查看下引用中的内容。 -
Lock Thrashing
这其实就是2PL最根本的问题了,就是在核数变多的时候第一阶段的加锁会阻塞所有其他操作此Tuple的事务,在DL_DETECT
中没法避免。下图是随着核数的增加,在YSCB负载中分别设置theta
为不同的参数,其实等价于均匀访问;40%和60%的访问集中在10%的tuple上。
-
Waiting vs Aborting
前面提到的Thrashing
问题可以通过中止一些事务解决,我们可以在DBMS中添加一个超时阈值,使得事务中止并重新启动,不同的阈值会造成不同的性能影响,下图是abort rate
和Throughput
的比较。
T/O算法相关
显然T/O算法中时间戳是一个非常重要的东西,因为所有的正确性都是基于时间戳的,以前只是认为在分布式系统中时间戳分配确实很成一个问题,毕竟据大多数公司都没有谷歌的原子钟[5],所以要获取严格递增的时间戳就非常困难了,不得不引入一个单点来解决这个问题,比如TiKV的PD。
一般单机中使用全局的原子计数器可能已经足够了,但是原子操作的本质其实就是CAS操作,我们暂且抛开缓存一致性这种问题不谈,总线锁的开销就已经很高了[6]。文中给出三种更优的方法去分配时间戳:
atomic addition with batching
:很好理解,其实就是批处理CPU clocks
:一种需要CPU支持的跨内核同步时钟方案hardware counters
:引入硬件解决
我们可以看看不同的时间戳分配算法在多核情况下的表现:
下图是在基础的TIMESTAMP
方案上应用不同的时间戳分配算法在不同冲突下的表现。
Distributed Validation
显然在OCC中最后的Validation操作是非常费时费力的,因为其需要与目前系统中存在的所有事务进行比较(当然指验证有两种方案),这显然需要latches,那这就确确实实是一个问题,paper中提到可以使用[7]中的方案解决,事实上我还没来得及看这个引用,后面有需求可以看看。
相关分析
Read-Only Workload
下图是每个事务以只读的形式访问16个单独的Tuple,这可以为后面的测试提供一个baseline以做对比。其实可以看到在只读事务中2PL才是真正的王者,因为它们基本是需要原地读,而乐观的解决方案大都需要拷贝Tuple,这其实是我没有想到的,因为一般都认为MVCC对读是很友好的,但现实狠狠的扇了我的脸,这也告诉我们性能测试的重要性,切忌不可用猜去定位问题。在乐观解决方案中显然时间戳排序成为了最大的瓶颈。
Write-Intensive Workload
当修改负载为写时,一切变的不太一样了,事务中一次访问16个Tuple,有50%的概率写,paper分别比较了中等冲突与高冲突两种负载方案。首先我们来看看在medium
争用下的情况:
可以看到DETECT
是一种扩展性最差的做法,因为可以看到其wait
时间很多,因为其需要等待其他事务。而2PL中的NO_WAIT
和WAIT_DIE
是扩展性最好的两种情况,但是它们都有很高的Abort
时间,其实也很好理解,虽然单核中Abort开销比较低,但是在多核中回滚multiple tables, indexes, materialized views的开销就很高了。
在高冲突场景中,可以看到所有的操作性能都非常差,它们都花了大量的时间在Abort
上。
一图胜过千言万语,可以看到在目前已有的所有算法在Theta
到0.6时性能急剧下降,也就是写比例到40%的时候。
Working Set Size
显然执行更多语句的事务发生冲突的概率就越大,对于2PL来说这会增加锁的持有时间,但是对于T/O族算法来说这会减少时间戳的分配,下图在512核上采用theta=0.6
进行测试:
在事务较小的时候2PL拥有更好的性能,因为几乎没有死锁和Abort,在读/写工作负载(图8,9)下显示时间戳分配不再是主要的瓶颈,而图12中显示T/O相关的算法比DETECT
更能应对多冲突情况。
Read/Write Mixture
下图是在64核配置上每一个事务执行16个请求,然后改变读比例,并设置theta=0.8
,可以看到在拥有少量部分写事务时MVCC还是王者(读比例在0.6到0.8时)。读更多的时候还是2PC这种原地读取性能更高。
一些有趣的结论
其实可以从上面各种各样的分析看出现有的DBMS并发控制算法在多核扩展上的瓶颈主要来源于如下几个方面:
lock thrashing
preemptive aborts
deadlocks
timestamp allocation
memory-to-memory copying
我们可以得到如下结论:
- 对于
lock thrashing
,可以通过主动Abort
某些事务来增加性能。 - 对于高竞争工作负载,死锁预防(NO_WAIT)要比死锁检测好的多。
- 既然某种算法没有办法应对所有的情况,那我们就可以采用混合方案,比如Mysql的解决方案,即只读事务使用MVCC,其他使用DL_DETECT[8]。
- 时间戳分配对于T/O来说是一个无法避免的瓶颈,硬件支持是一种趋势。
下图是对不同并发算法的总结:
多核与分布式系统
分布式系统是何其的美妙,回到最初我们为什么要使用分布式系统的理由,即以下三点:
- 性能:衡量一个系统处理各种任务的能力。
- 可用性:面对各种异常时可以正确提供服务的能力。
- 可扩展性:通过扩展集群机器规模提高系统性能 (吞吐量、响应时间、 完成时间)、存储容量、计算能力的特性。
这篇文章中其实就告诉我们以目前已有的数据库架构来说,多核扩展不是未来的主流,首先会有各种各样的瓶颈,其次也不具备可用性与可扩展性。
但是分布式系统也有问题,以分布式事务举例,其实大多还是2PC及其变种(Percolator等等),其需要一个广播保证正确性,且要求全部回复,而且分布式系统中通信的开销也是一个问题。所以看起来单个多核具有大量 DRAM 的机器可能优于分布式DBMS。
所以未来的发展可能是以下两点:
- a shared-nothing implementation between nodes
- a multi-threaded shared-memory DBMS within a single chip
所以可预料的,未来的分布式数据库领域仍旧会在很长时间内发光发热,除非有新的CPU架构或者新的DBMS架构出现。
总结
一篇非常有意义的文章,指引了未来数据库发展的方向。但是这已经七年以前的论文了,在这七年里数据库领域蓬勃发展,各类NewSQL,云原生数据库层出不穷,分布式数据库已经足够稳定与高效,扩展能力,可用性也已经足够优秀,甚至存储的成本都在不停的下降,所以单机多核带来的问题看起来已经淡化,也算是应验了文中提出的一种解决方案了,所以上面提到的并发控制算法依旧是很优秀的,已有的经验仍旧有效。
敬佩这类做开创性工作的先驱,这些人真正做到了从历史看未来。
参考:
- Staring into the Abyss: An Evaluation of Concurrency Control with One Thousand Cores
- 事务还不懂?这一篇文章就够了!详解从事务到分布式事务
- https://gist.github.com/jboner/2841832
- https://colin-scott.github.io/personal_website/research/interactive_latency.html
- Spanner: Google’s Globally-Distributed Database
- 从false sharing到缓存一致性,这其实与我们息息相关
- High-performance concurrency control mechanisms for main-memory databases
- 避免幻读 : next-key锁与MVCC