Paxos是一致性协议的基础,侧重理论。处理实际问题的侧重点不同,衍生出其它各种改进版本协议
角色
-
Proposer 负责提出提案
-
Acceptor 负责对提案做出裁决(accept or no)
-
Learners 负责学习提案结果
现实中,一个进程可担任多个角色。
议案ID生成算法
在Google的Chubby论文中给出了这样一种方法:假设有n个proposer,每个编号为ir(0<=ir<n),proposal编号的任何值s都应该大于它已知的最大值,并且满足:
s %n = ir => s = m*n + ir
proposer已知的最大值来自两部分:proposer自己对编号自增后的值和接收到acceptor的拒绝后所得到的值。
例: 以3个proposer P1、P2、P3为例,开始m=0,编号分别为0,1,2。
1) P1提交的时候发现了P2已经提交,P2编号为1 >P1的0,
因此P1重新计算编号:new P1 = 1*3+1 = 4;
2) P3以编号2提交,发现小于P1的4,因此P3重新编号:new P3 = 1*3+2 = 5。
决议的发布
Acceptor集群中的每个成员都有可能同意该提案且只能批准一个提案,Proposer只有当收到一半以上的Acceptor成员同意了一个提案,就认为该提案被选定了,
acceptors需要将accept消息发送给learners的一个子集,然后由这些learners去通知所有learners。
但是由于消息传递的不确定性,可能会没有任何learner获得了决议批准的消息。当learners需要了解决议通过情况时,可以让一个proposer重新进行一次提案。注意一个learner可能兼任proposer。
两个阶段
prepare阶段
- Proposer选择一个提案编号N,然后向半数以上的Acceptor发送编号为N的Prepare请求。Pareper(N)
- 一个Acceptor收到一个编号为N的Prepare请求,
- 如果小于它已经响应过的请求,则拒绝,不回应或回复error。
- 若N大于该Acceptor已经响应过的所有Prepare请求的编号(maxN),那么它就会将它已经接受过(已经经过第二阶段accept的提案)的编号最大的提案(如果有的话,如果没有的话accept返回{pok,null,null})作为响应反馈给Proposer,同时该Acceptor承诺不再接受任何编号小于N的提案。
accept阶段
当一个proposer收到了半数以上Acceptor对其发出的编号为N的Prepare请求的响应,就进入批准阶段。它要向回复prepare请求的acceptors发送对[N,V]提案的accept请求。
V就是收到的响应中编号最大的提案的value(某个acceptor响应的它已经通过的{acceptN,acceptV}),如果响应中不包含任何提案,那么V就由Proposer自己决定
如果Acceptor收到一个针对编号为N的提案的Accept请求,
- 只要该Acceptor没有对编号大于N的Prepare请求做出过响应,它就接受该提案。
- 如果N小于Acceptor之前响应的prepare(N)请求,则拒绝,不回应或回复error(当proposer没有收到过半的回应,那么他会重新进入第一阶段,递增提案号,重新提出prepare请求)
拜占庭将军问题
拜占庭帝国就是5~15世纪的东罗马帝国,拜占庭即现在土耳其的伊斯坦布尔。我们可以想象,拜占庭军队有许多分支,驻扎在敌人城外,每一分支由各自的将军指挥。将军们智能靠通讯员进行通讯。在观察敌人以后,忠诚的将军们必须制订一个统一的行动计划——进攻或者撤退。然而,这些将军冢有叛徒,他们不希望忠诚的将军们能达成一致,因而影响统一行动计划的制订与传播。
问题是:将军们必须有一个协议,使所有忠诚的将军们能够达成一致,而且少数几个叛徒不能使忠诚的将军们作出错误的计划——使有些将军进攻而另一些将军撤退。
如果这11位将军中有间谍呢? 假设有9位忠诚的将军,5位判断进攻,4位判断撤退,还有2个间谍恶意判断撤退,虽然结果是错误的撤退,但这种情况完全是允许的。因为这11位将军依然保持着状态一致性。
1.11位将军进攻城池
2.同时进攻(议案、决议)、同时撤退(议案、决议)
3.不管撤退还是进攻,必须半数的将军统一意见才可以执行
4.将军里面有叛徒,会干扰决议生成
先后提议的场景
角色:
proposer:参谋1,参谋2
acceptor:将军1,将军2,将军3(决策者)
- 参谋1发起提议,派通信兵带信给3个将军,内容为(编号1);
- 3个将军收到参谋1的提议,由于之前还没有保存任何编号,因此把(编号1)保存下来,避免遗忘;同时让通信兵带信回去,内容为(ok);
- 参谋1收到至少2个将军的回复,再次派通信兵带信给3个将军,内容为(编号1,进攻时间1);
- 3个将军收到参谋1的时间,把(编号1,进攻时间1)保存下来,避免遗忘;同时让通信兵带信回去,内容为(Accepted);
- 参谋1收到至少2个将军的(Accepted)内容,确认进攻时间已经被大家接收;
- 参谋2发起提议,派通信兵带信给3个将军,内容为(编号2);
- 3个将军收到参谋2的提议,由于(编号2)比(编号1)大,因此把(编号2)保存下来,避免遗忘;又由于之前已经接受参谋1的提议,因此让通信兵带信回去,内容为(编号1,进攻时间1);
- 参谋2收到至少2个将军的回复,由于回复中带来了已接受的参谋1的提议内容,参谋2因此不再提出新的进攻时间,接受参谋1提出的时间;
交叉场景
proposer:参谋1,参谋2
acceptor:将军1,将军2,将军3(决策者)
- 参谋1发起提议,派通信兵带信给3个将军,内容为(编号1)
- 3个将军的情况如下: 将军1和将军2收到参谋1的提议,将军1和将军2把(编号1)记录下来,如果有其他参谋提出更小的编号,将被拒绝;同时让通信兵带信回去,内容为(ok);负责通知将军3的通信兵被抓,因此将军3没收到参谋1的提议;
- 参谋2在同一时间也发起了提议,派通信兵带信给3个将军,内容为(编号2);
- 3个将军的情况如下:
- 将军2和将军3收到参谋2的提议,将军2和将军3把(编号2)记录下来,如果有其他参谋提出更小的编号,将被拒绝;同时让通信兵带信回去,内容为(ok);
- 负责通知将军1的通信兵被抓,因此将军1没收到参谋2的提议;
- 参谋1收到至少2个将军的回复,再次派通信兵带信给有答复的2个将军,内容为(编号1,进攻时间1);
- 2个将军的情况如下:
- 将军1收到了(编号1,进攻时间1),和自己保存的编号相同,因此把(编号1,进攻时间1)保存下来;同时让通信兵带信回去,内容为(Accepted);
- 将军2收到了(编号1,进攻时间1),由于(编号1)小于已经保存的(编号2),因此让通信兵带信回去,内容为(Rejected,编号2);
- 参谋2收到至少2个将军的回复,再次派通信兵带信给有答复的2个将军,内容为(编号2,进攻时间2);
- 将军2和将军3收到了(编号2,进攻时间2),和自己保存的编号相同,因此把(编号2,进攻时间2)保存下来,同时让通信兵带信回去,内容为(Accepted);
- 参谋2收到至少2个将军的(Accepted)内容,确认进攻时间已经被多数派接受;
- 参谋1只收到了1个将军的(Accepted)内容,同时收到一个(Rejected,编号2);参谋1重新发起提议,派通信兵带信给3个将军,内容为(编号3);
- 3个将军的情况如下:
- 将军1收到参谋1的提议,由于(编号3)大于之前保存的(编号1),因此把(编号3)保存下来;由于将军1已经接受参谋1前一次的提议,因此让通信兵带信回去,内容为(编号1,进攻时间1);
- 将军2收到参谋1的提议,由于(编号3)大于之前保存的(编号2),因此把(编号3)保存下来;由于将军2已经接受参谋2的提议,因此让通信兵带信回去,内容为(编号2,进攻时间2);
- 负责通知将军3的通信兵被抓,因此将军3没收到参谋1的提议;
- 参谋1收到了至少2个将军的回复,比较两个回复的编号大小,选择大编号对应的进攻时间作为最新的提议;参谋1再次派通信兵带信给有答复的2个将军,内容为(编号3,进攻时间2);
- 将军1和将军2收到了(编号3,进攻时间2),和自己保存的编号相同,因此保存(编号3,进攻时间2),同时让通信兵带信回去,内容为(Accepted);
- 参谋1收到了至少2个将军的(accepted)内容,确认进攻时间已经被多数派接受。
活锁问题
当一个proposer发现存在编号更大的提案时将终止提案。这意味着提出一个编号更大的提案会终止之前的提案过程。如果两个proposer在这种情况下都转而提出一个编号更大的提案,就可能陷入活锁,违背了Progress的要求。
解决方法:
- 随机睡眠-重试,在被打回第一阶段再次发起PrepareRequest请求前加入随机等待时间。
如果一个proposer通过accpter返回的消息知道此时有更高编号的提案被提出时,该proposer静默一段时间,而不是马上提出更高的方案,静默期长短为一个提案从提出到被接受的大概时间长度即可,静默期过后,proposer重新提案。
- 设置一个超时时间,到达超时时间后,不再接收PrepareRequest请求。
- 在proposer中选举出一个leader,通过leader统一发出PrepareRequest和AcceptRequest。
异常情况——持久存储
在算法执行的过程中会产生很多的异常情况:proposer宕机,acceptor在接收proposal后宕机,proposer接收消息后宕机,acceptor在accept后宕机,learn宕机,存储失败,等等。
为保证paxos算法的正确性,proposer、aceptor、learn都实现持久存储,以做到server恢复后仍能正确参与paxos处理。
- propose存储已提交的最大proposal编号、决议编号(instance id)。
- acceptor存储已承诺(promise)的最大编号、已接受(accept)的最大编号和value、决议编号。
- learn存储已学习过的决议和编号
一个Paxos集群,每个server同时担任proposer和acceptor,任何server都可以发起持久化redolog(即议案)的请求,
Basic-Paxos协议对每条redolog(即议案)的持久化都至少存在三次网络交互的延迟(1. 产生logID;2. prepare阶段;3. accept阶段)
1、首先要向所有的server查询当前最大logID,从多数派的应答结果中选择最大的logID,加1后作为执行本次Paxos Instance的唯一标识;
2、然后进入Paxos的prepare阶段,产生proposalID,并决定出要投票的redolog(即议案);
3、在accept阶段对prepare阶段产生的议案进行投票,得到多数派确认后返回成功。。
下面我们逐个分析每个阶段的必要性:
- 产生logID,由于Basic-Paxos并不假设一段时间内只有唯一的proposer,因此可能由集群内的任意server发起redolog同步,因此不能由单一server维护logID,而是需要进行分布式协商,为不同的redolog分配全局唯一且有序的logID。
- prepare阶段,上述一阶段的分布式协商logID并不能保证并发多个server分配得到得logID是唯一的,即会出现若干条不同的redolog在同一个Paxos Instance中投票的情况,而这正是Basic-Paxos协议的基本假设,因此需要执行prepare,以决定出这个Paxos Instance内的要进行投票的redolog(即议案)。如果执行prepare决定出的议案与server自己要投票的redolog内容不同,则需要重新产生logID。
- accept阶段,对prepare阶段决定出的议案进行投票,得到多数派确认后表示redolog同步成功,否则需要重新产生logID。
在这三个阶段中,根据Paxos协议的约束,server应答prepare消息和accept消息前都要持久化本地redolog,以避免重启后的行为与重启前自相矛盾。因此最终可以得到使用Basic-Paxos进行redolog同步的延迟包括了3次网络交互加2次写本地磁盘。并且在高并发的情况下,不同redolog可能被分配到相同的logID,最差可能会在accept阶段才会失败重试。
参考:
https://blog.csdn.net/changshaoshao/article/details/83343782
https://www.cnblogs.com/linbingdong/p/6253479.html