第四章-Raft共识算法(一)

一、什么是Raft

  • 相比于paxos,raft最大的特性就是易于理解,为了达到这个目标,raft主要做了两个方面的事情:
  1. 问题分解:把共识算法分为三个子问题,分别是领导者选举(leader election)、日志复制(log replication)、安全性(safety)
  2. 状态简化:对算法做出一些限制,减少状态数量和可能产生的变动。

     本章重点讨论raft算法中三个基础问题:领导者选举、日志复制和安全性问题 

1-1、复制状态机 (Replicated state machine)

  • 复杂状态机:相同的初始状态+相同的输入=相同的结束状态
  • 多个节点上,从相同的初始状态开始,执行相同的一串命令,产生相同的最终状态
  • 在raft中,leader将客户端请求(command)封装到一个个log entry中,将这些log entries复制到所有follower节点,然后大家按相同顺序应用log entries中的command,根据复制状态机的理论,大家的结束状态肯定是一致的。
  • 使用共识算法的目的,就是为了实现复制状态机。一个分布式场景下的各节点之间,就是通过共识算法来保证命令顺序的一致,从而始终保持他们的状态一致,从而实现高可用。
  • 复制状态机的功能可以更加强大,比如两个副一个采用行存储的数据结构,另一个采用列存储,只要他们初始数据相同,并持续发给他们相同的命令,那么同一个时刻从两个副本中读取到的结果也是一样的,这就是一种HTAP的实现方法,比如TiDB。     

        复制状态机模型: 

在这里插入图片描述

1-2状态简化 

  • 在任何时刻,每一个服务器节点都处于leader(领导者),follower(跟随者)或者canditate(Leader选举过程中的临时角色)这三个状态之一。
  • 相比于paxos,这一点就极大简化了算法的实现,因为raft只需考虑状态的切换,而不用像paxos那样考虑状态之间的共存和相互影响。

         图例解析:

  1.  任何一个节点启动的时候都属于follower状态。
  2. 如果发现集群中没有leader的话,会从follower状态切换到candidate状态。
  3. 在candidate状态中会经历一次或者多次的选举,最终会根据选举的结果决定自己切换到leader状态还是follower状态。
  4. 如果选举成功进入leader状态,并且在leader状态为客户端提供服务,如果在leader状态的任期结束或者自身发生宕机或者其他问题,那么会切换回follower状态,并且进行下一个循环。
  •  raft把时间分割成任意长度的任期(term),任期用连续的整数标记。
  • 每一段任期从一次选举开始。在某些情况下,一次选举无法选出leader(比如两个节点收到了相同的票数),在这种情况下,这一任期会以没有leader结束(下图term3阶段);一个新的任期(包含一次新的选举)会很快重新开始。raft保证在任意一个任期内,最多只有一个leader。 
  • 任期机制可以明确的标识集群的状态,并且通过任期的比较,可以帮助我们确认一台服务器历史的状态。比如说我们可以通过查看一台服务器是否具有term2任期内的日志来判断它term2这个时间段内是否出现过宕机。 

  • raft算法中服务器节点之间使用RPC进行通信,并且raft中只有两种主要的RPC:
  1. RequestVote RPC(请求投票):由candidate在选举期间发起;
  2. AppendEntries RPC (追加条目):由leader发起,用来复制日志和提供一种心跳机制;
  • 服务器之间的通信的时候会交换当前任期号;如果一个服务器上的当前任期号比其他的小,该服务器会将自己的任期号更新为较大的那个值。
  • 如果一个candidate或者leader发现自己的任期号过期了,它会立即回到follower状态。
  • 如果一个节点接受到一个包含过期的任期号的请求,它会直接拒绝这个请求。

二、Leader选举

