Raft算法解析-尝试讲解的透彻一点

前言

Raft算法是我今天无意间看到的东西,就想着了解一下。

Raft简介

这个东西是用来解决分布式中数据的一致性问题。(我当时看这个东西也是一脸懵逼,但是先别卡在这里,先继续看下去)

Raft状态机介绍

先放张图吧,画的不是很好。
在这里插入图片描述
咱们在开始介绍以前呢,先多啰嗦一下需要的背景知识。
1.任期号。这个概念应该很好理解,因为吧,这个分布式的一堆服务器也好,数据库也好,是用一种统治的方式去描述的,既然说到了统治,那么就会有王朝的兴衰更迭对吧,于是呢,每一次更新统治者的时候,都会同时更新这个任期号。每个分布式的子对象都有这样的任期号,当然大家的任期号基本上是一致的,不过也存在有的人任期号是旧的(就像新中国都成立了,然而有些偏远地区的人却还以为大清依旧健在)。
2.统治者-追随者-选举者(造反派)模型。很简单的,所有的分布式集群设备中,每次任期只能有最多一个统治者,叫leader,然后呢,它有一批追随者,叫followers;最后呢,还会时不时的有追随者造反,当造反派,我们称之为选举者(candidates),当然我叫它造反派。
OK现在咱们来聊聊这个状态转移的事情。
1.idle状态为系统刚上电完成初始化的状态,准备好后直接进入追随者状态。
2.追随者状态下,每个服务器都维护一个任期号,同时也都有一个选举超时计时器。这是干嘛的呢?leader(反正就是这个分布式集群里的某个设备)会定时的周期性的给其他所有人发送AppendEntries RPC消息,这东西带的数据是空的,就是为了安抚它的小弟们,让他们重启自己的选举超时计时器,不要叛乱当造反派。同时呢,这个AppendEntries RPC消息呢,带了当前的任期号,所以呢,这个东西也可以统一小弟们维护的自己的任期号。
3.完了,追随者状态下,有那么一些人没有按时收到这个消息,于是呢,这些没收到安慰的追随者们,先随机生成一个时间,然后开始等待这段时间(就是为了防止好多追随者一起叛乱,难搞)。时间一到,开始造反!于是这个家伙就领先了那些同样企图造反的追随者(因为它生成的随机等待时间很短嘿嘿嘿)。它肯定要改朝换代呀,于是它在自己维护的任期号基础上+1,同时摇身一变,变成了造反派。
4.造反派们的最终目的是成为leader统治者呀!但是咱们学过历史的都知道,想成为成功的leader,那必须要先获得人民群众的支持才行。Raft协议规定,必须得获得超过半数的支持!为啥是半数?为啥不是选举人里最多的就行呢?废话,这样就不用了解其他造反派的支持人数了。造反派先给自己投一票,然后呢,它开始给其他人发一种叫RequestVote RPC的消息,意思是说“大家都来支持我呀!”,然后呢,支持它的人就会回复它消息的,告诉它“好的!我支持你!”。如果有多个造反派同时在拉拢别人,那么对于吃瓜群众来说,谁先拉拢我,我就支持谁。当然啦,吃瓜群众也不是啥都不懂,它会做一点小小的判断:如果这个拉拢我的造反派是一个比自己还要旧的旧社会的残党,大清都亡了但是这个人却还不知道,那就不理他,干嘛理这种傻逼?(判断的标准是造反派的消息中携带的任期号是不是比自己要大,不满足的话就忽略)。
5.选举嘛,无非是三个结果:首先是它成功当选!当它发现,哎嘿嘿,有超过一半的人支持我,它就摇身一变成leader,开始发号施令啦。当然,也许他没那么好运,于是它等来了其他造反派成为leader的发号施令(太惨了),那没办法了呀,也就只能乖乖地当墙头草,听leader的话。最后一种比较僵硬,谁也没能当上老大。(其实很好理解,因为同一时间出现了俩造反派,然后他俩获得的选票数量还是一样多的)那没办法了呀,只能把任期号+1,咱们再拉一个PK场,继续角逐吧。
6.终于成为leader了,要干点啥呢?首先对于状态而言,肯定是要周期性的搞点事情,比如安抚小弟(发AppendEntries RPC),当然它也得干点正事呀(我指的是对外的,比如客户端)。咱们聊一下leader倒台的流程。很简单,就是它某一天突然接受到了更高任期号的消息(说来也荒谬,只要有人站出来了,且成立一个更先进的朝代,它自己就乖乖下台了)

