Raft 算法之日志复制

原文地址: https://qeesung.github.io/202...
Raft 论文地址:https://ramcloud.atlassian.ne...html

Raft论文中分为三块:git

  • 领导选举
  • 日志复制
  • 安全性

本文中主要介绍日志复制github

领导人必须从客户端接收日志而后复制到集群中的其余节点,而且强制要求其余节点的日志保持和本身相同。数组

鉴于日志复制这一块比较复杂,能够结合下面两个网页来理解:安全

复制状态机

复制状态机一般都是基于复制日志实现的,每个服务器存储一个包含一系列指令的日志,而且按照日志的顺序进行执行。以下图所示:服务器

  1. 客户端请求服务器,请求的信息就是一系列的指明,好比PUT KEY VALUE
  2. 服务器在收到请求之后,将操做指令同步到全部的服务器中
  3. 服务器收到同步的指令之后,就将指令应用到状态机中
  4. 最后响应客户端操做成功

raft-state-machine.png

状态机是按照同步到服务器上的指令的顺序,一个一个的去Apply指令,因此指令的顺序很重要,若是指令Apply的顺序不一致,或者丢失部分指令,那么最终状态机的状态也会不一致。网络

而咱们知道网络是不稳定的,好比延迟,分区,丢包,荣誉和乱序等错误。若是不保证状态机Apply指令彻底如出一辙,那么将会致使不一致的结果。而Raft的日志复制机制则能保证在发生上述网络问题的时候,全部的服务器都能同步到彻底如出一辙的日志,也就是说能保证每个服务器的状态机Apply到彻底如出一辙的指令。并发

附加日志RPC

请求体结构

全部服务器上都维持的状态:优化

  • commitIndex:最大的已经被提交的日志索引
  • lastApplied:最后被应用到状态的日志条目索引

lastApplied应该小于等于commitIndex,由于只有在日志被提交之后才能被Apply到状态机中spa

领导人常常改变的:

  • nextIndex[]:对于每个服务器,须要发送给他的下一个日志条目的索引值,初始化为当前领导人的最后的日志索引值加一
  • matchIndex[]:对于每个服务器,已经复制给他的日志的最高索引值

nextIndex[]matchIndex[]的长度等于整个集群中的服务器数量。

附加日志RPC的请求结构:

  • term:附加日志的领导人任期号
  • leaderId:当前领导人的Id
  • preLogIndex:当前要附加的日志entries的上一条的日志索引
  • preLogTerm:当前要附加的日志entries的上一条的日志任期号
  • entries[]:须要附加的日志条目(心跳时为空)
  • leaderCommit:当前领导人已经提交的最大的日志索引值

preLogIndexpreLogTerm主要是用于跟随者检测当前领导人要附加的日志是否和跟随者当前的日志匹配,若是不匹配的话,那么就须要继续向前搜寻和领导人匹配的日志(下面章节会介绍)

leaderCommit的话用于告诉跟随者当前提交到什么位置了(由于收到附加日志还不能立刻提交,不然可能存在日志丢失的状况),以便跟随者将已经提交的日志Apply到状态机中

附加日志RPC的响应结构:

  • term:跟随者的当前的任务号
  • success:跟随者是否接收了当前的日志,在preLogIndexpreLogTerm匹配的状况下为true,不然返回false

请求的流程

每个日志条目存储一条状态机指令和从领导人收到这条指令时的任期号。日志中的任期号用来检查是否出现不一致的状况,每一条日志条目同时也都有一个整数索引值来代表它在日志中的位置。

raft-commit-entries.png

领导人附加日志

一旦成为领导人,那么领导人就会在固定在必定时间内发送空的附加日志,也就是心跳,以组织更随着超时。

若是领导人收到来本身客户端的请求,那么首先将请求的指令附加到本身的日志队列中,而后领导人会将新附加的日志条目经过附加日志的RPC发送到全部的跟随者。

领导人须要附加那些日志?

领导人中维护了两个常常变更的属性nextIndex[]matchIndex[],用于记录要发什么日志和跟随者收到了什么日志。

其中nextIndex[]记录了须要发送给每个服务器的日志的下一个索引值,这个数组会在领导人被选举出来的时候初始化为领导人最后的索引加一。注意这个nextIndex[]只是记录了要发给下一次附加日志要发给服务器的索引值,这个索引值可能并不必定准确,好比在跟随者和领导人的日志不一致的状况下,nextIndex[]的值就须要进行递减以找到和跟随者最大的匹配的日志,具体流程后文中会解释。

matchIndex[]主要是用来记录跟随者收到了那些日志,以方便领导人确认是否当前的日志能够进行提交,由于必需要至少N/2+1的集群成员(包括领导人本身)都确认收到了日志才能够进行的日志的提交。

领导人收到收到跟随者附加日志的响应该如何处理?

