本文试图讨论这几个问题:
网站建设哪家好,找创新互联建站!专注于网页设计、网站建设、微信开发、微信小程序开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了柳城免费建站欢迎大家使用!
本文旨在阐述Fault-Tolerant Transaction的几种实现模式。虽然乍一看它们可能都是Raft+KVEngine +Concurrency Control,容易被认为是同一类方法,但实际上的差异很大,在讨论时不应该忽视它们之间的差异。
一、基本概念
讨论的Fault-Tolerance,指的是通过网络通信的多个计算机节点,在部分节点发生Stop Failure的情况下,仍然尽力保证可用性;
不讨论具体的Fault-Tolerance方法,默认读者对Raft等算法有基本理解;
也不讨论具体的Concurrency Control方法,默认读者对其有基本的理解;
会涉及到Spanner、TiKV、MongoDB等具体的数据库。
1、基于RSM的Fault-Tolerant KV
Replicated State Machine最早应该是在『Implementing fault-tolerant services using the state machine approach』提出。它是一种很简单实用的实现容错的方法,核心思想是:几个状态机具有相同的初始状态,并且按照同样的顺序执行了同样的命令序列,那么它们的最终状态也是一样的。由于状态一样,那么任意一个状态机宕机,都可以被其他的代替,因此实现了Fault Tolerant。
这里提到了几个概念,命令、执行顺序、状态机,它们都是抽象概念,对应到具体的应用场景才有实际意义。在KVEngine的场景下,命令就是Put/Get等操作,状态机就是KVEngine本身,而执行序列,则由Replication Log决定。
既然提到了RSM和KV,那么基于RSM的KV也就呼之欲出了。把发到KVEngine的操作先用Raft复制一遍,在Apply的时候扔回到KVEngine执行,于是我们就得到了一个Fault-Tolerant的KVEngine。
看起来很简单,但我在这里显然忽略了很多细节:
2、基于RSM的事务
我们来考虑***一个问题,RSM中的命令,可以直接是一个事务吗?
既然Raft都是串行Apply了,那么看起来把事务的所有操作作为一个命令扔到状态机执行并没有什么问题。
但问题在于,实际中的事务是交互式的,也就是包含了if-else等逻辑的,并且逻辑还可能依赖了数据库系统外部的状态,所以不能简单地用Write Batch + Snapshot来实现一个事务,还是要有Concurrency Control的逻辑。
为了解决Concurrency Control的问题,我们在Raft Leader上,实现一个Lock Table和Transaction Manager。拿S2PL方法举例:
这里举的例子是S2PL,但对于其他的并发控制方法也基本通用。例如Snapshot Isolation,事务开始时获得KV的Snapshot,读操作都走Snapshot,写操作获得写锁,数据Buffer在本地,事务提交时检查[begin, end]之间有没有写冲突,没有的话则通过Raft写事务日志,在Apply事务日志之后,把写操作应用到KVEngine,***释放写锁。
这种方法接近Spanner的做法,它具有几个特点:
3、基于共享存储的事务
重新看一下上面这个模型,复制协议所做的事情非常简单,和其他模块的耦合也很小,仅仅是维护一个有序的Log,因此,我们可以把它从share-nothing推广到share-storage的模型中。
也就是说,我们把普通的单机事务引擎,放到一个高可用的存储上,就得到了基本可用的Fault-Tolerant 事务引擎了,连复制协议也不需要实现的。
不过事情显然不会这么简单:
4、基于高可用KV的事务
回到一开始的***种方案,在一个节点实现了KV、Raft、Lock Table、Transaction Manager,看起来耦合度比较大了,我们能不能对其进行分层,进一步简化呢?例如Google的经典做法,基于GFS实现Bigtable,基于Bigtable实现Percolator,Layered设计易于迭代、易于开发、易于调试。
因此我们可以考虑把KV层单独抽离出来,基于KV去实现Lock Table、Txn Manager:
看过Percolator、TiKV设计的应该会比较熟悉,它们就是基于一个高可用的KV,把事务的状态都下沉到KV中。这种设计很容易拓展到分布式事务的场景,如果KV能够scale,那么上层的事务也能够scale了。
5、基于单机事务引擎实现高可用事务
上面的方案看起来都比较简单,不过有一个细节不容忽视:锁基本都是在复制协议提交之后才会释放,换句话说事务持有的锁会从事务开始直到多个节点写完日志,经历多次网络延迟、IO延迟,并且在拥塞情况下会面临排队延迟的风险。而锁意味着互斥,互斥意味着事务吞吐降低。
翻译一下:
不过这里存在一个问题:
暂且不做回答,我们再看***一种方案,基于单机事务引擎的高可用事务。
在正常的单机事务流程中,增加一个复制的环节:本地事务提交之后不是立即返回用户,而是写binlog,等待binlog复制到其他节点之后再返回用户。
这种方式的事务延迟,看起来还是本地事务的延迟,加上复制日志的延迟;但相比于之前的方案,本地事务可以先提交,锁可以提交释放,总体的事务吞吐相比之下会有所提升。
看起来甚至比之前的方案更加简单,事务和复制模块得到了***的分离,但这里忽略了一个复杂的问题:
由于直接复制Journal会引起一系列复杂的耦合问题,大部分数据库都选择单独写一个binlog/oplog来实现复制,不过在实现时可以做优化,因为如果真的写两个log会有原子性的问题(一个写成功了另一个没写成功)以及IO放大的问题。
这里的设计空间比较庞大,不做详细讨论,仅仅考虑在简化的模型下复制顺序的问题。
对于并发执行的事务,为了确定复制顺序,这里维护一个称之为OpTime的自增ID。后续的复制会按照OpTime的顺序,OpTime小的先复制。如果OpTime仅仅是在事务的开始和结束之间分配,会带来问题:
因此,OpTime的分配需要有更强的限制:对于并发且有冲突的事务,OpTime的顺序要和事务的Serialization Order一样:
在S2PL的场景中,我们把OpTime分配放到Lock之后Commit之前,即可满足这个要求。因为按照S2PL的调度,事务的Commit-Point就是Lock完成和Unlock之间。对照上面的例子,事务T2的OpTime被推迟到T1之后,复制的顺序也会相应改变,不会发生先前的异常了。
推广到其他的并发控制方法也是类似,例如上面的Snapshot Isolation。提交之前会检查[begin, end]是否有冲突,有冲突直接重启事务。相当于在[begin, end]区间内分配OpTime即可。
这种方法通过OpTime,保留了Transaction Serialization Order和RSM的Order之间的关系:
不过这里留下了一个问题,留待读者思考:
如何按照OpTime复制,因为有事务Abort的情况,OpTime做不到连续自增,仅仅是单调自增。
二、对比
***种其实是Spanner,第二种是TiKV、Percolator,第三种是MySQL、MongoDB。
它们在复制上的区别:
从复杂度来看:
从事务并发的角度来看:
从读写开销的角度来看:
不过这仅仅是理论上的分析,实际的复杂度、性能,很大程度上取决于实现而非理论。
三、总结
如果我们从很粗的层面来看,会觉得这些系统不过都是几个技术点的组合,而每一个技术点看起来都很简单,进而觉得事务系统不过是如此。
但实际上事务系统绝非简单的KV+Raft+Snapshot Isolation,它们之间不同的组合方式,会最终造就不同的系统。
本文留下了很多问题,RSM的Order往往认为是全序的,而Transaction 的Serialization Order是偏序的(偏序关系由事务冲突定义),它们之间如何统一?
RSM的Checkpoint和Transaction Checkpoint的统一?RSM的Recovery和Transaction Recovery的关系?写两条日志的系统(journal和binlog)两条日志之间的关系是什么?
新闻名称:事务系统实现模式很简单?你确定没忽视这些差异?
本文URL:http://www.shufengxianlan.com/qtweb/news48/302098.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联