共识算法PBFT和Raft

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言


提示:以下是本篇文章正文内容,下面案例可供参考

一、为什么需要共识算法?

背景:分布式系统集群设计中面临着一个不可回避的问题,一致性问题

对于系统中的多个服务节点,给定一系列操作,如何试图使全局对局部处理结果达成某种程度的一致?

这个一致性问题大致有如下的场景:

节点之间通讯不可靠的,延迟和阻塞。

节点的处理可能是错误的,甚至节点自身随时可能宕机。

节点作恶(拜占庭为例)。

二、经典的共识算法

在这里插入图片描述

2.1.Raft

Raft算法中,有三种很重要的超时设置:选举超时、最小选举超时、心跳超时。下文给大家详细的介绍一下。

(1)选举超时。就是新一轮选举开始时,每个节点随机思考要不要做领导者的时间,这个时间一般100-到200ms,非常短。假设集群由3个节点组成,为了防止3个节点同时发起投票,Raft会给每个节点分配一个随机的选举超时时间(Election Timeout)。在这个时间内,节点必须等待,不能成为Candidate状态。现在假设节点a等待168ms,节点b等待210ms,节点c等待200ms。由于a的等待时间最短,所以它会最先成为Candidate,并向另外两个节点发起投票请求,希望它们能选举自己为Leader。另外两个节点收到请求后,假设将它们的投票返回给Candidate状态节点a,节点a由于得到了大多数节点的投票,就会从Candidate变为Leader。

(2)最小选举超时。在分布式系统中,有时候需要对集群中的成员数量进行更新的操作。对于被下线的服务器而言,如果它们没有及时关闭,那么它们将不会接收到心跳信息和日志信息,从而不断发生超时,最后导致任期不断增加(高于集群中所有成员的任期),然后不断向集群中发送请求投票消息。集群中的Leader将变为Follower,集群中将不断开始新的选举,从而扰乱集群的正常运行。

解决方案:Raft引入了一个最小选举超时时间,意思是如果集群中存在Leader时,并且接收到心跳信息之后在最小选举超时时间内接受到请求投票消息,那么将会忽略掉该投票消息。

(3)心跳超时。心跳是Leader发送心跳给Follower的时间。这个时间一旦超时,Follower会觉得Leader挂了,这样会开始新的选举周期。

看到如上解释,我相信熟悉Java开发的程序员多多少联想到它和SpringCloud和zookeeper多多少有密不可分的联系。下面我们来说一说算法执行的步骤:
这里我们假设有三个节点参与(A、B、C)。

第一步,在最初,还没有一个主节点的时候,所有节点的身份都是Follower。每一个节点都有自己的计时器,当计时达到了超时时间(Election Timeout),该节点会转变为Candidate。
在这里插入图片描述
第二步,成为Candidate的节点,会首先给自己投票,然后向集群中其他所有的节点发起请求,要求大家都给自己投票。
在这里插入图片描述
那么,投票规则是什么呢?下面将讲述投票的规则步骤:竞选节点发起投票请求时,会向所有follower节点发送自己日志的任期和索引号, follower节点收到后会比较自己的日志是否比竞选节点的新,先比较任期,任期大的的日志最新,如果任期一样,则比较索引号,索引号大的日志最新。

但是,如果出现网络中断后又恢复又会出现什么情况呢?

当C节点和A和B节点由于网络问题失去联系,会触发选举,并转为Candidate。每次发起选举时,会把Term加1。由于网络隔离,它既不会被选成Leader,也不会收到Leader的消息,而是会一直不断地发起选举。Term会不断增大,这会产生什么问题呢?

在网络恢复之后,因为C还是处于竞选中,它这会把它的Term传播到集群的其他节点,其他节点认为自己的日志比它旧,就肯定会选它为leader,,但事实上C节点的日志可能会落后其他节点很多了,显然是不应该成为leader节点的。那如何避免这种情况发生呢?
Raft算法对竞选机制进行了改良,就是所谓的预选举。Candidate首先要确认自己要能赢得集群中大多数节点的投票,这样才会把自己的term增加,然后发起正式的投票,如果预选举不通过,则该节点的term不会增加。关键逻辑如下:

// becomePreCandidate()方法
func (r *raft) becomePreCandidate() {
    if r.state == StateLeader {
        panic("invalid transition [leader -> pre-candidate]")
    }
    r.step = stepCandidate
    r.prs.ResetVotes()
    r.tick = r.tickElection
    r.lead = None
    r.state = StatePreCandidate
    r.logger.Infof("%x became pre-candidate at term %d", r.id, r.Term)
}


//becomeCandidate()方法
func (r *raft) becomeCandidate() {
    // TODO(xiangli) remove the panic when the raft implementation is stable
    if r.state == StateLeader {
        panic("invalid transition [leader -> candidate]")
    }
    r.step = stepCandidate
    r.reset(r.Term + 1)    //一旦变为竞选角色,term立马加1
    r.tick = r.tickElection
    r.Vote = r.id
    r.state = StateCandidate
    r.logger.Infof("%x became candidate at term %d", r.id, r.Term)
}

可以看到在宕机预选举时Term并不会增加,而是等进入正式选举时Term才会增加。

第三步,其他收到投票请求且还未投票的Follower节点会根据规则向发起者投票,发起者收到同意反馈通知后,票数增加。
在这里插入图片描述

第四步,当得票数超过了集群节点数量的一半,该节点晋升为Leader节点。Leader节点会立刻向其他节点发出通知,告诉大家自己才是老大。收到通知的节点全部变为Follower,并且各自的计时器清零。
在这里插入图片描述
第五步,由Leader节点通知集群内所有的Follower节点提交数据,从而完成数据同步流程。

2.2PBFT