咱们知道领导人在附加日志的时候是并发的向全部跟随者发起的,而且是在固定周期内定时发送的,因此可能在上一个RPC没有收到响应的状况下,发出下一个RPC请求,或者是收到过时的日志追加请求等等。

  1. 比对响应的Term和当前的Term以确认本身是否过时:

    • 若是响应的Term大于当前的Term,那么说明当前的领导人已通过期,立刻将本身切换为跟随者
    • 若是响应的Term小于当前的Term,那么说明当前的收到了过时的响应(可能网路延迟致使),那么忽略
    • 不然进行步骤2
  2. 判断附加RPC时的preLogIndex和当前的nextIndex[peer]-1 是否相等,用于判断是不是过时的响应,或者是nextIndex[peer]是否被更改了:

    • 若是不相等,那么直接忽略
    • 不然进行步骤3
  3. 判断响应的success是否为真:

    • 若是为真:那么说明附加日志成功,进行步骤4
    • 若是为假:那么说明附加日志失败,preLogIndexpreLogTerm和跟随者的日志不匹配,进行步骤5
  4. 附加日志成功之后,须要更新matchIndex[peer]中的值为已经追加日志的索引值,表示追加日志成功,判断整个matchIndex[]数组中的是否有一半都大于等于追加日志的索引:

    • 若是是:那么更新commitIndex,进行日志的提交
    • 不然跳过处理
  5. 日志不匹配,那么须要找到下一个和跟随者匹配的日志索引,简单一点能够经过递减nextIndex[peer]来实现。

跟随者接收日志

跟随者收到附加日志的请求,不能简单的将日志追加到本身的日志后面,由于跟随者的日志可能和领导人有冲突,或者跟随者缺失更多的日志,入下图所示。

raft-conflict-log.png

那么必定要确保本次附加日志的以前的全部日志都相同,也就是说附加当前的日志以前,缺日志就要把缺失的日志补上,日志冲突了,就要把冲突的日志覆盖(领导人能够强行覆盖跟随者的日志)

  1. 判断附加日志任期Term和当前的Term是否相同:

    • 若是请求的Term小于当前的Term,那么说明收到了来自过时的领导人的附加日志请求,那么拒接处理。
    • 若是请求的Term大于当前的Term,那么更新当前的Term为请求的Term,进行步骤2
    • 若是请求的Term和当前的Term相等,那么说明请求合法,进行步骤2
  2. 判断preLogIndex是否大于当前的日志长度或者preLogIndex位置处的任期是否和preLogTerm相等以检测要附加的日志以前的日志是否匹配:

    • 若是preLogIndex的长度大于当前的日志的长度,那么说明跟随者缺失日志,那么拒绝附加日志,返回false
    • 若是preLogIndex处的任期和preLogTerm不相等,那么说明日志有冲突,拒绝附加日志,返回false
    • 不然说明以前的日志全都匹配,那么进行步骤3,检测preLogIndex以后的日志是否匹配。
  3. 逐一比对要附加的日志entries[]是否和本身preLogIndex以后的日志是否有冲突:

    • 若是entries[]中的任何一位置的日志发生冲突,那么须要将以后的日志进行截断,并追加为entries[]中以后的日志
    • 若是没有发生冲突,那么存在两种状况:

      • 跟随者的日志和entries[]的日志所有匹配,这种状况多是重复的附加日志RPC,那么这种状况只会简单的校验一遍全部日志
      • entries[]的日志匹配当前的全部日志,那么将没有匹配的日志全都追加到当前的日志后面。

上述的步骤3是很重要的,若是不对现有的日志进行比对,并且简单的进行截断追加日志,那么是很危险,由于可能收到延时的重复日志附加请求而致使日志没必要要的截断,从而致使已经提交的日志丢失。

另外还有一个优化点,若是存在大量的冲突日志,那么若是经过递减nextIndex[peer]将会很慢,因此能够经过批量跳过冲突日志方式来作到,能够再响应中添加conflictIndexconflictTerm来作到,这里不展开详细讨论。

日志的提交和应用

全部的服务器都有两个常常变更的值commitIndexlastAppliedcommitIndex表示已经提交的日志索引,而lastApplied表示最后Apply到状态机中的日志索引。commtiIndex会在日志成功附加到集群中的N/2+1的节点上更新。

通常应用日志到状态机中是经过一个独立的线程来作到的,经过监控是否有新提交的日志,若是有新提交的日志,那么就将日志Apply到状态机中,而且更新lastApplied。因此通常commitIndex >= lastApplied

在实际的应用中,通常将Raft单独做为独立的一层共识模块,上层模块将须要达到共识的指令下发给Raft共识模块,在Raft模块达到共识之后,就将达成共识的指令Apply到上层模块中。好比EtcdTiKV等等

相关文章
相关标签/搜索