Raft算法拆解

Raft的前世今生

  • 因为Paxos优秀而并不完美,Raft继承Paxos优点,改进缺点。
  • Paxos优点:实现了安全性(safety)与存活性(liveness), 且在正常的情况下, 效率不差。
  • Paxos缺点:难以理解和缺失了工程化的细节信息

Raft目标

  • 安全性: 算法正确性能得到证明, 确保多状态机一致性的达成
  • 存活性: 半数以上结点正常工作且相互能够通信的情况下, 整个系统能持续对外提供服务
  • 易懂性: 聪明的你和我都能快速理解 Raft 协议的核心原理与正确性
  • 易实现: 充分描述算法的实现细节, 让心思简单的程序猿也能对着算法内容,按部就班地实现一个可用的系统。

Raft缺点

  • raft的目标即是raft优点
  • raft缺点总的来说就是效率不高,而某些Paxos的改进算法可以多读多写,乱序执行效率提升。
    1)无法乱序commit;
    2)无法乱序execute;
    3)无法多写
    4)无法多读
  • 因此某些中间件不使用raft而是改进的multi-paxos算法

思路

抽象问题:多结点数据一致性的问题

  • 多结点数据一致性的问题可以被抽象简化为多状态机副本一致性问题。
  • 客户端可以向多份状态机组成的系统发送指令
    • 客户端具体向哪个状态机发送指令, 向几个状态机发送指令, 都可以由共识协议算法来规定
  • 共识协议算法需要保证每份状态机最终执行的一样的客户端指令, 且执行顺序一致。
  • 因为如果每条指令引发的状态机变化的结果是确定唯一的, 只要每台状态机都按相同顺序执行了相同的指令, 那必然会达到相同的状态, 由此解决了一致性问题。
  • 这种模型被称为复制状态机
  • 我们可以将需要保持内容与顺序一致的指令集简称为指令日志( command log ) 或直接简称为日志( log)

怎样‘简单的’处理这个问题

  • 设法让整个集群拥有一个主结点, 客户端只向集群中的主结点发送指令
    • 如果客户端将指令发送给了集群中的非主结点, 非主结点都会直接返回主结点的地址, 让客户端与主结点建立连接
    • 或者进行从节点进行转发处理
  • 由主结点进行所有指令的接收和分发, 这样非常简单地就可以确保集群中的每个状态机都收到一份相同的指令log

拆分

  • 上述方案建立在“主节点运行正常”的前提下,我们需要进一步细化明确细节。主要是:日志复制逻辑
    • 日志复制大体流程
      • 主节点即leader在接受客户端请求后,将日志持久化到本地,并发送日志到所有从节点
      • 从节点接受日志,持久化到本地并返回成功
      • 当主节点收到多数从节点的成功返回后,即认为日志已提交
      • 主节点将日志应用到状态机,称为日志已提交,将结果返回客户端
      • 虽然操作结果已返回客户端,主节点仍旧会同步和提交日志到所有节点,直到所有节点都成功
    • raft是强领导的模式,主节点全权负责管理复制日志来实现一致性
    • 考虑到节点宕机,日志和其他数据的持久化是必然的方式
  • 并且如何选举主节点,主节点异常时如何处理我们也需要进一步讨论。主要是:选主逻辑
    • 什么情况下选主,没有主的时候选主。
      • 集群初始化时:都是fallower
      • 主节点挂掉。如何判断主节点挂掉,心跳机制,主节点向所有节点心跳广播
    • 选主,多票胜出。
      • 新的主节点的承诺
        • 需要保证不漏掉已提交的日志,这里已提交的日志指的是已同步到多数节点的日志,而不是前一个主节点已提交的日志
        • 未提交的日志可以继续提交或者丢弃。只同步到少数节点的日志,有可能这些节点都宕机,而不能继续提交
      • 因此,选出的主一定是有最新已提交日志,这样只需要将新主日志直接同步到所有的follower
      • 不太严谨的推理描述
        • 主节点提交一个必须是多数(大于1/2)节点提交。
        • 选主,必须得到多数(大于1/2)节点同意,如果选出了新主,那么参与投票的节点中至少有一个节点是有最新已提交的数据。
        • 又因为选出的主一定拥有最多(最新)记录,所以必然包含最新已提交的记录。
  • 非主干问题
    • raft集群大小
    • 日志复制的效率的问题
    • 支持集群动态配置

概念

Raft 集群中结点的角色

  • leader(主结点):leader 负责接收、处理、响应所有来自客户端的请求
  • follower (从结点):follower 自己不会发送任何请求, 只会响应来自 leader 和 candidate 的请求, 不会响应其他 follower 发来的请求, 如果 follower 收到了来自客户端的请求, follower 就将主结点的信息返回客户端, 让客户端去联系主结点。
  • candidate (主结点候选人)candidate 是选举过程中的结点角色, 当 leader 失效后, 会有follower 转变为 candidate , 下一个 leader 从candidate 中产生。

时间和任期(term)