2.2.1PBFT概述

在这里插入图片描述

9个将军带领9支军队,打一场攻城战役。假设每个将军都能独立根据眼前战况做出两种判断:进攻或撤退,要求(或者最终目的是)如何让这9个将军的命令是一致的(一致性,即共识)?要么一起进攻,要么一起撤退(每个将军之间也是互不信任的,也有消灭对方的动机)

最简单的策略即:投票(上图中的红色箭头和绿色箭头为每个将军做出的判断),超过半数支持某个决定,那么所有9个将军一定执行这个决定。如上图,5个将军决定进攻,4个将军决定撤退,那么所有将军都会下令:进攻!

这种策略需要每个将军把自己的判断通过一种途径(途中灰色箭头)传递到所有其他将军处。相对的,每个将军只有在收到了所有投票结果后,才会下令。如上面的例子,所有将军得到投票:4进攻5撤退,才下令撤退。
这个投票策略的最大问题:假设出现了叛徒,如上图所示,会出现两种情况

【1】对自己位置的战场情况进行错误广播(比如他这个地方优势很大,但是投票给撤退)
【2】可以选择靠给不同的将军送去不同的消息破坏整体决定的一致性(导致左边四个将军选择撤退,右边四个将军选择进攻)
此时总结一下,拜占庭问题的问题到底是什么:

所有将军如何才能达成共识去攻打(或撤退)城堡。根据相关的研究,得出一个【一般性的结论】:如果叛徒的数量大于或等于三分之一 ,那么拜占庭问题不可解,这个三分之一也被称为拜占庭容错,三模冗余是完全无法容错的(也就是说无解,不可能保持一致性)释方法使用副官模型即可。
推广到计算机系统内,【将军】类比为【计算机】,而计算机因为物理或被感染等其他原因造成的【运行异常】就是【叛徒】,其实整个问题也是为了保证分布式系统的一致性和可用性。

2.2.2PBFT算法概述

这个算法说起来也不难理解,他的核心思想是:对于每一个收到命令的将军,都要去询问其他人,他们收到的命令是什么。也就是说利用不断的信息交换让可行的节点确认哪一个记录选择是正确的,即发现其中的背叛者。采用PBFT方法,本质上就是利用通信次数换取信用。每个命令的执行都需要节点间两两交互去核验消息,通信代价是非常高的。通常采用PBFT算法,节点间的通信复杂度是节点数的平方级的。
在这里插入图片描述
其中每个将军投降下方的数字就是收到的攻击事件列表,在该规则下,可以看到能保证,当叛徒数量小于1/3维护系统的一致性,即无论是什么情况,都可以防止不一致的决定被执行(至少也是按兵不动,并且很容易定位叛徒是谁)

比如忠诚将军首先发动决定这个例子。这里的叛徒,虽然收到了三个1的进攻消息,但是他却向外广播错误的2撤退消息。但是B将军已经看到了有两个将军是同意进攻,一个反对的。少数服从多数,最终得以攻下城市。

再比如背叛将军首先发动决定这个例子。C叛徒分别向A、B、D发送1、2、3点进攻的不统一消息。A、B、D三个将军交换信息(123)知道了信息不是一样的(比如都是111、222或者333)。则暂停进攻。只需核对信息是谁发出则可以找到背叛者。

注意,在这种仅有4个节点的情况下看似复用信道和传递消息的数量不多。但随着结点的增加,时间复杂度和信道使用量级是节点数的平方。大规模网络基本瘫痪,效率太低。

三、总结与展望

通过上面的叙述,我们知道Raft是为了在分布式系统中为了选出一个leader而产生的算法。leader会很好的维护数据的一致性。如果follower挂掉还好说,但是leader挂掉就比较麻烦了。而PBFT是为了让系统不被恶意节点破坏的一种算法。但是Raft的TimeOut产生是一种随机的行为,有诸多不确定性。
在此,提出一种思考。分布式的leader通常都是单个节点。不会像区块链一样动辄上千上万的block。节点间的通信量和次数和区块链相比其实非常小。我们能否将Raft中融合拜占庭算法的思想,用共识方式来动态产生leader?一是可以去除选举,二是良好的降低了leader宕机的可能性。
分布式服务通常都是部署在不同地区的某台服务器上,比如上海,广州,北京。。。各个地区都有可能成为节点。我们都知道网络中有很多拥塞,且服务器内部本身也可能出现拥塞。leader通常如果不是停电等天灾行为,一般都是由于压力过大而产生的宕机行为(leader服务器本身也运行了一定量的服务程序)。我们可以给leader设置一些阈值比如:1.通信时延。2.内存和硬盘剩余容量。3.剩余可用核数,等等。当这些阈值被触发的时候时候leader可以通知follower需要进行新一轮选举。每台follower(包括leader)都可以将自己与其他服务器的通信时延记录、内存剩余容量、硬盘剩余容量、可用core数量等参数记录下来广播给其他节点(所需通信量很小)。其他节点收到消息之后根据某些个原则比如:
1.通信时延时延最小。(权重较小)
2.内存和硬盘剩余容量最多。(权重次大)
3.剩余可用核数最多。(权重较小)
4.日志数据最新(这里借鉴Raft的与选举机制,宕机自选举不增加term。权重最大,硬性要求)
通过以上原则来共识产生该时段的leder。省去选取过程。选出当前时间段性能最好的结点。

四、参考文档

https://www.cnblogs.com/zqwlai/p/15324984.html
http://www.mybatis.cn/archives/1631.html
https://xie.infoq.cn/article/0a8c579d8110b2025b536f79e
https://blog.csdn.net/bjweimengshu/article/details/80222416
https://www.cnblogs.com/sueyyyy/p/9757681.html
https://charlesliuyx.github.io/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值