文章目录
- 拜占庭将军问题
- Paxos
-
- 问题描述
- 执行过程
-
- Prepare阶段
- Accept阶段
- Learner获取提案
- 活锁问题
- Raft
-
- 状态机
- 执行流程
-
- 主节点选举
- 数据同步
拜占庭将军问题
拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了达到防御目的,每个军队都分隔很远,将军与将军之间只能靠信差传消息。在战争的时候,拜占庭军队内所有将军和副官必须达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,叛徒可以任意行动以达到以下目标:欺骗某些将军采取进攻行动;促成一个不是所有将军都同意的决定,如当将军们不希望进攻时促成进攻行动;或者迷惑某些将军,使他们无法做出决定。如果叛徒达到了这些目的之一,则任何攻击行动的结果都是注定要失败的,只有完全达成一致的努力才能获得胜利。
这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成 。
拜占庭将军问题的核心在于在缺少可信任的中央节点和可信任的通道的情况下,分布在不同地方的各个节点应如何达成共识。我们可以将这个问题延伸到分布式计算领域中。
在分布式计算领域中,试图在异步系统和不可靠的通道上达到一致性状态是不可能的,因此在对一致性的研究过程中,都往往假设信道是可靠的,而事实上,大多数系统都是部署在同一个局域网中,因此消息被篡改的情况非常罕见;另一方面,由于机器硬件和网络原因导致的消息不完整问题,也仅仅只需要一套简单的校验算法即可避免。
那么当我们假设拜占庭问题不存在(所有消息都是完整的,没有被篡改),那么这种情况下需要什么算法来保证一致性呢?拜占庭将军问题的提出者Lamport提出了一种非拜占庭将军问题的一致性解决方案——Paxos算法
Paxos
问题描述
与拜占庭将军问题一样,Lamport同样使用故事的方式来提出Paxos问题
希腊岛屿Paxon上的议员在议会大厅中表决通过法律,并通过服务员传递纸条的方式交流信息,每个议员会将通过的法律记录在自己的账目上。问题在于执法者和服务员都不可靠,他们随时会因为各种事情离开议会大厅,并随时可能有新的议员进入议会大厅进行法律表决,使用何种方式能够使得这个表决过程正常进行,且通过的法律不发生矛盾。
不难看出故事中的议会大厅就是分布式系统,议员对应节点或进程,服务员传递纸条的过程就是消息传递的过程,法律即是我们需要保证一致性的值。议员和服务员的进出对应着节点/网络的失效和加入,议员的账目对应节点中的持久化存储设备。上面表决过程的正常进行可以表述为进展需求:当大部分议员在议会大厅呆了足够长时间,且期间没有议员进入或者退出,那么提出的法案应该被通过并被记录在每个议员的账目上。
Paxos算法需要解决的问题就是如何在上述分布式系统中,快速且正确地在集群内部对某个数据的值达成一致,并且保证不论发生以上任何异常,都不会破坏整个系统的一致性。
Paxos解决的问题
为了能够使得表决过程正常进行,且通过的法律不发生矛盾,那么我们需要保证以下几点
- 在这些被提出的提案中,最后只能有一个被选定
- 如果没有提案被提出,那么就不会有被选定的提案
- 当一个提案被选定后,节点们应该可以获取被选定的提案信息
在Paxos算法中,主要有以下三种角色,并且一个节点可能同时担任几种角色
- 提议者(Proposer):负责提出提议;
- 接受者(Acceptor):对每个提议进行投票;
- 告知者(Learner):被告知投票的结果,不参与投票过程。
各节点之间的关系
同时,假设不同参与者之间通过收发消息来进行通信,那么我们需要具备以下条件
- 每个参与者可能会因为出错而导致停止、重启等情况,
- 虽然消息在传输过程中可能会出现不可预知的延迟、重复、丢失等情况,但是消息不会被损坏或篡改(即不存在拜占庭问题)
执行过程
首先我们规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。
Paxos执行过程主要分为两个阶段,与之前讲过的2PC(二阶段提交协议)类似。
- Prepare阶段
- Accept阶段
Paxos算法流程
Prepare阶段
- 首先,每个Proposer都会向所有的Acceptor发送一个Prepare请求。
- 当Acceptor接收到一个Prepare请求,提议内容为[n1,v1],由于之前还未接受过Prepare请求,那么它会返回一个[no previous]的Prepare响应,并且设置当前接受的提议为[n1,v1],同时保证以后不会再接收序号小于n1的提议。
- 如果Acceptor再次收到一个Prepare请求,提议内容为[n2,v2],此时会有两种情况。
- 如果n2 < n1,此时Acceptor就会直接抛弃该请求
- 如果n2 >= n1,此时Acceptor就会发送一个[n1,v1]的Prepare响应,设置当前接受到的提议为[n2,v2],同时保证与上一步逻辑相同,保证以后不会再接受序号小于n2的提议。
Accept阶段
- 当一个Proposer接收到超过一半的Acceptor的Prepare响应时,此时它就会发送一个针对[n,v]提案的Accept请求给Acceptor。
- 如果一个Acceptor收到一个编号为n的提案的Accept请求,此时有两种情况。
- 如果该Acceptor没有对编号大于n的Prepare请求做出过响应,它就会接受该提案,并发送Learn提议给Learner
- 如果该Acceptor接受过编号大于n的Prepare请求,那么它就会拒绝、不回应或回复error。(如果一个Proposer没有收到过半的回应,那他就会重新进入第一阶段,递增提案号后重新提出Prepare请求)
在上述过程中,每一个Proposer都有可能会产生多个提案。但只要每个Proposer都遵循如上述算法运行,就一定能保证算法执行的正确性。
Learner获取提案
在Accept阶段之后Acceptor选定提案后,根据具体的应用场景不同,Learner主要采用以下三种方案来学习选定的提案
方案 | 优点 | 缺点 |
---|---|---|
Acceptor直接将提议发给所有的Learner | Learner能够快速获取被选定的提议 | 通信次数过多,每个Acceptor都要和Learn产生通信(M * N) |
Acceptor接受提议后将提议发给主Learner,主Learner再通知其他Learner | 通信次数减少(M + N – 1) | 单点问题(主Learner出现故障就会崩溃) |
Acceptor将提议发一个Learner集合,Learener集合再发给其他Learener | 解决了单点问题,集合中Learner个数越多就越可靠 | 网络通信复杂度变高 |
当Learner们发现有大多数的Acceptor接受了某一个提议,那么该提议的提议值则就是Paxos最终选择出来的结果。
活锁问题
在Paxos算法实际运作的时候还存在这样一种极端的情况——当有两个Proposer依次提出了一系列编号递增的提案,此时就会导致陷入死循环,无法完成第二阶段,也就是无法选定一个提案,如以下场景。
- Proposer P1发出编号为n1的Prepare请求,收到过半响应,完成了阶段一的流程。
- 同时,Proposer P2发出编号为n2的Prepare请求(n2 > n1),也收到了过半的响应,完成了阶段一的流程,并且Acceptor承诺不再接受编号小于n2的提案。
- P1进入第二阶段时,由于Acceptor不接受小于n2的提案,所以P1重新回到第一阶段,递增提案号为n3后重新发出Prepare请求
- 紧接着,P2进入第二阶段,由于Acceptor不接受小于n3的提案,此时它也重新回到第一阶段,递增提案号后重新发出Prepare请求
- 于是P1、P2陷入了死循环,谁都无法完成阶段二,这也就导致了没有value能被选定。
为了保证Paxos算法的可持续性,以避免陷入上述提到的死循环,就必须选择一个主Proposer,并规定只有主Proposer才能够提出提案。这样一来,只要主Proposer和过半的Acceptor能够正常进行网络通信,那么肯定会有一个提案被批准(第二阶段的accept),则可以解决死循环导致的活锁问题。
Raft
从上面可以看出,Paxos算法相对来说较为复杂且难以理解,因此在后来又出现了一种用于替代Paxos的算法——Raft
Raft算法是一种简单易懂的共识算法,所谓共识,就是多个节点对某个事情达成一致的看法,即使是在部分节点故障、网络延时、网络分割的情况下。它依靠状态机和主从同步的方式,在各个节点之间实现数据的一致性。
Raft算法的核心主要为以下两部分,下面将进行具体讲解
- 主节点选举(Leader Election)
- 数据同步(Log Replication)
状态机
在Raft中节点中存在三种状态,状态之间可以相互进行转换
-
Leader(主节点)
-
Follower(从节点)
-
Candidate(竞选节点)
同时,每个节点上会存放一个倒计时器(Election Timeout),时间随机在150ms到300ms之间。Leader节点会周期性的向所有Follower发送一个心跳包(Heartbeat),收到心跳包的节点会将其计时器清零后重新计时,如果在倒计时结束前没有收到Leader的心跳包,此时Follower就会变为Candidate,开始进入竞选状态。
具体的状态转移如下图
状态转换图
执行流程
主节点选举
介绍了状态机后,下面就来看看主节点的选举流程
第一步,在一开始时,由于没有Leader,所以此时所有节点的身份都是Follower,每一个节点上都有着自己的计数器,当计时器达到了超时时间后,该节点就会转换为Candidate,开始选举过程。
第一步:开始选举
第二步,成为Candidate的节点首先会给自己投一张票,然后向集群中的其他所有节点发起投票请求。
第二步:开始拉票
第三步,收到投票请求并且还未投票的Follower节点会向发起者回复投票反馈,如果收到了超过半数的回复,此时Candidate选举成功,状态转变为Leader。
第三步:选举成功
第四步,Leader节点会立刻向其他节点发出通告,告知其他节点它已成功选举成Leader,收到通知的节点会转换为Follower。并且Leader会周期性的发送心跳包给所有Follower来表明它还存活,当Follower接收到心跳包时,就会重置计时器。
第四步:Leader向其他节点发送通知
一旦Leader节点挂掉,它就无法发出心跳包来重置Follower的倒计时器,那么当Follower的超时时间到达后,其就会转换成Candidate节点,再次重复以上过程。
上面介绍的是单节点的选举,倘若同时有多个Follower同时倒计时结束后成为Candidate,同时开始选举,并且在选举过程中所获得的票数相同,此时就陷入了僵局,谁都无法成为Leader。
多个Candidate选举时出现僵局
在重新选举时,由于每个节点设置的超时时间都是随机的,因此下一次同时出现多个Candidate并获得同样票数导致选举失败的情况的概率则非常低。
随机化超时时间避免冲突
数据同步
Raft中数据同步的方式与之前写过的2PC(二阶段提交协议)有一些相似,也是分为投票和提交两个阶段,具体流程如下。
第一步,客户端将修改传入Leader中(此时修改并没有提交,只是写入日志中)。
客户端将修改传入Leader
第二步,Leader将修改复制到集群内的所有Follower节点上,如果复制失败,会不断进行重试直至成功。
Leader将修改复制到所有Follower
第三步,Follow节点们成功接收到复制的数据后,会反馈结果给Leader节点,如果Leader节点接收到超过半数的Follower反馈,则表明复制成功,于是提交自己的数据,并且通知客户端数据提交成功。
Follower接受成功后反馈结果
第四步,提交成功后,Leader会通知所有的Follower让它们也提交修改,此时所有节点的值达成一致,完成数据同步流程。
Leader通知所有节点提交,完成数据同步
为了便于理解,可以结合下面的Raft原理动画一同学习。
Raft原理动画
除了上面介绍的Paxos、Raft,还有其他的一些解决分布式一致性的共识算法,如Zookeeper使用的ZAB,区块链中使用的PBFT等,在未来的博客中,如果有机会我还会继续为大家分享这些内容。