在这里插入图片描述

  • term单调递增,它作为一个逻辑时钟,用于检测系统的过期信息,如过期的leader。每种rpc请求都会携带term,在响应请求前会检查term
    • 如果任期相同,则正常处理。
    • 请求任期大于自己的任期,则更新自己的任期,并退化为follower
    • 请求任期小于自己的任期,则拒绝请求
  • 因为是通过通信更新term,所以集群中的每台服务器所观察到的term切换可能是不一样的。

Raft 集群中结点的角色转化

  • 在任意时刻, 集群内的任意结点都只会是follower,candidate,leader三种角色的其中一种。
  • 集群刚刚启动时, 所有结点都是follower。
  • follower收不到主结点发来的心跳时,就会转变为 candidate, 发起leader 选举流程
  • candidate收集了集群中半数以上结点的选票后,就能变成leader
  • candidate的选举过程可以被其他更高任期的candidate或者Leader中断,退回到follower
  • leader持续运行直到自己宕机或者发现其他拥有更高任期leader或者candidate
    在这里插入图片描述

Raft 集群中的通信

  • 通信使用RPC同步调用的方式,引入超时重试和并发调用
  • 三个RPC接口
    • RequestVoteRPC:由竞选过程中的 candidates 发起调用, 用于收集选票
    • AppendEntriesRPC:由 leader 发起调用, 用于将指令 log 分发到各台服务器上或者作为心跳通知使用
    • 快照传输的RPC

日志

  • 日志是由一条一条存储 log[]
  • commitIndex:最后提交的日志的索引位置
  • lastAplied:最后接受的日志索引位置

设计

待翻译。。。
在这里插入图片描述
在这里插入图片描述

问题的解决

选主

  • 什么情况下选主?没有主节点才需要选主。怎样判断没有主节点?
    • 心跳机制,通过AppendEntriesRPC实现
      • 一台服务器只要能够收到来自 leader 或者candidate发来的 RPC 调用就会保持自己的 follower 身份。
        • 这条服务器并未限制角色,因为任期机制的原因。即使是leader或者candidate,如果任期小于leader的任期,则会转为follower。
  • 谁能发起选主?如果一个 follower 在一段时间后(election_timeout) 一直没有收到任何 RPC 调用请求,它就会认为当前集群没有有效的 leader , 发起一轮新的选举。
    • 执行 current_term = current_term + 1
    • follower将自己的身份转变为candidate
    • 给自己投一票, 并向集群中所有其他服务器发起 RequestVoteRPC并发调用。
  • 谁能响应选举:所有角色均可响应
    • 基础规则
      • term规则
        • 拒绝term比自己小的请求
        • 接受到的请求term比自己的大,那么会改变term,转为follower
      • 投票规则
        • 每个服务器在同一个term中,只能投一票
        • 幂等:如果给一个candidate投过票,则再次调用也会投票
        • candidate在向其他节点发起投票前,会给自己投一票
    • 通过基础规则推论,可知在节点收到投票请求时
      • 如果一个节点是follower
        • 请求的term比自己小,拒绝
        • 请求的term等于自己
          • 如果本term给这个candidate过投票,则投票
          • 如果没投过票
            • 日志大于等于自己,投票
            • 日志小于自己,不投票
          • 否则,即是本term给别的candidate过投票,则不投票
        • 请求的term比自己大,更新自己的term
          • 日志大于等于自己,投票
          • 日志小于自己,不投票
      • 如果一个节点是candidate或follower
        • 请求的term比自己小,拒绝
        • 请求的term等于自己,因为已经给自己投过票了,因此不投票
        • 请求的term比自己大,更新自己的term,转为follower。
          • 本term一定没投过票
            • 日志大于等于自己,投票
            • 日志小于自己,不投票
    • 怎样判断日志的新或者多,找到最后接受的日志索引,而不是提交的日志,
      • trem大的新
      • term相同看索引编号
  • candidate的选举结果
    • candidate赢得选举:收到过半投票
    • candidate输掉选举:收到过半投票前,收到了更高term请求,更新term,退化为 follower
    • candidate平票
      • 如果同时有多个candidate参与竞选,那每个候选者的票数可能都无法达到半数以上,因选票募集超时而开启一轮新的选举。
      • 为了减少平票问题的出现,采用了随机化election_timeout的策略,例如100-300ms。

日志复制

大体流程

  • 一旦 leader 被选出,它就可以开始接收客户端发来的请求。 每一个客户端请求都包含着一个需要被多个状态机副本执行的指令。
  • Leader 把这个指令作为一条日志记录(log entry) 追加到自己 command log末尾,再对其他所有服务器并行发起AppendEntriesRPC调用。
  • 当一条log entry被复制到多数状态机上后,leader 就可以执行这条日志中的指令,然后将指令的执行结果返回给客户端,并向再对其他所有服务器并行发起AppendEntriesRPC调用,使follower提交日志。
  • 如果 follower 因为宕机、运行缓慢、或者网络数据通信丢包等问题导致 leader 发出的 AppendEntriesRPC 调用没有得到响应,leader 会不限次数地一直进行重试直到所有的 follower 都存储了相同的 log 记录,即使已将执行结果返回给客户端。
    在这里插入图片描述