上伪代码!

节点刚启动,进入follower状态,同时创建一个超时时间在150-300毫秒之间的选举超时定时器。
follower状态节点主循环:
  如果收到leader节点心跳:
    心跳标志位置1
  如果选举超时到期:
    没有收到leader节点心跳:
      任期号term+1,换到candidate状态。
    如果收到leader节点心跳:
      心跳标志位置空
  如果收到选举消息:
    如果当前没有给任何节点投票过 或者 消息的任期号大于当前任期号:
      投票给该节点
    否则:
      拒绝投票给该节点
candidate状态节点主循环:
  向集群中其他节点发送RequestVote请求,请求中带上当前任期号term
  收到AppendEntries消息:
    如果该消息的任期号 >= 本节点任期号term:
      说明已经有leader,切换到follower状态
    否则:
      拒绝该消息
  收到其他节点应答RequestVote消息:
    如果数量超过集群半数以上,切换到leader状态
    
  如果选举超时到期:
    term+1,进行下一次的选举

当然了,这里我是引用了网上一位老哥,侵删。

leader要干点啥

我指的是leader对外部的如客户端发来的请求。

日志复制!!

对没错,就是让它的子民同步记录这些操作。那么是怎么做到的呢?
首先看一下Raft的日志格式:
在这里插入图片描述
第一个是log index,这个东西就是逐步递增的。
第二个是任期号,这个不解释。
第三个是command操作,这个就是想执行的操作,比如set一个数据。
具体的流程是:
1.所有客户端的请求都被重定向到leader服务器上;
2.leader服务器先把这个请求操作对应的log日志生成一下,然后写到自己的日志记录中;
3.然后呢,在本地添加完日志之后,leader将向集群中其他节点发送AppendEntries RPC请求同步这个日志条目,当这个日志条目被成功复制之后(什么是成功复制,下面会谈到),leader节点将会将这条日志输入到raft状态机中,然后应答客户端。
在这里插入图片描述
一条日志如果被leader同步到集群中超过半数的节点,那么被称为“成功复制”,这个日志条目就是“已被提交(committed)”。如果一条日志已被提交,那么在这条日志之前的所有日志条目也是被提交的,包括之前其他任期内的leader提交的日志。如上图中索引为7的日志条目之前的所有日志都是已被提交的日志。
每个服务器都维护自己的log日志记录,同时呢,还有俩参数是为日志记录服务的,也需要维护,他们是:committedIndex和appliedIndex,其中committedIndex的含义是,提交成功的index(提交成功,对于leader而言,就是半数人都同步了新的操作的log;对于follower而言,其实都是leader在下一次同步中告诉它的),而appliedIndex指的是同步完成后应用到状态机中的日志索引值。
别问我啥叫输入到Raft状态机,我现在也不是很清楚,但是这里根本就不影响理解,咱们继续。
总有 committedIndex >= appliedIndex不等式成立,这个应该非常好理解,因为顺序上就规定了,必须得先同步成功了,才能将其应用到Raft状态机。
4. 收到AppendEntries请求的follower节点,同样在本地添加了一条新的日志,也还并没有提交。你想想,followers哪里能知道,这条log记录已经被其他半数以上的人同步了呢?肯定不知道呀,所以它只能默默地把这条log记录先加入到自己的日志记录中,它自己的committedIndex能修改么?答案是可以的,但是不能像leader那样修改到最新。followers只能修改到最新-1,啥意思?很好理解,因为虽然刚刚到的这条log,我作为随从,没法知道是不是超过半数而提交成功,但是您leader既然都已经发了这一条了,那想必上一条,一定已经成了。为啥followers能知道,leader的committedIndex呢?废话,leader发过来的信息里带了。。。
5. follower节点向leader节点应答AppendEntries消息。肯定得应答,这样leader才能做统计。
6. 好了,leader发现确实超过半数同步成功了,于是认为同步操作成了,自增committedIndex;所有提交的日志都在磁盘中,不会掉电丢失的。
7. 同步日志完成操作后,leader就将日志输入到Raft状态机里咯,同时修改AppliedIndex,美滋滋。