2-1、选举流程

        Raft年内部有一种心跳机制,如果存在leader,那么它就会周期性的向所有follower发送心跳,来维持自己的地位。如果follower一段时间内没有收到心跳,那么它就会认为系统中没有可用过的leader了,然后开始进行选举。

        开始一个选举过程后,follower先增加自己的当前任期号,并转换到candidate状态。然后投票给自己,并且并行的向集群中的其他服务器节点发送投票请求(RequestVote RPC)。

 解析:

        server-1、server-2、server-3、server-4都是属于follower,每个follower都有自己的心跳检测过期时间,如果一个follower在规定的时间内,还没有收到server-5(leader)的心跳消息的话,那么它就会认为系统中没有可用的leader,此时follower进入candidate状态,然后进行一个选举。

  •  选举最终会有三种结果
  1. 发起选举的follower获得超过半数的选票赢得选举。第一时间把状态切换为leader,并主动开始发送心跳,告诉集群成员产生了新的leader,同时结束选举流程。
  2. 其他节点赢得选举。收到新的leader发送过来的心跳后,如果新leader的任期号不小于(大于等于)自己当前的任期号,那么就从candidate回到follower状态。(注意:当前candidate的任期号已经比老leader的任期号加了一)。
  3. 一段时间之后没有任何获胜者。每个candidate都在一个自己的随机选举超时时间后,增加任期号开始新一轮投票。
  • 为什么会没有获胜者?比如有多个follower同时成为candidate,得票太过分散,没有任何一个candidate得票超过半数,此时会快速进入一个新的投票阶段。(注意:当前选举阶段没有产生任何leader这个结论,并不需要集群中的所有节点对此产生共识,而是通过每个candidate都在等待一个随机选举超时时间之后,默认进入下一个选举阶段)。
  • follower随机选举超时时间为150~300ms。(注意,如果candidate没有收到超过半数的选票,也没有收到新leader的心跳,那么它就会在150到300毫秒之间随机选择一个时间再次发起选举)

2-2、请求投票RequestVote RPC

        请求投票RPC分为request(arguments)和response(results),request由candidate发起,response由follower回复candidate。      

        请求投票RPC Request消息体:

type RequestVoteRequest struct{
    term int //自己当前的任期号,raft节点通过任期号来确定自己状态
             //以及判断接不接受这个RPC  
    candidateId int //自己的id
    lastLogIndex int //自己最后一个日志号,安全性章节说明
    lastLogTerm int //自己最后一个日志的任期,安全性章节说明
}

      follower的投逻辑非常简单,收到一个requestVoteRequest之后会先校验这个candidate是否符合条件,这里的条件有两个:

  1. 第一个是term是否比自己大。  
  2. 第二个就是安全性问题(后续补充)。

        请求投票RPC Response消息体:

type RequestVoteResponse struct{
    term int //自己当前的任期号,raft节点通过任期号来确定自己状态
             //以及判断接不接受这个RPC 
    voteGranted bool //自己会不会投票给这个candidate
}

     在确认requestVoteRequest没有问题以后,就可以开始投票了,投票的规则如下:

     对于没有成为candidate的follower节点,对于同一个任期,会按照先来先得的原则投出自己的选票,且每个投票的follower只有一张选票,投完了就没了。

      备注:raft集群启动时,所有节点都是follower,然后第一个认识到集群中没有leader的节点会把自己变成candidate,他会给自己的任期号加一,并发送投票request请求给其他follower,其余follower则响应Response给candidate。通常来讲这个新发优势使得它大概率会成为leader,在leader任期结束或者失效后,同样的集群会进入一个新的任期选举的循环。

三、日志复制(核心功能)

