Raft协议解读

962 阅读6分钟

什么是Raft?Raft解决了什么?

Raft协议是一种一致性算法,用于在分布式系统中保持整个系统的一致性

比如,我们现在拥有一个五台机器组成的kv存储集群,想要让系统保持高可用性,需要让整体达成一致性。当一台机器宕机,其他机器依然能够保持之前的状态来对外提供服务。

相比于Raft,还有PaxosZab等一致性协议。在这里我们主要来看一下Raft协议

有很多著名的开源项目都使用了Raft协议,比如:

  • etcd
  • TiKV / TiDB
  • Redis

Raft思想

Raft协议使整个集群组成了一个状态复制机,一个主节点来对客户端提供服务,服务只能通过命令来对状态进行改变。每执行一个命令,主节点便将执行命令的日志复制给从节点,通过复制日志,如果从节点和主节点的日志是一样的,那么它们的状态就是一样的了。

Raft将一致性问题分解为三个子问题:

  • leader选举

    在集群中选举出主节点对外提供服务

  • 日志复制

    通过日志复制,让从节点和主节点的状态保持一致

  • 安全措施 ( safety )

    通过一些措施,保证服务出现问题依旧可用

leader选举

Raft协议leader选举中,节点会处于三种状态之一:

  • Follower

    从节点,给其他节点提供选票

  • Candidate

    候选人,如果一个Follower在一定时间后没有收到Leader的心跳包,则判断Leader故障,将自己的状态转换为Candidate,开始新一期选举流程。

  • Leader

    主节点,如果Candidate选举成功,则转换为Leader,开始对外提供服务。

一个选举流程如下:

首先我们假设当前term=1,我们拥有一个五个节点的集群。

  1. Follower在一段时间后没有收到Leader的心跳包,则转变为Candidate,该节点的term值加 1 ,然后给自己投一票,向其它所有节点发送选举请求。该选举请求是一个 RPC 调用:RequestVoteRPC

    一般这段等待时间为150ms ~ 300ms,该RPC调用会包含Candidate的当前term值

  2. 从节点收到选举请求,返回投票或者拒绝投票。

    投票遵从先到先得原则Follower判断请求的term值是否比自己的大,如果大,则投选举成功,否则拒绝投票。

    假设当前有两个Candidate节点 A 和 B 同时向 C 发出选举请求,C 只会对最先到的选举请求投选举票。

  3. 当一个节点获得最多的票数后,该节点成为Leader,开始对外提供服务。其他Candidate自动转为Follower

    如果多个节点同时拥有最多的票数,比如 A 和 B 两个节点的票数都是 2 ,这种时候会等待一个超时时间 ( 150ms ~ 300ms ) 后,重新开始选举流程。

    这种情况下会让服务更长时间不能提供服务,所以Raft协议的超时值是一个随机的区间时间,来避免这种情况出现。

日志复制

Leader选举出来后,集群开始提供服务,Leader将命令包装为一条条的日志,将日志复制给其他节点,来让集群中每个节点执行的命令保持一致,保持了状态的一致。

一个完整的客户端请求流程如下:

  1. 客户端向Leader发送一条Set key 49命令

  2. Leader收到请求,将Set key 49命令包装为日志,通过 RPC 请求AppendEntriesRPC发送给其他所有节点

    该 RPC 请求包含Leader上次处理的命令的日志编号,记为preLogIndexFollower会校验该编号。

  3. Follower收到请求,写入这条日志,返回Ack

    如果请求中的preLogIndex和本机的preLogIndex不一致,会拒绝执行该条命令。

  4. Leader收到大多数节点的Ack后,就**提交这条命令 ( commit ) **,对客户端返回Ack。然后通知所有Follower提交这一条命令 ( commit )

在上述流程下,每条命令具有两个状态:

  • 提交 ( Committed )
  • 未提交 ( Uncommitted )

只有提交的数据才会执行,应用到状态机。

在日志复制的过程中,Raft协议遵循以下原则:

  • Leader Append-Only

    主节点只添加新的日志,不删除,修改日志。

  • Log Matching

    如果两个节点日志条目的term和index相同, 那么当前它们之前的日志是相同的,当前的状态也是相同的。

安全措施(Safety)

我们假设现在拥有五个节点的集群,集群可能会发生以下多种意外情况:

日志不一致

LeaderFollower发送AppendEntriesRPC时,如果Follower发现Leader的前一条日志index和自己不一致,Leader会将自己的已提交日志复制到Follower中。

请求到达集群前,Leader挂掉

在请求到达集群前Leader挂掉,集群会重新选举出一个Leader来提供服务。

请求到达Leader,但未复制到Follower

此时请求的命令处于**未提交 ( uncommitted ) **状态,集群重新进行选举,客户端不会收到 Ack,重新进行请求即可。

请求到达Leader,复制到所有Follower后,未提交,挂掉

此时Follower数据处于**未提交 ( uncommitted ) **状态,重新选举后提交数据。客户端在这种情况下不会收到 Ack,并重新进行请求。Raft协议需要保持请求的幂等,这次请求进行去重,返回Ack。

请求到达Leader,复制到部分Follower后,未提交,挂掉

解决方案同上,在选举时拥有最新数据的Follower会成为Candidate,然后将日志复制给其他Follower,保证了这种情况的一致性。

请求到达Leader,提交后,未通知Follower,挂掉

解决方案同上。

请求到达Leader,提交成功,未响应Ack,挂掉

此时服务是保持一致的,重新选举出一个Leader即可。

Follower挂掉

少数Follower挂掉对集群没有影响,重新连线后,Leader将日志复制到Follower即可。

脑裂

脑裂指在网络出现分割的情况下,集群会分为两个小集群,这种情况下会产生两个Leader

脑裂的情况下,两个小集群中,一定只有一个可以提供服务。

假设当前集群有N个节点,N为奇数,发生脑裂后,最多只有一个集群的Follower数量大于等于N/2,只有该集群可提供服务。

发生脑裂的情况下,客户端请求到了少数集群,不会收到 Ack,再次尝试请求,此时请求了多数集群,会收到 Ack。当网络恢复后,少数集群会自动成为Follower

参考

这篇文章还是具有很多不足,可以参考以下来理解Raft协议。

raft 论文下载

可以下载英文原文论文理解

Raft 为什么是更易理解的分布式一致性算法

通过图片生动的展示了Raft协议中的请求流程与安全措施

raft 动画演示

需要梯子,通过动画展示了Raft协议。强推。