简单介绍 EPaxos 算法。本文还结合论文作者提供的 tla+ 作出了详细的解读。
Epaxos的简介
EPaxos是一个Leaderless的算法,具有以下的优点:
- Client可以任意选择一个节点发送Command,这个Command的Commit的路径大部分都不会干扰到其他的Command。而在诸如Raft的算法中,必须有一个Master显式向其他节点复制日志。并且也避免了Client可能和一个跨地区的Master交互的情况。
- 因为没有Leader选举的过程,所以整个集群的可用性变高了。
讨论Basic Paxos算法,当收到来自Client的Command后,某个replica会尝试成为一个尚未被使用过的instance的Leader,方式是发送Prepare消息。其他replica对Prepare的返回值,首先包含了它们认为的已经被chosen的Command(如果出现这种情况,Leader后面也需要使用这个Command)。在返回值中还会包含不会再ack早前的Leader发送的更旧的消息的承诺。在收到majority的对Prepare的ack之后,这个Leader会发送Accept消息。当Accept消息再被majority确认后,Leader会在本地记录Command已被Commit,并且异步通知Peer和Client。
容易发现,Paxos算法需要Prepare和Accept两个阶段,才能完成一个Command的提交。这是不经济的,因为即使从Raft的经验来看,只要Leader一直存在,那么一轮就可以决定一个instance,也就是Log Entry的值了。此外Basic Paxos中如果多个Leader打架,那2轮都不够。因此MultiPaxos中引入了stable leader(又称为distinguished proposer)的机制来处理。
EPaxos的设计目标是:
- 减少Commit时延
- 优化Load Balance
- 对于较慢甚至故障的节点,提供一个优雅的降级方案
为此,EPaxos需要做到:
- 每个节点都可以同时作为Proposer(Command Leader)
- 每个Proposer在和最小数量的节点交流后就可以Commit,并且交流的次数也要最小化
类似generic broadcast算法,以及Generalized Paxos算法,会广播消息。但如果两个Command不互相影响,实际上并没有必要强制他们有一个consistent的顺序。
而在实际workload上,可以认为大部分的Command是这样的,也就是说它们修改的是不同的对象。 - Quorum的组成应当是可变的
EPaxos做到这些,得益于它给Command定序的方案。在这之前,诸如MultiPaxos和GenerizedPaxos选择通过一个Leader来选择顺序;而在 canonical Paxos和Mencius中选择将这些Command放入已经预先分配好的instance空间中。
EPaxos的方案是当对一个Command投票时,每个参与者需要附带上对这个Command的Order Constraints。EPaxos能够保证所有正常的节点能够以提交同样的Command和Order Constraints。
简介一下EPaxos的方案。当一个Command被发送给某个replica时,该replica就被称为Command Leader。这个称呼特意区别于MultiPaxos中的Leader,因为显而易见,同时可能存在多个Command Leader。
如果Command之间不冲突,如下图的左边所示,两个Command分别更新obj_A
和obj_B
。为了Commit这个Command,可以走论文中说的Fast Path。Command Leader需要和F + floor((F + 1) / 2)
(称为fast-path quorum)个Peer交互。其中F是允许失败的节点的个数,对应到下图,F为2。特别地,对于常见的3节点集群,Fast-path Quorum为2;对于5节点集群,Fast-path Quorum为3,都等于传统的简单多数。
【Q】这里有个疑问,不同对象的先后顺序,有影响么?比如实际上一个Client先于obj_B
写obj_A
,但obj_B
先于obj_A
被Commit,当Client收到obj_B
的时候读取obj_A
,那么可能读到obj_A
的stale的数据呢?可以看下面的Execution linearizability说明。
如果两个Command互相冲突,如下图的右边所示,C3和C4同时更新obj_A
,就不能走Fast Path了。在提交的时候需要附带上Order Constraint。为了保证所有的Replica在存在部分节点故障的情况下,都能提交相同的Order Constraint,需要另一轮的通信。这一轮通信中,Command Leader需要和F + 1
个Peer交互。这就是Slow Path,F + 1
也就是传统意义上的简单多数Quorum。
EPaxos和其他算法的比较
在MultiPaxos中,Leader会负责Propose所有的Command。这样会导致Leader处理O(n)个请求,同时非Leader节点只会处理O(1)个请求,负载十分不均衡。
为了解决MultiPaxos的问题,Mencius选择逐Command将Paxos Leader在多个节点之间轮换。也就是说节点R_id
拥有instance i,当i满足(i mod N) = R_id
。这里的坏处是,每一次Commit Command A之前都需要询问所有节点。否则按照这个轮换顺序,另一个依赖于A的Command B可能在Command A之前被提交。这个询问过程会产生两个后果,首先是整个Commit的速度由最慢的那个Replica决定;其次是当一个节点Fail后,整个过程将block住,直到其他节点发现这个节点Fail掉了,并替他返回一个NoOp。
FastPaxos为了解决从Propose到Commit的延迟,选择让Client同时向所有的Replica发送Propose。MultiPaxos一样使用了Stable Leader,不仅用来开启voting round,也用来仲裁各个Acceptor的顺序(考虑到每个Acceptor可能以不同顺序收到)。
Generalized Paxos的方案是当Command之间不互相影响时,对它们乱序提交。Replicas learn commands after just two message
delays—which is optimal—as long as they do not interfere。TODO 后面看不懂了。
EPaxos相对于Generalized Paxos有三点优势:
- EPaxos的Fast Path Quorum Size比Generalized Paxos的正好小一个,能够减小延迟(应该是木桶效应吧),以及总的消息量
- EPaxos解决冲突,也就是Slow Path只需要额外一轮,但Generalized Paxos需要两轮
- 对于three-site replication而言,EPaxos即使在所有Command都冲突的情况下,只需要和距离Proposer最近的Replica通信一轮,即可Commit
EPaxos和Fast/Generalized Paxos的Fast Path的一个重要区别是,EPaxos需3 message delays来Commit,但Fast等只需要2个。但一般EPaxos的第一个Delay可以忽略掉,因为Client和最接近的Replica通常都在一个数据中心中。
S-Paxos TODO
EPaxos的实现
结合了两篇论文的内容
预备内容
在允许F个失效节点的分布式系统中,至少存在N=2F+1个节点。对于每一个节点R,它拥有的instance写为序列R1, R2, …。在每个instance中,只有一个Command能被选出。需要注意的是,R1、R2等instance的order,并不是已经确定的,而是由协议来动态确定。
Commands γ和δ是interfere的,当存在一系列指令Σ使得顺序执行Σ,γ,δ不等价于顺序执行Σ,δ,γ。这里不等价包含它们会导致不同的状态机状态,或者导致在这些序列中读取到的值是不同的。
性质
- Nontriviality
所有被Commit的Command都由Client发出。 - Stability
对于任意Replica,不会丢失已经Commit的数据。
进一步地,如果在t1时刻,Replica R在Q.i这个instance上提交了Command Y,那么在t1之后的任意时刻t2,R上的instanceQ.i的值也是Command Y。
【Q】这里有个疑问,Q是哪里来的?不应该是R么?应该不矛盾,作为Acceptor,R也会有Q拥有的instance的信息的,不然怎么投票? - Consistency
同一个instance在Commit之后,就在各个Replica上的值都是一样的。 - Execution consistency
如果两个interfere的Command Y和Z在任意Replica上被成功Commit了,那么所有Replica上的执行顺序是一样的。 - Execution linearizability
如果Client对Command Y和Command Z是serialized的,比如说在Command Y被Commit之后,才会Propose Command Z,那么所有的Replica都会先执行Y再执行Z。 - Liveness
活性
基础版本
基础版本的fast-path quorum是2F
,后面优化版本的才是F + floor((F + 1) / 2)
Commit Protocol
Commit Protocol解决两个问题:
- Commit Command
- Determine Order Between Commands
在论文的Figure2中描述了Commit Protocol的整个过程,在Commit过程中,并不是每个过程都会走到。如果一个Command没有冲突,就可以走Fast Path,这也意味着走完Phase 1就能够直接提交了。Slow Path还需要额外的Phase 2(Paxos-Accept phase)。如果我们进行failure recovery,则需要运行Figure3描述的Explicit Prepare过程。
当一个Replica收到来自Client的Command γ后,会给这个Command γ分配一个全局唯一的instance number,例如R1、Q2等。全局唯一是通过组合Replica的名字,以及在Replica中递增的序列号保证的。这里Replica是可以将多个操作组合到一个instance中的。除了分配instance number之外,Replica还会给Command分配一个dependency set(deps)和一个sequence number(seq),称为attribute。Dependency set包含了每个Replica中最高的和自己冲突的instance编号。Sequence number会选取一个大于dependency set中所有值的值作为初始值。The originating replica derives the dependency set and sequence number for a new instance from all other instances it has knowledge of。对于每个修改了新Command操作对象的Replica,需要持久化它们的最高的instance number和sequence number。
Command Leader通过PreAccept消息,即(γ,seqγ,depsγ,pre-accepted)
,将Command以及deps和seq发送给至少fast-path quorum(在这里是2F)个Replica。每个Replica在收到后,会根据自己的cmds log来更新Command γ的deps和seq,并将γ和新的attribute记录在自己的log中,然后回复给Command Leader。
如果Command Leader满足了fast-path quorum,并且所有更新了的attribute(seq和deps)是一样的(根据Figure2的第10行代码,不包括Command Leader),那么可以直接Commit。否则就需要走Slow Path。
在运行Slow Path前,我们得更新seq和deps。其中deps是从每个Replica之间的union,seq取每个Replica过来的最大的seq。然后在Slow Path中,Command Leader会给另外F个Replica发送Accept请求,让它们Accept三元组(γ,seq,deps)
。其实这个过程类似于Basic Paxos的Accept阶段。
在Slow Path完成了取得了F + 1票后,就可以和Fast Path会师了。此时Command Leader会发送Commit消息给所有的Replica,并且返回给Client。
类似于经典的Paxos,EPaxos也会给每个消息(message)分配一个ballot number。每个Replica会忽略小于它们已经见过的最大的ballot number。这个ballot number格式为epoch.b.R
,其中:
- epoch
如果集群的配置有变动,那么epoch会变化 - b
如果某个replica R,在做Explicit Prepare时,需要创建一个新的ballot时,会自增b - R
表示这个Replica
每个Replica是自己拥有的instance的初始Leader,换言之,对于每个instance R.i
,隐式存在一个epoch.0.R
。
Explicit Prepare过程
【这一块还包含了优化部分,比较难懂】
假如说Replica L可能宕机了,那么Replica Q会尝试接管它的instance L.i。
首先,将ballot number设置为epoch.(b + 1).Q,其中epoch.b.R是Q知道的L.i中最大的ballot number。然后,发送Prepare(epoch.(b + 1).Q,L.i)
给包括自己在内的所有Replica,并且等待至少floor(N/2) + 1
个回复。令R等于the set of replies w/ the highest ballot number:
- 如果R中包含了
(γ,seqγ,depsγ,committed)
,则在L.i对(γ,seqγ,depsγ)运行Commit Phase。 - 再如果R中包含了
(γ,seqγ,depsγ,accepted)
,运行Paxos-Accept phase。 - 再如果R包含了至少
floor(N / 2)
个不同的对L.i
上默认的ballot即epoch.0.L
的回复(γ,seqγ,depsγ,pre-accepted)
,并且没有一个回复是来自于L的,则运行Paxos-Accept phase。
【优化算法】此时,只需要包含floor((F + 1) / 2)
个PreAccept了(γ,seqγ,depsγ)
这个三元组的Replica即可。在发现他们后,新的Command Leader Q会尝试向其他Replica发送TryPreAccept消息。在某个Replica收到TryPreAccept消息后,会判断这个三元组是否和自己已有的log冲突:一个Command的deps中不包括γ,并且depsγ中也不包括这个Command(简单来说两个Command的deps互相不包含),那么这个Command就和γ冲突;或者这个Command虽然在depsγ中,但是它的seq大于等于seqγ,那么也是和γ冲突的。只有当自己的log中没有这些冲突Command时,这个Replica才会回复TryPreAccept。
如果真的存在某个Command和三元组冲突,并且这个Command被Commit了,那么Q就知道γ不能再fast path中被提交。但如果这个Command没有被Commit,那么新的Comamnd Leader Q就会defer,知道这个Command被Commit。最后,如果Q能够得到F + 1个Replica(在计算F时,老Command Leader也被计算在内)的PreAccept票后,就可以运行Paxos-Accept phase了。
这里有一个Corner case,也即是当deps中的某个Command修改它的seq,然后比需要Recover的Command γ的seq还要大了。为了阻止这个情况,对于depsγ中的所有Command,至少需要有一个Acceptor记录为Commit状态后,才允许Command Leader走Fast Path提交。对于N小于7的情况,更有效的方案是将更新后的deps放到Accept和AcceptReply消息中,并且保证这些消息的接收方都去缓存了它们。这些信息只会被用来辅助Recovery。
【Q】为啥优化算法只改这一个分支? - 再如果R包含了至少一个
(γ,seqγ,depsγ,pre-accepted)
,则在L.i上执行Phase1,Command为γ,并且避免fast path。 - 最终,则在L.i上执行Phase1,Command为noop,并且避免fast path。
Replica R, 在收到来自Q的Prepare(epoch.b.Q,L.i)后:
- 如果epoch.b.Q比最近接受到来自L.i的请求epoch.x.Y大,则返回PrepareOK(cmdsR[L][i], epoch.x.Y,L.i)
- 否则返回NAC
Execution Protocol
为了执行在R.i这个instance上提交的Command γ,需要执行下面的步骤:
- 等待R.i被提交,或者运行Explicit Prepare过程强制这个过程
- 构造γ的依赖图,其中需要加入γ的dependency set中的所有Command
注意,这个过程是递归的,也就是我们需要考虑依赖的依赖 - 找到所有的SCC,并对这些SCC进行拓扑排序
- 根据逆拓扑序,执行:
- 对于SCC中的所有命令,按照sequence number(seq)从低到高排序
- 按照这个顺序执行所有尚未被执行的Command,并标记他们为已被执行
关于性质的(Informal)证明
Nontriviality
Stability和Consistency
先证明命题1:如果Replica R通过instance Q.i提交了Commit γ(这里R和Q可能是同一个Replica),那么对于任意的提交Q.i的Replica R’,它提交的Command γ’一定等于γ。
证明简要:如果说Command γ能被在Q.i这个instance中提交,一定是因为γ的Phase 1是在instance Q.i中被启动的。这是因为:
- 对于每个新Command,Q会自增instance number,也就是这里的i
- 如果Q宕机并重启了,那么根据”4.7 Reconfiguring the Replica Set”,它会被分配得到一个新的identifier
【TODO】这里需要详细解释下。
可以看到,命题1实际上暗示了consistency。进一步地,在Replica崩溃时,Command可能被丢失,所以当Command Log被保存在持久化存储上时,也暗示了stability。
首先引入一个定义。如果Command γ具有seqγ和depsγ,如果元组(γ,seqγ,depsγ)是Q.i这个instance上唯一被或者可能被commit的元组,那么我们称元组(γ,seqγ,depsγ)在Q.i这个instance上是safe的。
下面证明命题2:Replica只会提交safe的tuple。首先知道(γ,seqγ,depsγ)如果在Q.i被提交,那么它要么在Paxos Accept phase提交,或者在Phase 1之后提交。
- 对于第一种情况,即Slow Path。此时有多于半数的Replica写入了值(Figure2的L20),这对应了经典Paxos的要求。
- 对于第二种情况,即Fast Path。此时Coomand Leader收到了不含自己的N-2个相同的回复(Figure2的L11)。如果此时另外的Replica需要接管这个instance,通常这是因为初始的Leader宕机了,那么新Leader必须执行Prepare过程,并且会发现至少
floor(N/2)
个Replica都返回(γ,seqγ,depsγ)这个元组。因此新的Leader会认为这个元组可能提交了,并在Paxos-Accept过程中使用这个元组。
至此我们可以发现,元组们,包括他们的attributes,都会在各个Replica之间被consistently和stable地提交。
下面我们证明这些consistent和stable的attributes能够保证所有互相interfere的Command,在每个Replica上都按照同样的顺序被提交。
Execution consistency和Execution linearizability
引理1 (Execution consistency):
如果互相interfere的两个Command γ和δ都被成功Commit了(并不一定通过同一个Replica提交),那么他们在任意Replica上都会以相同的顺序被执行。
如果两个Command互相interfere,至少有一个Command的deps中能找到另一个Command。Phase1在这个命令被至少简单多数(注意我们现在讲的还是基础版本的EPaxos,其中Phase1的Fast Path Quorum为2F)个Replica Pre-Accepted后结束,并且这个命令最终的deps集是由至少简单多数个Replica的deps的union。这也同样适用于recovery过程中使用的Explicit Prepare(L32)的情况,因为所有的依赖都是基于那些被可能宕机的Leader初始设置的集合计算得到。所以至少一个Replica同时PreAccept γ和δ,并且它的PreAcceptReplies被用来构造γ和δ的最终的deps集。
通过执行Execution Algorithm,一个命令只有在自己dependency graph中的所有命令都被Commit后才会执行。主要有三个场景:
- 两个Command都在彼此的dependency graph中,这也意味着dependency graph是identical的,并且γ和δ在同一个SCC中。所以当执行一个Command时,另一个Command也会被执行,并且是根据sequence number来执行的。根据命题2,这些已Commit的Command的attribute在所有Replica上都是stable和consistent的。所以所有dependency graph相同的Replica都会在相同的顺序中执行γ和δ。
- γ在δ的dependency graph中(δ依赖γ),但δ不在γ的dependency graph中。也就是说dependency graph有一个从δ到γ的路径,反之不成立。因此γ和δ来自不同的SCC,在逆拓扑序即实际的执行顺序中,γ在δ前面。补充一点,在一些Replica中,γ在δ被Commit之前就被执行了,这样的场景是没毛病的,因为γ并不依赖于δ。
- δ在γ前面,这个就是上面的对称形式。
引理2 (Execution linearizability):
如果两个Command γ和δ,被客户端指定了顺序,例如只有在γ被任意Replica Commit之后才会Propose δ,这样的话我们要求所有的Replica都要在δ之前执行γ。
证明。因为δ是在γ Commit之后被Propose的,所以在任何Replica收到Command δ的PreAccept时,γ的Sequence Number应该是Stable和Consist的。因为一个包含γ的元组,以及它最终的sequence number至少被majority个Replica记录,那么δ的sequence number会最终被更新到大于γ的值,并且delta的deps中会包含γ。所以在执行δ时,δ的dependency graph必须包含γ,并且:
- 它们在同一个SCC中
- 按照逆拓扑序,γ在δ前面的一个SCC中。
无论那种情况,γ都会在δ前面执行。
Liveness
只要不是大多数Replica有故障,那么Client会一直重试某个Command,直到被Accept。
优化版本
优化版本会修改Recovery过程,也就是Explicit Prepare Phase。我已经写到“Explicit Prepare过程”上面了。
当F个Replica宕机后,在Fast Quorum中就只有floor((F + 1) / 2)
个存活的成员(可以翻翻前面Fast-Path Quorum的定义),那么就不能在剩下的Replica中形成多数了。所以,当Command Leader向所有Replica(而不只是Fast Quorum中的Replica)发送PreAccept消息后,Recovery过程不一定能正确地发现,老的Command Leader,是通过哪些Replica的回复,从而Commit了这个instance的。 Still, such redundancy is sometimes desirable because the command leader may not know in advance which replicas are still live or which replicas will reply faster。
对于这种情况,需要修改Fast Path的条件:当Command Leader收到F + floor((F + 1) / 2) - 1
个满足initial ordering attributes的PreAccept的回复,才会选择Fast Path。并且这些Replica需要被记录到Log中,以便在Recovery过程中只考虑这些Replica。
在不发送redundant PreAccept的时候(【Q】什么时候会发送?),一个三副本的系统永远可以通过Fast Path提交,这是因为在做还有一个Acceptor的集合中肯定不存在disagreement。
EPaxos的TLA+简介
如果光读论文就能读懂,那我感觉你真的挺厉害的,反正我是半懂不懂的,特别是对Recovery部分。所以我转过来看TLA+了。
定义
下面是对SlowQuorum和FastQuorum的定义。对于某个Replica r,SlowQuorum(r)表示它作为Command Leader时,所有可能的Slow Path Quorum组成的集合,也就是类似于{ {r,a}, {r,b} }
这样的东西。那么,SlowQuorum(r)
必须是SUBSET Replica
的子集,换句话说,就是不能出现Replica之外的元素。然后对于SlowQuorum中的任意的Quorum SQ,它必须包含r,并且大小为N/2+1
,其中N为集群大小。
同理,FastQuorum中的大小为N/2 + (N/2+1)/2
,这个其实就是把F = N/2
带入进去就得到了。
1 | CONSTANTS FastQuorums(_), SlowQuorums(_) |
下面是对一些状态的定义。Commands是一个常量,表示所有可能的命令,在运行前由我们给出。none是一个none Command(也许是Noop?),对它的定义还挺新颖的,如果是我,可能选择直接CONSTANT了一个Nil的值。
Instance是做了一个笛卡尔积,没啥讲的。
Status标记了Log中的某个Command可能处于的状态。
1 | CONSTANTS Commands |
下面是Replica之间可能发送的消息。这里的TLA+语法可以参考TwoPhase章节。
- pre-accept、pre-accept-reply
Phase1由Command Leader发起的消息和回复,即(γ,seqγ,depsγ,pre-accepted)
注意要区分这里的committed字段,和全局的committed变量 - accept、accept-reply
Phase2由Commmand Leader发起的消息和回复,即(γ,seqγ,depsγ,accepted)
- commit
Commit阶段由Command Leader发起的消息,即(γ,seqγ,depsγ,committed)
- prepare、prepare-reply
Explicit Prepare阶段由新的Command Leader Q发起的消息。 - try-pre-accept、try-pre-accept-reply
优化算法
一些常见字段的说明:
- src和dst都属于Replicas。
- inst属于Instances表示当前操作的实例。
- ballot应该是指的ballot number,不是
epoch.b.R
么?看起来是吧epoch和b合并了? - cmd应该是指instance中包含的command。
- deps和seq就是所谓的attribute,没啥讲的。
1 | Message == |
下面是定义变量,可以结合TypeOK和Init条件看出类型
1 | VARIABLES cmdLog, proposed, executed, sentMsg, crtInst, leaderOfInst, |
- cmdLog
每个Replica上的日志。
每条日志中包含对应的Instance、Status,ballot、Command、以及deps和seq - proposed
已经被被Propose了的Command - executed
每个Replica上已经被execute的Command - sentMsg
所有被发送,但是还没有被接受的消息,理解成Message类型对象的集合 - crtInst
对于每个Replica,它下一个可以使用的instance number(一个数字),初始值是1 - leaderOfInst
这是从Replicas到SUBSET Instances的一个映射,表示对于每个Replica,它发起但是还没有完成的Instance的集合 - committed
【Q】maps commands to set of commit attributs tuples
从实现上来看,应该是个三元组,表示每个Command被提交时的deps和seq - ballots
所有Replica中,最大的ballot number,这里指的应该是epoch.b - preparing
对于每个Replica,它目前正在preparing(例如recovering)的所有Instance
验证
Nontriviality的验证,对于任意的Instance,始终(因为有[]
)有:在committed中的所有Command,它要么是在proposed集合中(也就是被某个Command Leader主动Propose的),或者为none
1 | Nontriviality == |
Stability的验证,是个三层大循环,遍历所有的Replica/Instances/Commands,此时我们检查(replica, i, C),始终满足:
在replica的cmdLog[replica]中任选一条记录rec1,它的(inst,cmd)等于(replica, i),并且status为committed或者executed,那么始终存在另一个满足条件的rec2。
1 | Stability == |
Consistency的验证,对于所有的Instance i,始终满足它最多只会被commit一次。
1 | Consistency == |
总体的Action
可以选择执行Command Leader的某个动作,或者普通Replica的某个动作。Command Leader肯定属于Replica,但我们是按照行为来区分的。
1 | Next == |
首先是Command Leader的所有可能的动作:
- 检查
Commands \ proposed
,也就是Commands
中所有还没被propose的Command,如果存在,则选择一个C(\E
可以表示选出一个的意思):
选出一个cleader,对C做状态转移Propose(C, cleader)
- 在Replicas中选出一个cleader,在cleader为Command Leader的instance中选出一个instance,可以执行下面任意的状态转移:
- 可能走Fast Path
- 可能走Slow Path
- 可能走Phase2Finalize
【Q】Phase2的Accept啥时候发送的? - 可能走优化路径FinalizeTryPreAccept
1 | CommandLeaderAction == |
下面是ReplicaAction,选择一个Replica:
- 可能回复Phase1
- 可能回复Phase2
- 可能对于某个Instance i,
i[1]
表示Instance这个tuple的第一个元素,也就是Replica;crtInst[i[1]]
表示它对应的Replica下一个可以使用的instance number,如果大于i本身的instance number。
这个条件说明这个instance被自己原来的owner start。
则通过SlowQuorum个节点进行SendPrepare。
【Q】不太懂什么意思,但这里无疑就是Explicit Prepare里面的Prepare过程了。 - 可以ReplyPrepare
- 选择一个正在preparing的Replica
通过SlowQuorum个节点进行PrepareFinalize - 回复ReplyTryPreaccept
1 | ReplicaAction == |
Phase 1
Propose
从上文看到,CommandLeaderAction的一个可能的状态转移是Propose。
此时,生成newInst和newBallot。新生成的newInst的instance number为crtInst[cleader],newBallot的epoch.b
部分始终为0,看来是不考虑epoch了。
然后,将要Propose的Command C添加进proposed中,并且在FastQuorum里面选择一个Quorum,尝试走FastPath。
然后,我们自增crtInst,这里的@
就是crtInst[cleader]
,实际上是一个简写。
1 | Propose(C, cleader) == |
对Phase1的处理起始于StartPhase1
,这个状态转移触发的条件有很多,比如上面的Propose。
首先,构建依赖deps和seq:
- newDeps是Command Leader即cleader中所有Command的Instance,即
rec.inst
。
【Q】这是不是太强了点?按照协议,首先要是冲突的,然后只要包含最高冲突Instance的编号就行啊。 - newSeq是cleader中所有Command的seq的最大值,然后还要加1,这是符合协议的论述的。
- oldRecs表示cleader中所有和当前Command的Instance相同的Command。
然后进行状态转移:
- cmdLog
需要修改cleader的Log。具体来说,需要把同Instance的所有的旧的日志全部干掉。【Q】为啥可能会有同Instance的日志呢?
然后,我们把新的Command加上到日志中,并且设置status为”pre-accepted”。也就是说cleader我PreAccept了这个日志,并不代表其他Replica也PreAccept了这个日志。 - leaderOfInst
显然,对于这个Instance,cleader我是它的Leader。这是显然的,因为Instance二元组里面,Replica是cleader,然后instance number也是自增的。
应该加入到leaderOfInst,表示是我发起的,但还没有完成的Instance。 - sentMsg
显然要往其他的Replica发消息。在此之前,我们先要把oldMsg干掉。如果我们是走Propose过来的,那么oldMsg是空的。但如果是走Explicit Prepare过来的,那么是会传入oldMsg的,这个到时候再说。
这里需要注意,消息的定义是src: Replicas, dst: Replicas
,但我们传进来的src和dst实际上是两个集合,这会导致产生一系列新Msg。可以参考TLA的文档。
1 | StartPhase1(C, cleader, Q, inst, ballot, oldMsg) == |
Phase1Reply
Propose是发送PreAccept请求,Phase1Reply是其他Replica处理该请求。
任选一个发给replica的type为”pre-accept”的消息msg。
oldRec为replica日志中,inst等于msg对应的Instance的日志组成的集合。
那么对于oldRec中所有的日志rec,要么它的ballot等于msg里的ballot,要么比msg传过来的ballot要小。否则这个消息就应该被忽略。
构造newDeps为msg传来的deps,加上当前replica所有的log中的Instance(除去msg.inst)。
构造newSeq为Max(msg.seq, replica的所有日志中最大的seq+1)。
构造instCom,为replica自己日志中所有status为”committed”或”executed”(其实就是已提交)的日志对应的Instance构成的集合。
下面是状态转移:
- 修改当前replica的cmdLog
主要是将status变成”pre-accepted”,然后deps和seq设置为刚算出来的newDeps和newSeq。 - 发送消息
发送一条”pre-accept-reply”消息给msg.src,实际上就是刚才发命令过来的cleader。
1 | Phase1Reply(replica) == |
Phase1Fast
Phase1Fast是cleader在其他Replica处理该请求后,尝试通过FastPath提交这个请求。里面包含了是否能够进行FastPath提交的判断,以及提交的状态转移。
首先,是一部分简单的Enable条件,校验cleader是不是Instance i的Leader,并检查Q是不是cleader的FastQuorums。
然后,选择cleader的某条日志record,日志中的Instance为i、Status为”pre-accepted”、ballot的为0。这些日志肯定是从StartPhase1设置的。并且因为ballot为0,所以肯定是从Propose设置的。
Q通过刚才的Phase1Reply发回来的replies,它们是”pre-accept-reply”类型的消息,并且ballot等于record对应的ballot,我们进行下面的检查:
- 对于Q中除了cleader之外的replica,在replies中都会有从它发给cleader的”pre-accept-reply”消息。
也就是说Q中的Replica,都回复了cleader的pre-accept请求。
这个检查是必要的,如果说之前cleader通过StartPhase1发过去的ballot比较小,那么有的replica就不会回复,因此这条Enable条件就过不了。 - 并且replies中任意两个消息r1和r2,它们的deps和seq是相等的。
这是走FastPath的要求,只有满足该要求,才能进下一步。
这个实际上也是Enable条件,如果不满足那么根本就不会有下面的状态转移。 - CHOOSE replies中的某一个r,基于它构造新的r.deps:
这里可以CHOOSE,是因为大家的deps和seq都一样。
注意已提交的日志,指的是status为”committed”和”executed”的日志。- localCom
cleader的所有的已提交日志的Instance构成集合。 - extCom
Phase1Reply中,replica在回复时,会带上自己日志中所有已提交的日志对应的Instance构成的集合。
extCom是这些的并集。 - 然后构造新的r.deps集合,为localCom和extCom中所有的Instance
- localCom
然后基于r构造状态转移:
- cmdLog
更新这个日志状态为committed。
更新当前的record的attribute为r.deps和r.seq。 - sentMsg
产生一条Commit信息,这个消息可能是直接给Client的,所以没有dst和src。 - leaderOfInst
在leaderOfInst[cleader]中移除掉当前的Instance i,表示这个Instance已经提交了。 - committed
将<<record.cmd, r.deps, r.seq>>
加入已提交的数组中。
1 | Phase1Fast(cleader, i, Q) == |
Phase1Slow
Phase1Slow是cleader在其他Replica处理该请求后,尝试通过SlowPath提交这个请求。
前面的条件都一样,不再赘述。
主要不一样的地方是从finalDeps开始的。这里大家的deps和seq都不一定一样了,所以不能像FastPath一样CHOOSE了,而要老老实实取并集、取最大值。
- finalDeps
就是所有来自Replica的回复中的deps的并集。 - finalSeq
就是所有回复中最大的seq。
下面构造状态转移:
- cmdLog
更新日志状态为”accepted”。
注意,在FastPath中,能够直接进入commited状态,但这里得先是”accepted”,后面再走一轮Phase2才行。 - sentMsg
往除自己之外的SlowQuorum成员发送”accept”形式的消息。并且带上deps和seq为finalDeps和finalSeq。这个消息会在Phase2Reply被Replica处理。
1 | Phase1Slow(cleader, i, Q) == |
Phase2
Phase2Reply
Phase2Reply处理在SlowPath中,从Command Leader发过来的”accept”消息。
这里的Enable条件是,在自己的日志cmdLog[replica]
中按照msg.inst
找到对应的记录,要求消息的ballot要么等于,要么大于日志中的ballot。
如果满足Enable条件,则可以进行状态转移:
- cmdLog
修改自己的日志,状态为”accepted”,并且更新deps和seq为msg中传入的值。这个值是由finalDeps和finalSeq一起计算得到的。 - sentMsg
发送”accept-reply”消息给cleader。
1 | Phase2Reply(replica) == |
Phase2Finalize
Phase2Finalize是整个SlowQuorum过程的终点,cleader会收集各个Replica的消息,并提交Command。即对于cleader下的某个Instance i,和SlowQuorum Q,尝试完成Phase2的最终提交。
照例还是Enable条件,在自己的日志里面看看能不能找到i对应的状态为”accepted”的日志,如果找不到,那么也不用往下看了。
如果能找到,就需要在sentMsg中找出所有和这个Instance相关的,类型为”accept-reply”的消息(也就是各个Replica在Phase2Reply过程中发出的消息)。显然,对于Q中除cleader之外的replica,都需要发一条这样的消息才能继续提交。
下面就是状态转移,和Phase1Fast基本一致,就不另外说明了。
1 | Phase2Finalize(cleader, i, Q) == |
Recovery
下面是Recovery相关操作。
SendPrepare
这里的replica就是论文里面的Replica L。其中,Enable条件:
- replica并不是i这个Instance的Leader
这里注释掉一句,是replica并没有prepare这个instance。【Q】我不清楚为什么这个被注释掉。 - 目前使用过的ballots不超过MaxBallot
这个应该是防止最后算法不会终止 - replica当前的日志中,不存在任何Instance为i的日志是已提交的(即其status为”committed”和”executed”)。
状态转移:
- sentMsg
从这个replica向一个SlowQuorum,即Q发送”prepare”消息。
这里注意,更新ballot为<< ballots, replica >>
,这里的ballots是全局最大的ballot值。 - ballots
更新全局最大的ballots,自增1。 - preparing
将当前replica的preparing集合中增加Instance i。
1 | SendPrepare(replica, i, Q) == |
ReplyPrepare
ReplyPrepare处理来自SendPrepare里面replica的”prepare”消息。下面进行讨论:
- 如果自己的日志中,存在一条对应于msg里面instance的日志,并且消息中的ballot要大于日志中的ballot。
状态转移:- sentMsg
将来自SendPrepare里面replica的”prepare”消息删除,并重新发送一条”prepare-reply”消息给msg.src。
消息中的ballot为自己的msg.ballot,prev_ballot为rec.ballot。根据前文,msg.ballot肯定是大于rec.ballot的。
消息中的deps和seq沿用自己本地日志中的。【Q】这里很奇怪,似乎我们不在ReplyPrepare阶段对deps和seq决议。 - cmdLog
删除自己日志中的rec,并且增加一条新条目。这个新条目和rec大致一样,只是将ballot更新为msg中的ballot。 - leaderOfInst
如果日志中的instance的Command Leader是replica,那么将它移出leaderOfInst[replica]
。否则保持不变。
【Q】这里是啥意思,重新选一个Command Leader,还是直接走Basic Paxos那一套提交呢?
- sentMsg
- 如果在自己日志中,不存在对应于msg里面instance的日志。
状态转移:- sentMsg
发送”prepare-reply”消息。
ballot为消息中传来的ballot,并且prev_ballot为<< 0, replica >>
,相当于是一开始的ballot。【Q】msg.ballot肯定是大于rec.ballot的么?
其他的字段也是设置成默认值:status为”not-seen”。cmd为none。deps为{}
,seq为0。 - cmdLog
参照sentMsg的变更
- sentMsg
1 | ReplyPrepare(replica) == |
PrepareFinalize
SendPrepare里面的replica,也就是所谓的Replica L,在收到对端的”prepare-reply”消息后,会走到PrepareFinalize中。这个函数也是最为复杂的。
Enable条件:
- i要在preparing[replica]中
也就是确认当前replica是Replica L - replica的日志中有i这个Instance,并且还没被提交
- Q中所有的replica都给当前replica的发送”prepare-reply”回复(replies),并且:
下面会讨论四种情况,总体是”越来越惨”的- 如果这些replies中的某个com的status是已提交(即”committed”, “executed”)
【Q】为什么会有这种情况?感觉应该是因为在FastPath的Phase1Fast或者SlowPath的Phase2Finalize之后,cleader宕机导致的。
那么就将这个Instance i移出preparing[replica]。并且在sentMsg中删除所有的replies。
【Q】感觉这就相当于直接让它往后提交了? - 如果这些replies中没有status是已提交(即”committed”, “executed”),但是存在acc是**”accepted”**的。
同时还需要检查acc的prev_ballot大于等于(如果其他消息不是”accept”状态,那么就默认acc的大)所有其他消息的prev_ballot
状态转移:- sentMsg
删除所有的replies,并且对除当前replica之外的所有Q,都发送一条”accept”消息。
这个消息的ballot为自己日志中的ballot。
关键内容,即cmd、deps和seq,都设置为acc传过来的。
【Q】为什么replies里面prev_ballot最大的那个消息是”accpeted”,就可以以它为准走”accept流程”? - cmdLog
参照sentMsg创建一个新条目。
- sentMsg
- 如果这些replies中的某个com的status是已提交(即”committed”, “executed”)
因为太长了,所以后面两种情况拆到下面讨论
1 | PrepareFinalize(replica, i, Q) == |
下面是后两种情况:
- Q中所有的replica都给当前replica的发送”prepare-reply”回复(replies),并且:
- 第一种情况;已经讨论
- 第二种情况:已经讨论
- 如果这些replies中,同时没有”committed”、”executed”、”accept”状态,但是有”pre-accepted”
将replies中所有的”pre-accepted”提出来为preaccepts,又分为三种情况:- preaccepts里面任意两个的cmd、deps和seq都相等。并且preaccepts中没有一个消息是从Instance i最初的Command Leader即
i[1]
发出的。并且Q中所有的replica都发了”pre-accepted”给当前的replica。
执行状态转移:- sentMsg
从当前replica向Q中所有其他replica发送”accept”。
cmd、deps和seq取其中任意一个就行,因为都相等。 - cmdLog
参照sentMsg - preparing
在preparing[replica]移除掉Instance i。 - leaderOfInst
在leaderOfInst[replica]移除掉Instance i。
- sentMsg
- 如果和之前一样,但Q中只有大于
Cardinality(Q) \div 2
个replica发送了”pre-accepted”
执行状态转移:- sentMsg
这也是全局唯一会走到ReplyTryPreaccept流程里面的情况,对应了优化算法。
从当前replica向Q中所有其他replica发送”try-pre-accept”。
注意,这个消息也要发送给自己,所以和上一个情况是不同的处理策略。
cmd、deps和seq的处理办法和上面是一样的。
- sentMsg
- 如果
preaccepts
不为空,并且满足下面三个条件之一:连cmd、deps和seq都不完全一样了,或者Cardinality(preaccepts)
也小于Cardinality(Q) \div 2
,或者preaccepts
中存在一个消息是从Instance i最初的Command Leader即i[1]
发出的
执行状态转移:- 在preaccepts中CHOOSE一个cmd不为none的pac,执行StartPhase1
但是和Propose的处理不同,这里会带上rec.ballot, replies作为Ballot和oldMsg参数。
而replies会在StartPhase1中被直接干掉(从sendMsg里面删除掉)。 - preparing
去掉i
- 在preaccepts中CHOOSE一个cmd不为none的pac,执行StartPhase1
- preaccepts里面任意两个的cmd、deps和seq都相等。并且preaccepts中没有一个消息是从Instance i最初的Command Leader即
- 如果这些replies中,状态都是”not-seen”的
执行状态转移:- 同样执行StartPhase1,但cmd为none。
- preparing
去掉i
1 | ... |
ReplyTryPreaccept
找到发给自己的”try-pre-accept”的请求tpa,首先找到自己日志中所有Instance等于tpa.inst的条目oldRec,进行校验:
- tpa的ballot number要大于等于所有oldRec中的ballot number
- 所有oldRec中的状态不能是{“accepted”, “committed”, “executed”}
然后进行讨论:
- 如果在oldRec之外,存在某个rec,和tpa是冲突的
如果tpa.inst不属于rec.deps,并且要么rec.inst不属于tpa.deps,要么rec.seq大于tpa.seq。那么rec和tpa是冲突的。
如果存在冲突的话,则执行状态转换:- sentMsg
消费掉tpa消息,并且回复”try-pre-accept-reply消息。
此时,status为rec.status。 - cmdLog
不做改动,可以认为是拒绝了这个消息。
- sentMsg
- 如果对于oldRec之外的的所有rec,都和tpa不冲突
执行状态转换:- sentMsg
消费掉tpa消息,并且回复”try-pre-accept-reply”消息。
注意,status的值是”OK”,这个和冲突情况不一样。【Q】如果此时status的值为”pre-accepted”,那么在FinalizeTryPreAccept中,这条日志仍有被提交的希望? - cmdLog
删除所有的oldRec,也就是自己日志中所有有关Instance的日志。
新增一条”pre-accepted”日志,这就相当于走完了StartPhase1。
- sentMsg
1 | ReplyTryPreaccept(replica) == |
FinalizeTryPreAccept
选出自己Instance为i的日志rec,在所有”try-pre-accept-reply”回复中,取出Instance为i且ballot为rec.ballot的回复,构成集合tprs。
那么对于Q中的任意一个r,tprs中肯定有一条消息是从r中发出的。也就是说必须得Q中每一个Replica都回复了”try-pre-accept-reply”,这个也是老生常谈的条件。
下面是三种情况的讨论:
\A tpr \in tprs: tpr.status = "OK"
这说明在ReplyTryPreaccept中,大家都没冲突。这个类似于PrepareFinalize中的accept流程,也就是论文中所谓的Paxos-Accept phase。执行状态转换:- sentMsg
直接发送”accept”消息给Q,后面就是走Phase2Reply的流程。 - cmdLog
设置status为”accepted”
- sentMsg
- 如果在tpr中存在某个status为{“accepted”, “committed”, “executed”}的消息
走StartPhase1 - 如果tpr.status不全是”OK”,也有”pre-accepted”
这里应该是冲突了,所以直接干掉。
1 | FinalizeTryPreAccept(cleader, i, Q) == |
Reference
- There Is More Consensus in Egalitarian Parliaments
- EPaxos Revisited
- https://github.com/efficient/epaxos/blob/master/tla%2B/EgalitarianPaxos.tla
一份TLA+的说明