3-1、leader发现、消息条目、消息复制

        leader被选举出来以后,开始为客户端请求提供服务。那么客户端怎么知道新的leader是哪个节点呢? 这个问题解决的办法有很多,例:

        客户端随机向一个节点(或者是老leader)发送请求,这个时候有三种情况:

  1. 该节点就是leader,那么直接执行指令即可。
  2. 这个节点为follower,那么可以通过心跳得知leader的ID,然后告诉客户端该找谁。
  3. 找到的这个节点正好宕机了,也就是没有响应,这个时候超时以后得找其他节点获取leader。

        leader接受到客户端的指令后,会把指令作为一个新的条目追加到日志中去。

        一条日志中需要具有三个信息:

  1. 状态机指令。通常是对某个值进行某些操作,比如下图中,索引为1的日志指令是x<--3,即把x改成3。
  2. leader的任期号。日志中包含任期号,对于检查多个日志副本之间的不一致情况,和判定节点状态,都有重要的作用。一个任期号用一个颜色标注,可以看到哪个节点空缺了哪个任期是非常清楚的。
  3. 日志号(日志索引)。日志号是每个日志唯一的,但是因为日志号是单调递增的,如果遇到如leader宕机的情况,会造成日志号相同,但日志内容却不同的情况。所以,只有日志号和任期号两个因素才能唯一确定一个日志。               在这里插入图片描述
  •  复制状态机实现:生成日志之后,leader就会把日志放到AppendEntries RPC中,然后并行的发送给follower,让他们复制该条目。当该条目被超过半数的follower复制后,leader就可以在本地执行指令并把结果返回给客户端。在所有机器都不出错,且网络正常的情况下,这个机制可以保证所有节点具备完整且正确的日志。
  • 我们把本地执行命令,也就是leader应用日志与状态机这一步(即把日志丢到状态机里面执行),称作提交。是不是日志复制到了超过半数的节点后,就百分百会提交呢?当然不是的,因为follower复制完成后,到follower通知leader,再到leader完成提交是需要时间的。这个时间内,如果leader宕机了,是不是这条日志虽然复制到了超过半数的节点,但没能提交呢?(安全性章节来解释)。

        上图中有一个leader,四个follower。可以看到每个follower节点的进度是不一致的。有的节点差了几个日志,有的节点遗漏了整个任期,但是在这里,只要有三个节点,包括leader在内复制到了日志,leader就可以提交了。图中可以提交的位置是log index为7的位置,因为有三个节点日志复制到了7,就构成了大多数。

        对于进度落后的follower,是怎么追上leader的呢,并保证所有节点的日志都是完整且顺序一直的呢?这里需要分为三种(下一节具体讨论):

  1. follower缓慢。
  2. folower宕机。
  3. leader宕机。              

        强调一下,作为一个共识算法,其目的是让集群中的每个节点都可用,也就是具备完整的正确的日志,在分布式系统重,故障是常见现象,一台机器出现故障了,在短时间内肯定不可用,但在恢复故障以后,依旧可以在一定时间内,能恢复可用。

3-2、日志复制进度问题

        leader或follower随时都有崩溃或缓慢的可能性,raft必须要在宕机的情况下继续支持日志复制,并且保证每个副本日志顺序的一致(以保证复制状态机的实现)。具体有三种可能(follower两种,leader一种):

follower问题:

  1. follower缓慢:如果有follower因为某一些原因没有给leader响应,那么leader会不断的重复发生追加条目请求(AppendEntries RPC),哪怕leader已经回复了客户端。leader回复了客户端意味着日志已经复制给了超过半数的节点,已经提交了。但是leader不能抛弃这个follower,leader仍然需要不断重发,直至follower追上日志。
  2. follower宕机:如果有follower崩溃后恢复,这个时候raft追加条目的一致性检查生效,保证follower能按照顺序恢复崩溃后的缺失的日志。这里和上面的区别在于,follower在崩溃的过程中可能发生很多事情,有可能经历了多次选举,leader已经换了好几个了,并且follower恢复后的状态也是不知道的,当前leader并不知道follower宕机前日志
  • raft的一致性检查:leader在每一个发往follower的追加条目RPC中,会放入前一个日志条目的索引位置和任期号,如果follower在它的日志中找不到前一个日志,那么它就会拒绝此日志,leader收到follower的拒绝后,会发送前一个日志条目,从而逐渐向前定位到follower第一个缺失的日志。
  • raft一致性检查虽然可以进一步优化,比如通过二分法查找,或者通过follower最新日志条目的索引位置和任期号来定位条目日志,但在实践中没有必要,因为失败不经常发生,并且也不可能有很多不一致的日志条目。