性质与证明

  1. 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们存储了相同的指令。
    • Leader 在特定的任期号内的一个日志索引处最多创建一个日志条目,同时日志条目在日志中的位置也从来不会改变。
  2. 如果不同日志中的两个条目拥有相同的索引和任期号,那么他们之前的所有日志条目也都相同。
    • 在发送 AppendEntries RPC 的时候,leader 会将前一个日志条目的索引位置和任期号包含在里面。
    • 如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。

选举限制

每个节点只给比自己新的节点投票,这样就可以选出最新的节点
这个最新的节点必然包含所有已提交的日志,也有可能有未提交的日志。这里的已提交和未提交指的是是否同步到集群中的多数节点

提交之前任期内的日志条目

怎样提交之前任期内的日志条目
  • raft不能通过直接复制前任任期的日志到从节点,因为会有已提交的日志被覆盖的问题。
  • 而是在复制本任期的日志时一起复制前任任期的日志,注意是在一次AppendEntriesRPC中
  • 这个“本任期的日志”可以是客户端的真实请求,也可以是为了同步前任任期的日志而虚构的空操作日志
已提交的日志覆盖问题
  • 一旦当前任期内的某个日志条目已经存储到过半的服务器节点上,leader 就知道该日志条目已经被提交了
  • 如果某个 leader 在提交某个日志条目之前崩溃了,以后的 leader 会试图完成该日志条目的复制
  • 然而,如果是之前任期内的某个日志条目已经存储到过半的服务器节点上,leader在某些情况下无法断定该日志条目是否被提交
    在这里插入图片描述
  • 构造案例
    • 在 (a) 中
      • S1 是 leader ,部分地复制了索引位置 2 的日志条目。
    • 在 (b) 中
      • S1 崩溃了
      • S5 在任期 3 中通过 S3、S4 和自己的选票赢得选举,然后从客户端接收了一条不一样的日志条目放在了索引 2 处
  • 如果leader直接复制前任任期的日志,就会出现已提交的日志被覆盖的问题
    • 在 © 中
      • S5 又崩溃了
      • S1 重新启动,选举成功,继续复制日志。
        • 来自任期 2 的那条日志已经被复制到了集群中的大多数机器上
        • S1可以将任期 2 的那条日志应用到S1的状态机中
    • 在 (d) 中
      • S1又崩溃了
      • S5 可以重新被选举成功(通过来自 S2,S3 和 S4 的选票),
      • 然后覆盖了他们(S2,S3 和 S4)在索引 2 处的日志,但是索引2处的日志是已提交。
      • S1也能覆盖,因为现在是任期5,S1恢复后,发现自己的任期为4,小于S5的任期,会退化到follower,从而覆盖日志。
    • leader通过复制当前任任期的日志,从而将前任任期的日志一起同步到其他节点,就不会有这种问题
      • 如 (e) 中
        • (e)是(b)情况的后续
        • 在同步任期4中的日志时,一起同步任期2的日志,
        • 已同步到S2和S3中,然后这个条目就会被提交,之前的所有条目也会被提交
        • 此时,S5 就不可能选举成功,也就不会有日志覆盖的情况了

异常情况的讨论

主节点挂掉

待补充。。。

非主干问题

Raft 集群大小

  • 集群内的机器数量可以自由定义,但是通常为5。
  • 因为 5 台机器构成的 Raft 集群可以容忍2个结点的宕机, 这在一般的实践场景中已经足够,同一时刻出现3个机器的宕机概率很小,只要不出现三台机器同时失效的情况, 系统都可以正常对外提供服务。 如果是集群内某一台或者两台机器宕机, 我们都有充足的时间可以修复宕机机器。
  • 如果集群内的机器数量过大, 可能会降低整个系统对客户端的指令处理速度( 因为要复制更多数据,协调更多数据的一致性)。

什么情况下Raft集群可以稳定工作

  • 多数节点正常工作
  • raft可以选举并维持一个稳定的领导人:
    • 广播时间(broadcastTime) << 选举超时时间(electionTimeout) << 平均故障间隔时间(MTBF)
    • 广播时间大约是 0.5 毫秒到 20 毫秒,取决于存储的技术。
    • 选举超时时间可能需要在 10 毫秒到 500 毫秒之间。

集群成员变化

难点,待补充。。。

参考资料

  • 动画演示:
    • http://thesecretlivesofdata.com/raft/
    • https://raft.github.io/
  • raft论文翻译:https://www.cnblogs.com/linbingdong/p/6442673.html
  • https://zhuanlan.zhihu.com/p/344781101:raft论文+理解
  • https://zhuanlan.zhihu.com/p/36547283:raft实现细节
  • 异常情况:https://www.cnblogs.com/bonelee/p/6408318.html
  • 安全性证明:https://zhuanlan.zhihu.com/p/363888815
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值