一、Raft协议基本流程
1、示例动画
先来看下Raft协议到底是个什么样子的。网上有个动画文稿,是对Raft算法最生动形象的描述。地址: http://thesecretlivesofdata.com/raft/
从这个动画中可以看到, Raft协议是分为两个阶段工作的:Election选举和Log Replicaion日志同步。也就是说Raft要解决的其实是两个事情,一个是集群中选举产生主节点。另一个是在集群内进行数据同步。
2、工作流程
Raft算法的基本工作流程是这样的:
这里重点是需要理解Log日志和State Machine状态机。Log日志就是保存在Server上的操作日志,其中每个条目称为Entry。Entry中的操作,最终都会落地到StateMachine中。Raft算法的核心就是要保证所有节点上的Entry顺序一致。
注意:是保证Entry顺序一致,而不是保证Entry不丢失,这是两个概念。
1、多个Server基于他的一致性协议,会共同选举产生一个 Leader,负责响应客户端的请求。
2、Leader 通过一致性协议,将客户端的指令转发到集群所有节点上。
3、每个节点将客户端的指令以 Entry 的形式保存到自己的 Log 日志当中。此时 Entry 是uncommited状态。
4、当有多数节点共同保存了 Entry 后,就可以将 Entry 中的客户端执行,提交到State Machine 状态机中。此时 Entry 更新为commited状态。
3、节点的三种状态
为此,Raft协议给每个节点设定了三种不同的状态,Leader,Follower和Candidate。
- Leader :
1、选举产⽣。多数派决定。
2、向 Follower 节点发送⼼跳,Follower 收到⼼跳就不会竞选Leader。
3、响应客户端请求。集群内所有的数据变化都从 Leader 开始。
4、向 Follower 同步操作⽇志。 具体实现时,有的产品会让发到 Follower 上的请求转发到 Leader 上去。 也有的直接拒绝
- Follower:
1、参与选举投票。
2、同步 Leader 上的数据。
3、接收 Follower 的⼼跳。如果 Follower ⻓期没有发送⼼跳,就转为 Candidate,竞选 Leader。
- Candidate:
没有 Leader 时,发起投票,竞选 Leader。
4、节点状态变化
Raft协议为了保证同一时刻,集群当中最多只会有一个主节点,防止脑裂问题,还会增加一个Term任期的概念。
时间被划分为多个任期。每个任期都以选举开始。选举成功后,由一名Leader管理集群,直到任期结束。有些选举失败了,没有选举出Leader,那就进入下一个任期,开始下一次选举。
从CAP理论的角度分析,Raft优先保证的是CP,而放弃了A。与之形成对比的是Eureka,保证AP。
他们的状态变化过程是这样的:
1、所有节点启动时都从 Follower 状态开始。
2、每个Follower 设定了⼀个选举过期时间Election Timeout 。Follower持续等待 Leader 的⼼跳请求。如果超过选举过期时间,就转为 Candidate,向其他节点发起投票,竞选 Leader。为了防⽌所有节点在同⼀时间过期,这个选举过期时间通常会设定为⼀个随机值,⼀般在 150ms到 300ms之间。
3、Candidate 开始新⼀个任期的选举。每个 Candidate 会投⾃⼰⼀票,然后向其他节点发起投票 RPC 请求。然后等待其他节点返回投票结果。等待时⻓也是Election Timeout。
4、每个节点在每⼀个任期内有⼀次投票的资格。他们会响应 Candidate 的投票 RPC 请求。按照⼀定的规则进⾏投票。返回⽀持 或者 不⽀持。
5、Candidate 收到其他节点的投票 RPC 响应之后,会重置他的 Election Timeout,继续等待其他响应。⼀旦某⼀个 Candidate 接收到了超过集群⼀半节点的投票同意结果后,就会转为 Leader 节点。并开始向其他节点发送⼼跳 RPC 请求。确认⾃⼰的 Leader 地位。
6、其他节点接收到 Leader 的⼼跳后,就会乖乖的转为 Follower 状态。 Candidate 也会转为 Follower 。然后等待从 Leader 同步⽇志。直到 Leader 节点⼼跳超时或者服务宕机,再触发下⼀轮选举,进⼊下⼀个 Term任期。
二、Raft协议实现机制
接下来思考下Raft算法要如何实现这些流程呢?这里我们主要分析每个节点要保存哪些数据,然后RPC请求要传递哪些数据。
这里简单总结Raft算法的基础数据结构:
首先是数据:
所有节点都需要的信息:
- currentTerm: 服务器当前的任期
- votedFor:当前任期内投票给了谁。
- log[]:日志条目Entry。每个 Entry 要包含Command:客户端指令,term:任期,idx:Entry 的偏移量。
- commitIndex:标记为commited的 Entry 的索引。记录消息同步的进度。
- lastApplied:已执行完 Command的 Entry 索引。 记录往状态机提交的进度。lastApplied<=commitIndex。 这两个主要是提交到状态机需要
Leader 上的特有参数:
- nextIndex[] : 给每个 Follower 同步到了哪一条 Entry。记录与follower 的同步进度。
- matchIndex[]:给每个 Follower 已经复制到了哪一条 Entry。主要是要记录有哪些 Entry 发给 Follower,正在等待 Follower 确认中。
然后看RPC 请求,最为核心的有三个。第一个是 Candidate投票的 RPC 请求。第二个是 Leader 发送的心跳请求。第三个是Leader 发送的日志同步请求。
对于投票请求,主要请求参数
- term : 当前任期
- candidateId: 投票的候选人 ID。
- lastLogIndex:候选人的最后日志Entry 索引。
- last logo term:候选人最后日志条目的任期号。
前两个参数是必须的。后两个参数主要是当主从发生切换时,可以用来找出最新的Candidate。
主要响应参数:
- term : 当前任期号
- voteGranted:投票结果。是否支持当前 Candidate 当选为 Leader。
**对于后两个请求,都是有 Leader 往 Follower 发送。**其实可以合并为一种请求。心跳请求不带日志条目,而同步日志请求带日志条目。Follower 只要判断下有没有日志条目就可以区分是哪种请求。
主要请求参数:
- term:当前领导者的任期
- leaderId:当前领导者的 ID。
- entries[]:要同步的日志条目。心跳请求就传空。同步消息请求则可以支持批量同步。
- leaderCommit:领导者已知已提交的最高的日志条目的索引。主要是 Follower 要知道新的条目是要从哪里开始同步。
- 为了安全起见,论文中还建议将上一条 Entry 的Index 以及 Term 发送过来。主要还是用来协助 Follower 定位 Entry
主要响应参数:
- term 当前任期。
- success: 响应是否成功。