leader问题:

  • leader宕机:如果leader崩溃了,那么崩溃的leader可能已经复制了日志到部分follower但是还没有提交,而被选出的新的leader又可能不具备这些日志,这样就有部分follower中的日志和新的leader的日志不相同。
  • raft在这种情况下,leader通过强制follower复制它的日志来解决不一致的问题,这意味着follower中跟leader冲突的日志条目会被新leader的日志条目覆盖(因为没有提交 ,所以不违背外部一致性)。这意味着leader通过一致性检查找到follower中最后一个和自己一致的日志后,就会把这之后follower和自己冲突的所有日志全部覆盖掉,为什么可以这样做呢,是因为没有提交,所以说抛弃那些没有提交的日志是不违反一致性的。
  1. 下图中,最上面就是当前的leader,可以发现这个时候的c和d竟然比这个leader还多出两个日志。那么为什么leader没有这些日志还能够当选leader呢?因为c和d多出的日志没有提交,也不构成多数派。在这七个节点的集群中,leader可以通过a、b、e和自己的选票当选leader(当然f页可以投票),这个时候c和d多出来的日志就会和leader产生冲突了。
  2. 还有就是f,它当选的2、3任期内的日志,其他节点都不具备,这大概率意味着它在那两个任期内担任leader,但是它在2、3任期内的日志都没有正常复制到大多数节点,也没有提交。

        新的leader在成功当选以后,通过强制follower复制自己日志的方式实现数据的一致性,这意味着新的leader不需要任何特殊的操作来使日志恢复到一致状态

  • leader只需要进行正常的操作,发送AppendEntries RPC。
  • follower在回复AppendEnntries RPC进行一致性检查的时候,就可以自动趋于一致。
  • leader从来不会覆盖或者删除自己的日志条目。(Append-Only)
  • 这样的日志复制机制,可以保证一致性特性:
  1. 只要过半的服务器能正常运行,raft就能够接受、复制并应用新的日志条目;
  2. 在正常情况下,新的日志条目可以在一个RPC来回中,被复制给集群中的过半机器;
  3. 单个运行慢的follower不会影响整体性能;        

3-3、追加条目AppendEntries RPC具体内容

        追加条目AppendEntries RPC分为Request和Response两个部分。

        Request:

//追加日志RPC Request
type AppendEntriesRequest struct{
    term int //自己当前的任期号
    leaderId int  //leader(就是自己)的id(告诉follower我是谁)
    prevLogIndex int //前一个日志的日志号(一致性检查用)
    prevLogTerm int //前一个日志的任期号(一致性检查用)
    entries []byte //当前日志体,也就log entries
    leaderCommit int //leader的已提交日志号
}

        这里需要单独说明一个概念,那就是LeaderCommit,提交是一个非常重要的状态,对于follower而言,接收到了leader的日志,并不能立即提交。因为这时候还没有确认这个日志是否被复制到大多数节点,只有leader确认了日志被复制到了大多数节点后,leader才会提交这个日志,也就是应用到自己的状态机里。然后leader会在AppendEntries RPC中把这个提交信息告知follower,也就是leaderCommit状态。然后follower就可以把自己复制但未提交的日志设置为已提交状态,然后应用到自己的状态机里面了。

        对于那些还在苦苦追赶日志进度的follower来说,leaderCommit大于自己最后一个日志,这时它订单所有日志都是可以提交的。

        Response:

//追加日志RPC Response
type AppendEntriesResponse struct{
    term int //自己当前任期号
    success bool //如果follower包含前一个日志,则返回true
}

四、安全性问题

        前面讲到的领导者选举和日志复制,实际已经涵盖了共识算法的全部过程,但是这两点不能保证每个状态机会按相同的顺序执行相同的命令。 因为日志中的命令应用到状态机的顺序是一定不能颠倒的,但很多共识算法为了提高效率,会允许日志乱序复制到非leader的节点,这样就会在日志中出现很多空洞。

       raft是一个非常追求易理解性的共识算法,索引raft为了简化设计,避免对这些边界情况的复杂处理,在日志复杂阶段就保证了日志是有序且无空洞的。但日志复制阶段对于日志顺序的保证,能生效的前提是leader是正常的,如果leader出现宕机,他的后几个日志 的状态就有可能出现不正常了,这时新leader是否具备这些不正常的日志,以及怎么处理这些不正常的日志,就是非常关键的。这也是raft的为数不多几个需要进行特殊处理的边界情况。

        所以raft通过几个补充规则完善整个算法,使算法可以在各类宕机问题下都不出错。这些规则包括(不讨论安全性条件的证明过程):

  1. leader宕机处理:选举限制。
  2. leader宕机处理:新leader是否提交之前任期内的日志条目?
  3. follower和candidate宕机处理。
  4. 时间和可用性限制。