一致性检查

这个也很好理解。刚刚咱们说了,leader发送同步日志消息给followers时会带上自己的committedIndex,这个committedIndex一般呢,会是followers当前最新的committedIndex+1对吧。如果followers发现,卧槽,不是这样的,那么这个followers就认为有异常,我不能接受!

日志复制的两点保证

1.如果不同日志中的两个条目有着相同的索引和任期号,则它们所存储的命令是相同的;
2.如果不同日志中的两个条目有着相同的索引和任期号,则它们之前的所有条目都是完全一样的(原因的话参考上面的一致性检查);

日志不正常了咋办

这种事情的发生是很好理解的,followers很可能拥有一些leader没有的日志,或者是followers断档了,缺了不少日志。这个很好理解的,你想想,如果在同步的过程中,leader宕机,然后他的followers们都收到了,且更新了committedIndex,这不就followers超前了么。followers滞后更简单了,它自己宕机呗。
要不给你看看这张不同步的图:
在这里插入图片描述
那如何解决呢?其实也很简单,就是作为leader,还得维护一个followers的数组,这里面是每个follower的下一次给该节点同步日志时的日志索引和这个节点目前的最大的index,分别称为nextIndex和matchIndex。在follower与leader节点之间日志复制正常的情况下,nextIndex = matchIndex + 1。但是如果出现不一致的情况,则这个等式可能不成立。
每次leader上任,都要把这个matchIndex全部初始化成0,因为刚一来,leader啥也不知道它小弟们的底细。
每次leader给小弟们发同步信息指令时,如果小弟回复,OK,那么leader就知道,没毛病。
但是如果小弟发现一致性检查无法通过,于是会回复,Error,同时呢,会带上自己的最大的Index,发给leader,让leader下次别再一次次的尝试了。
咱们拿上面这张图举个例子好了。
1.a节点:由于节点的最大日志数据二元组是<6,9>,正好与leader发过来的日志<6,10>紧挨着,因此返回复制成功。同时呢,leader也会更新那个follower数组的a项;
Leader为了使Followers的日志同自己的一致,Leader需要找到Followers同它的日志一致的地方,然后覆盖Followers在该位置之后的条目。

安全性

选举限制

并不是什么牛鬼蛇神都能做leader的,咱们看看上面的同步就清楚了,leader可是很粗暴的家伙,它完全不认同那些followers中超前自己的log,只会完全让他们删除掉。于是您想想,如果让一个宕机断档了很久的家伙担当leader,那它不得开倒车???
一个节点会投票给一个节点,其中一个充分条件是:这个进行选举的节点的日志,比本节点的日志更新。之所以要求这个条件,是为了保证每个当选的节点都有当前最新的数据。为了达到这个检查日志的目的,RequestVote RPC请求中需要带上参加选举节点的日志信息,如果节点发现选举节点的日志信息并不比自己更新,将拒绝给这个节点投票。
如果判断日志的新旧?这通过对比日志的最后一个日志条目数据来决定,首先将对比条目的任期号,任期号更大的日志数据更新;如果任期号相同,那么索引号更大的数据更新。
继续引用老哥的伪代码:

follower节点收到RequestVote请求:
  对比RequestVote请求中带上的最后一条日志数据:
    如果任期号比节点的最后一条数据任期号小:
      拒绝投票给该节点
    如果索引号比节点的最后一条数据索引小:
      拒绝投票给该节点
    其他情况:
      说明选举节点的日志信息比本节点更新,投票给该节点。

提交前面任期的日志条目

Leader只能推进commit index来提交当前term的已经复制到大多数服务器上的日志,旧term日志的提交要等到提交当前term的日志来间接提交(log index 小于 commit index的日志被间接提交)。

集群成员的变更

