引言
EPaxos(Egalitarian Paxos)作为工业界备受瞩目的下一代分布式一致性算法,具有广阔的应用前景。但纵观业内,至今仍未出现一个EPaxos的工程实现,甚至都没看到一篇能把EPaxos讲得通俗一点的文章。EPaxos算法理论虽好,但由于其实在晦涩难懂,工程实现上也有很多挑战,实际应用落地尚未成熟。
本文旨在通俗易懂地介绍EPaxos算法,由浅入深、一步一步的让只有Paxos或Raft等分布式一致性算法基础的同学都能轻易看懂EPaxos,真正将晦涩难懂的EPaxos,变的平易近人,带入千万家。
在[《一文了解分布式一致性算法EPaxos》] 中,从Paxos的问题引出EPaxos,介绍了EPaxos的基本概念与直观理解,相信读者已经对EPaxos有了整体的印象。
本文将从Paxos与EPaxos对比的角度,介绍EPaxos核心协议流程。上一篇文章最后留下的思考题,相信在阅读完本文后,都能找到答案。阅读本文需要一些Paxos或Raft等分布式一致性算法背景。
一 EPaxos基本思想 EPaxos是一个Leaderless的一致性算法,无需选举Leader,任意副本均可发起提议。
Leaderless也可以看作每个副本都是Leader,从Multi-Paxos或Raft的角度看,如果使用多Group,将每个Leader划分到不同的Group,每个副本担任一个Group的Leader,同时担任其它所有Group的Follower,好像也可以做到类似Leaderless的效果。
使用多Group实现的Leaderless,每个Group独立的对一系列Instance达成一致,每个Group产生一条Instance序列,不同Group产生的Instance彼此独立,不能确定先后顺序。因此跨Group的一致性是一大问题,在一致性层面无法解决,往往需要在上层使用分布式事务来解决。
EPaxos解决了这个问题,实现了真正的Leaderless。EPaxos通过跟踪Instance之间的依赖关系,确定不同Group产生的Instance的相对顺序,然后通过排序将多Group产生的多条Instance序列合并成一条全局的Instance序列,实现了跨Group的一致性,也即实现了真正的Leaderless。
EPaxos先运行共识协议,使各副本对Instance的值和Instance依赖的相对顺序达成一致,随后运行排序算法,基于前面已经达成共识的Instance的相对顺序,对Instance进行全局排序,最终得到一致的全局Instance序列。
以上是站在Multi-Paxos或Raft使用多Group实现Leaderless的角度引出EPaxos的基本思想,实际Group是一致性算法之外的概念,这里引入Group只是为了方便介绍,实际EPaxos中并没有Group的概念,但与Paxos或Raft类似,可以在EPaxos之上实现多Group。
二 EPaxos的Instance
EPaxos的Instance与Paxos有所不同,Paxos的Instance事先分配序号,但EPaxos的Instance不事先分配序号,各副本可以并发的乱序提交,但跟踪记录Instance之间的依赖关系,最后根据依赖关系排序。这里先总结一下不同点:
EPaxos的Instance与Paxos的不同点
Paxos的Instance由全局连续递增的InstanceID标识,InstanceID也是Instance的序号,全局唯一,连续递增。
EPaxos的Instance空间是二维的,每个副本拥有其中一行,因此由二维的R.i标识,其中R为副本标识,i为副本内连续递增的整数,每开始一个新的Instance就加一。副本R拥有的Instance序列为R.1,R.2,R.3,...... R.i,......
EPaxos的Instance相对Paxos还有一些额外的属性:
state标识Instance当前的状态,可取值pre-accepted、accepted、committed。因为EPaxos的Instance的状态比较多,所以需要专门的state字段标识。
deps为依赖的Instance集合,存放所有依赖的Instance的标识,即要在前面执行的Instance。deps保存了Instance之间的相对顺序,后续将基于deps对Instance进行排序。
seq为Instance的序列号,其值为deps中所有Instance的seq的最大值加一,反映Instance提议的顺序,后续排序的时候使用。
EPaxos的Instance的deps和seq属性与Instance的值一样,也需要在各副本之间达成一致,后续各副本将独立的基于deps和seq对Instance进行排序。因为EPaxos的排序算法是确定性的,各副本基于相同的deps和seq对Instance进行排序,最终会得到一致的全局Instance序列。
将Instance看作图的顶点,deps就是顶点的出边,图的顶点和边都确定并在各副本之间达成一致之后,各副本对图进行确定性的排序,最终得到一致的Instance序列。
三 EPaxos共识协议
Paxos提交一个值需要两阶段,而EPaxos的Instance多了依赖集合deps和序列号seq,为了确定这些属性的值,EPaxos比Paxos多了一个阶段。完整的EPaxos有三阶段,但并非每个阶段都是必须的,下表将Paxos与EPaxos的协议流程进行对比:
Paxos与EPaxos的协议流程对比
EPaxos与Paxos相比,Prepare阶段和Accept阶段类似,但EPaxos多了PreAccept阶段,也是EPaxos最关键的一个阶段。正因为EPaxos多了PreAccept阶段,Instance的状态更多了,所以引入专门的state属性来标识Instance当前所处的状态(pre-accepted、accepted、committed)。没有引入Prepare阶段的状态,是因为Prepare阶段没有提议值,可以通过本地有无提议值来与其它状态区分。通常情况下,EPaxos只运行PreAccept阶段,即可Commit,Prepare阶段和Accept阶段都能跳过。
EPaxos与Paxos类似,在任意阶段如果发现Instance被抢占,都需要回退到Prepare阶段重新开始。
1 Prepare阶段
Prepare阶段的作用,EPaxos与Paxos类似,都是为了争取提议权,同时学习之前已经提议的最新值。在EPaxos中,因为每个副本都拥有各自独立的Instance空间,在自己的Instance空间上提议,相当于Multi-Paxos的Leader,所以与Multi-Paxos类似,通常情况下可直接跳过Prepare阶段,直接从下一阶段开始。
2 PreAccept阶段
PreAccept阶段是EPaxos特有的,其作用是确定Instance的依赖集合deps和序列号seq,同时尽量让提议值、deps和seq在各副本间达成一致。若PreAccept阶段已经达成一致,则直接到Commit阶段(Fast Path),否则需要运行Accept阶段,然后才到Commit阶段(Slow Path)。
PreAccept阶段是如何确定Instance的依赖集合deps和序列号seq的呢?其实比较简单,从副本本地已存在的Instance中,收集需要在前面执行的Instance,即本地deps集合,本地seq即本地deps中的所有Instance的seq的最大值加一。最终的依赖集合deps取大多数副本的本地deps集合的并集,最终的序列号seq则取他们的本地seq的最大值。
各副本并发提议的时候,不同副本的本地deps和本地seq都可能不一样,那么PreAccept阶段如何才能达成一致呢?如果有足够多的副本(Fast Quorum)的本地deps和本地seq都相同,则已经达成了一致。否则,最终的依赖集合deps取大多数(Slow Quorum)副本的本地deps的并集,最终的序列号seq取它们的本地seq的最大值,然后运行Accept阶段达成一致。
PreAccept阶段的Fast Quorum始终包含提议者,会在后面讨论原因。Fast Quorum的值不小于Slow Quorum。假设副本总数为N,可容忍F个副本同时失效,N = 2F + 1,则Fast Quorum = 2F,优化后的EPaxos可优化至F + [ (F + 1) / 2 ],Slow Quorum = F + 1。Fast Quorum取值的推导这里先不做介绍,会在后续文章中详细讨论,Slow Quorum即大多数副本,与Paxos的Accept阶段相同。
3 Accept阶段
Accept阶段的作用,EPaxos与Paxos类似,但Paxos只将提议值同步到大多数副本,EPaxos需要将提议值、deps和seq都同步到大多数副本,一旦形成多数派则决议达成。若在PreAccept阶段已经达成决议,则可跳过Accept阶段,直接进入Commit阶段。
4 Commit阶段
Commit阶段的作用,EPaxos与Paxos类似,都是将达成的决议异步发送给其它副本,让其它副本学习到决议,不同的是EPaxos的决议不仅包括决议值,还包括deps和seq。
四 EPaxos排序算法
与Paxos不同,EPaxos的Instance提交后,它们的顺序还没有确定,因此EPaxos需要一个额外的排序过程,对已提交的Instance进行排序。当Instance被提交,并且其依赖集合deps中的所有Instance也都提交后,就可以开始一次排序过程。
将EPaxos的Instance看作图的顶点,Instance的依赖集合deps看作顶点的出边,Instance的值和依赖集合deps达成一致后,图的顶点和边就在各副本之间达成一致,因此各副本会看到相同的依赖图。
EPaxos对Instance排序的过程,类似于对图进行确定性的拓扑排序。但需要注意的是EPaxos的Instance之间的依赖可能形成环,即图中可能会有环路,因此不完全是拓扑排序。
为了处理循环依赖,EPaxos对Instance排序的算法需要先寻找图的强连通分量,环路都包含在了强连通分量中,如果把一个强连通分量整体看作图的一个顶点,则所有强连通分量构成一个有向无环图(DAG),然后对所有的强连通分量构成的有向无环图进行拓扑排序。
EPaxos排序算法的流程如图1所示,其中由背景色圈起来的部分是强连通分量:
EPaxos排序算法
寻找图的强连通分量一般使用Tarjan算法,它是一个递归算法,实测发现递归实现很容易爆栈,也给工程应用带来了一定的挑战。
不同强连通分量中的Instance按照确定性的拓扑顺序排序,同一强连通分量中的Instance是并发提议的,理论上可以按任意确定性规则排序。EPaxos为每个Instance计算了一个seq序列号,seq的大小反映了Instance提议的顺序,同一强连通分量里面的Instance按照seq大小排序。实际seq可能重复,并不能保证全局唯一递增,EPaxos论文并未考虑到这一点,实际可以使用seq加副本标识排序。
事实上,随着新的Instance不断的运行,旧的Instance可能依赖新的Instance,新的Instance又可能依赖更新的Instance,如此下去可能导致依赖链不断延伸,没有终结,排序过程一直无法进行,形成活锁。这也是EPaxos工程应用的一大挑战。
因为Instance排序算法是确定性的,各副本基于一致的依赖图对Instance排序后,将得到一致的Instance序列。
五 EPaxos案例
下面使用一个具体的案例,介绍EPaxos的核心协议流程,如下图所示,系统由R1、R2、R3、R4、R5,5个副本组成,水平方向代表时间,值A、B、C的提议流程如图所示。
EPaxos共识协议
案例中各Instance的属性取值如下表所示:
EPaxos核心协议流程案例中Instance的属性
1 提议值A
首先R1运行PreAccept阶段发起提议值A,它首先获取自己的本地deps和本地seq,此时它本地还没有任何Instance,所以本地deps为空集,本地seq为初始值1,它持久化提议值A、本地deps和本地seq。
然后R1向R2、R3广播PreAccept(A)消息,消息携带提议值A、本地deps、以及本地seq(图中未标示),此时R1、R2、R3组成Fast Quorum。PreAccept消息可以只广播给Fast Quorum中的副本,EPaxos论文中将这种优化称为Thrifty优化。
R2、R3收到PreAccept(A)消息后,分别获取自己的本地deps和本地seq,与R1类似,本地deps为空集,本地seq为1,持久化后回复R1。
R1收到Fast Quorum中的副本的本地deps和本地seq都相同,决议达成,最终的deps为空集,seq为1,运行Commit阶段提交。
2 提议值B
接下来R5运行PreAccept阶段发起提议值B,此时它本地也还没有任何Instance,所以本地deps为空集,本地seq为初始值1。R5本地持久化后,向R3、R4广播PreAccept(B)消息。
R4回复的本地deps为空集,本地seq为1,R3此时本地已经有了值A的Instance,因此R3回复的本地deps为{1.1},也即图上标示的{A},本地seq为2,即A的Instance的seq加一。
Fast Quorum中的副本的本地deps和seq不都相同,因此需要运行Accept阶段,最终的deps取大多数副本的本地deps的并集为{1.1},也即图上标的{A},最终的seq取大多数副本的本地seq的最大值为2,通过Accept阶段,将提议值B、最终的deps、最终的seq达成多数派。最后运行Commit阶段提交。
3 提议值C
最后R1运行PreAccept阶段发起提议值C,此时R1本地已经有了值A的Instance,因此本地deps为{1.1},也即图上标示的{A},本地seq为3。R1本地持久化后,向R2、R3广播PreAccept(C)消息。
R2和R3此时本地已经有了值A和值B的Instance,因此R2、R3回复的本地deps为1.1,5.1},也即图上标示的{A,B},本地seq为3,即B的Instance的seq加一。
Fast Quorum中除了提议者R1以外的其它副本的本地deps和seq都相同,因此决议已经达成,最终的deps为{1.1,5.1},也即{A,B},seq为3,运行Commit阶段提交。
4 排序
提议值A、B、C的Instance按照它们的依赖集合deps画出的依赖关系如下图(左)所示:
提议值A、B、C的Instance之间的依赖关系(左),排序之后的顺序(右)
A的Instance的deps为空集,因此没有出边;B的Instance的deps为{A},因此有一条出边指向A;C的Instance的deps为{A,B},因此有两条出边,分别指向A和B。
依赖关系图中没有循环依赖,已经是一个有向无环图(DAG)。因此顶点A、B、C各自是一个强连通分量,进行确定性的拓扑排序后得到它们的顺序:A <— B <— C,如图如图(右)所示。
六 EPaxos讨论
1 Instance冲突
EPaxos引入Instance冲突(Interfere)的概念(与Parallel Raft类似,与并发冲突不是一个概念),若两个Instance的值之间没有冲突(例如访问不同的key),则它们的先后顺序无关紧要,甚至可以并行处理,因此EPaxos只处理有冲突的日志之间的依赖关系。
EPaxos的Instance的依赖集合deps,存放的是需要在该Instance之前执行的Instance。引入冲突之后,deps中存放的是与该Instance冲突的Instance。
冲突是一个与具体应用相关的概念,引入冲突之后,所有Instance不再是全序的,变成了偏序的,减少了依赖,降低了Slow Path的概率,提高了效率。
2 Fast Quorum
关于Fast Quorum取值的推导,留待后续文章介绍,这里先讨论下,为什么PreAccept阶段的Fast Quorum始终包含提议者。
EPaxos在每个阶段,提议者总是本地先持久化成功之后,再广播消息给其它副本。也就是说提议者总是在Quorum中,因此判断是否达成Quorum时,提议者总是可以算一票。
在PreAccept阶段,提议者将其本地deps和seq包含在了PreAccept消息中,作为其它副本计算它们本地deps和seq的基础,使得提议者的本地deps和seq总是包含在最终的deps和seq中,因此PreAccept阶段的Fast Quorum始终包含提议者。
EPaxos总是先本地持久化成功之后再广播给其它副本,这样可以减小Fast Quorum,但也导致本地持久化与网络消息收发不能并行进行,降低了一些效率,同时也使得提议者不能容忍本地磁盘损坏的情况,这些都是EPaxos工程应用必须面对的问题。
Fast Quorum的值为什么不会小于Slow Quorum呢?这里无需推导Fast Quorum的取值,从直观上就可以得出这个结论。在Paxos中一个副本提议一个值,所有副本只会有两种结果,接受或者不接受这个值。而在EPaxos中每个副本都可能给出不同的deps和seq,因此需要更多的副本给出相同的结果才能保证在有副本失效后能恢复出正确的结果。
七 EPaxos伪代码
到这里,相信读者已经可以看懂EPaxos核心协议流程的伪代码了。EPaxos核心协议流程伪代码如下图所示,为了简单,省略了提议ID(Proposal ID,或者叫Ballot Number,投票编号)相关的部分,这部分与Paxos相同。
伪代码中将日志视为命令(Command),每个Instance对一条Command达成一致,每个副本使用一个二维数组cmds保存收到的Command。
EPaxos核心协议流程伪代码
八 总结
EPaxos通过显示维护Instance之间的依赖关系,不仅去除了对Leader的依赖,还使得Instance可以并发的乱序提交,可以更好的进行Pipelining优化,同时显式维护依赖也使得乱序执行成为可能。EPaxos支持乱序确认、乱序提交、乱序执行,理论上可以有更高的吞吐量。同时也可以看到一些EPaxos工程应用的挑战,这些都是EPaxos工程落地要解决的问题。
本文从Paxos与EPaxos对比的角度,介绍了EPaxos核心协议流程,但EPaxos的内容决不仅仅只有这些,特别是Failover场景下,如何保证日志序列的顺序性。
思考
最后留下几个思考题,感兴趣的同学可以先思考思考:
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8