4-1、选举限制 

        4-1-1、leader宕机处理:选举限制 

        如果一个follower落后了leader若干条日志(但没有遗漏一整个任期),那么下次选举中,按照领导者选举规则,他依旧有可能当选leader。他在当选新leader后就永远也无法补上之前缺失的那部分日志,从而造成状态机之间不一致。

        所以需要对领导者选举增加一个限制,保证被选举出来的leader一定包含了之前各任期的所有被提交的日志条目。这里有个问题预留到后续讨论,就是“被提交”这个概念(非常重要)。

        那么raft是如何做到保证被选举出来的leader一定包含了之前各任期的所有被提交的条目的呢?这就需要回到前面提到的RequestVote RPC中后两个参数了。

         RequestVote RPC包含了candidate的日志信息,如果投票者自己的日志比candidate的还新,它会拒绝掉该投票请求。那么怎么定义“新”这个概念呢?raft通过比较两份日志中最后一条日志条目的索引值和任期号来定义谁的日志比较新。

  • 如果两份日志最后条目的任期号不同,你那么任期号大的日志更“新”。
  • 如果两份日志最后条目的任期号相同 ,那么日志号更大的那个条目更“新”。        

        为了需要强调“新”这个概念呢?看下图

         解析:

  • (a)中S1是leader并且部分地复制了index-2。
  • (b)中S1宕机,S5得到S3、S4、S5的投票当选为新的Leader(S2不会选择S5,因为S2的日志较S5新),并且在index-2写入到一个新的条目,此时是term=3(注:之所以是term=3,是因为在term-2的选举中,S3、S4、S5至少有一个参与投票,也就是至少有一个知道term-2,虽然他们没有term-2的日志);
  • (c)中S5又崩溃了,这时候S1重启,并且选举成功,此时日志2已经被复制到了大多数机器上,但是还没有被提交。
  • (d)中S1再次崩溃,S5通过S2/S3/S4选票能够再次选举成功。为什么S2/S3会投票给S5呢?因为他们的日志号相同,但是S5的任期号更大,投票符合前面说的规则。这里的问题是日志2已经被复制到了大多数节点,但依旧被覆盖了,是否存在数据安全问题?这里需要引入leader宕机处理:新leader是否提交之前任期内的日志条目。 

      4-1-2、leader宕机处理:新leader是否提交之前任期内的日志条目? 

      前面提到,一旦当前任期内的某个日志条目已经存储到了过半的服务器节点上,leader就知道该日志条目可以被提交了。在这里我们需要补充说明一下raft中具体的提交过程。  

      待续....

        

                 

               

        

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
raft共识算法是一种分布式一致性算法,用于解决分布式系统中节点之间达成一致性的问题。它主要包含了Leader选举、日志复制和安全性等基本机制。 在raft算法中,节点分为Leader、Follower和Candidate三种状态。初始状态下所有节点都是Follower,然后它们通过相互通信进行Leader选举。选出的Leader负责接收客户端请求并进行日志复制等操作。如果Leader出现故障或无法通信,那么其他节点会重新进行选举,选出新的Leader。 日志复制是raft算法的关键过程,Leader负责将客户端请求记录在日志中,然后将日志复制给所有的Follower节点。Follower节点在接收到Leader的日志之后进行存储,然后发送应答给Leader确认。只有当大多数节点都复制了同一条日志之后,这条日志才算是已提交的。 raft算法还通过逻辑时钟和心跳机制来保证系统的一致性。每个节点都有自己的逻辑时钟,用于识别事件的顺序。Leader节点会定期发送心跳信号给Follower节点,以确保它们的存活状态。 在raft算法中,安全性是非常重要的一部分。它通过限制节点之间的信息交换,避免了“脑裂”等问题的发生。同时,每个节点都有持久性的存储,当节点宕机之后可以通过快照恢复。 总的来说,raft共识算法通过Leader选举、日志复制和安全性等机制,实现了分布式系统中节点之间的一致性。它比Paxos算法更容易理解和实现,因此在实际应用中被广泛使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值