任何时候都不能出现集群中存在一个以上leader的情况。为了避免出现这种情况,每次变更成员时不能一次添加或者修改超过一个节点,集群不能直接切换到新的状态,如下图所示。
在这里插入图片描述
在同一时刻,S3、4、5是新加入的节点,他们仨可能自发的选举出一个新的leader,这是不允许的。
raft采用将修改集群配置的命令放在日志条目中来处理。
另外,咱们添加新的节点到集群中,会导致集群出现故障的。看下图:
在这里插入图片描述
左边本来是三个服务器组成的集群,现在突然加入设备4.本来也没啥关系,因为4是刚加入的,所以4里的日志啥也没有,相当于是暂时坏的。但是您想想,如果在4坏的这段时间,突然3页坏了,那么本身4个服务器坏了俩!那不就超过半数了么!!!所以,leader在给4同步日志的过程中,先不将4算在集群数量内,直到等到4追上集群的日志数量,再将其算入集群数量中。
leader同步数据给新节点的流程是,划分为多个轮次,每一轮同步一部分数据,而在同步的时候,leader仍然可以写入新的数据,只要等新的轮次到来继续同步就好。
在这里插入图片描述
如上图中,划分为多个轮次来同步数据。比如,在第一轮同步数据时,leader的最大数据索引为10,那么第一轮就同步10之前的数据。而在同步第一轮数据的同时,leader还能继续接收新的数据,假设当同步第一轮完毕时,最大数据索引变成了20,那么第二轮将继续同步从10到20的数据。以此类推。
这个同步的轮次并不能一直持续下去,一般会有一个限制的轮次数量,比如最多同步10轮。
删除leader节点时,leader节点将发出一个变更节点配置的命令,只有在该命令被提交之后,原先的leader节点才下线,然后集群会自然有一个节点选举超时而进行新的一轮选举。
如果Leader主动将集群中的某个服务器移除出去,,但是这个节点又不知道这个情况,那么按照前面描述的Raft算法流程来说,它应该在选举超时之后,将任期号递增1,发起一次新的选举。
咱们为了防止这个节点对集群产生干扰,于是对于followers接受别人的选票时,要加个筛选流程:如果台面上有个leader正在活跃,那么这些followers们就不会接受这个选票。

follower处理RequestVote请求:
  如果请求节点的日志不是最新的:
    拒绝该请求,返回
  如果此时是leader迁移的情况:
    接收该请求,返回
  如果最近一段时间还有收到来自leader节点的心跳消息:
    拒绝该请求,返回
  接收该请求

日志压缩

咱们肯定不能让日志无限增长下去,否则某次重启的时候你肯定得重读这些日志来完成数据恢复,那你要做的操作可就海了去了。
snapshot快照就很棒了。leader会给各位followers发送snapshot RPG指令。接到指令的家伙们就会把自己的committedIndex以前的指令全部丢弃掉,同时生成一个snapshot记录放在那里。这个snapshot里放了啥呢?举个例子,比如之前多次对变量a进行操作,那么我只保留最后一次对a的操作,而对其他变量也是同样的道理。
在这里插入图片描述

高效处理只读请求

上面的流程咱们清楚了以后,那么问题来了:如果客户端只是提出了读请求,那这一整套流程下来,花费的时间其实是很大的。可是不这么做的话又不安全。只读操作又没有改变数据,为啥还要搞这套流程呢?
咱们就说一个方法好了。
首先呢,对于刚上台的leader,很可能会有上一个leader同步了一半就下台的情况。这事儿要是没遇上客户端的读请求呢,那么新leader就自己干自己的事情,这样呢,旧leader的那些同步了一半就去世的log就能默认被同步成功。
但是如果上来就遇到客户端的读请求,那么这个新leader会先给各位followers发一条no-op的空白log。followers收到这条消息后,还是会正常发一些消息回复leader,但是不会将此写到log里(不过还是会更新committedIndex)。
新leader收到回复消息后相当于变相的提交了旧leader的中道崩殂log,同时这个新leader还得再搞一次心跳消息的广发,确定自己的leader地位。
最后leader执行读操作,然后将结果返回给客户端。

猜你喜欢

转载自blog.csdn.net/weixin_44039270/article/details/106953316
今日推荐