什么是Raft?Raft解决了什么?
Raft协议是一种一致性算法,用于在分布式系统中保持整个系统的一致性。
比如,我们现在拥有一个五台机器组成的kv存储集群,想要让系统保持高可用性,需要让整体达成一致性。当一台机器宕机,其他机器依然能够保持之前的状态来对外提供服务。
相比于Raft,还有Paxos、Zab等一致性协议。在这里我们主要来看一下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,我们拥有一个五个节点的集群。
-
Follower在一段时间后没有收到Leader的心跳包,则转变为Candidate,该节点的term值加 1 ,然后给自己投一票,向其它所有节点发送选举请求。该选举请求是一个 RPC 调用:RequestVoteRPC一般这段等待时间为150ms ~ 300ms,该RPC调用会包含
Candidate的当前term值。 -
从节点收到选举请求,返回投票或者拒绝投票。
投票遵从先到先得原则。
Follower判断请求的term值是否比自己的大,如果大,则投选举成功,否则拒绝投票。假设当前有两个
Candidate节点 A 和 B 同时向 C 发出选举请求,C 只会对最先到的选举请求投选举票。 -
当一个节点获得最多的票数后,该节点成为
Leader,开始对外提供服务。其他Candidate自动转为Follower。如果多个节点同时拥有最多的票数,比如 A 和 B 两个节点的票数都是 2 ,这种时候会等待一个超时时间 ( 150ms ~ 300ms ) 后,重新开始选举流程。
这种情况下会让服务更长时间不能提供服务,所以
Raft协议的超时值是一个随机的区间时间,来避免这种情况出现。
日志复制
当Leader选举出来后,集群开始提供服务,Leader将命令包装为一条条的日志,将日志复制给其他节点,来让集群中每个节点执行的命令保持一致,保持了状态的一致。
一个完整的客户端请求流程如下:
-
客户端向
Leader发送一条Set key 49命令 -
Leader收到请求,将Set key 49命令包装为日志,通过 RPC 请求AppendEntriesRPC发送给其他所有节点该 RPC 请求包含
Leader上次处理的命令的日志编号,记为preLogIndex,Follower会校验该编号。 -
Follower收到请求,写入这条日志,返回Ack。如果请求中的
preLogIndex和本机的preLogIndex不一致,会拒绝执行该条命令。 -
当
Leader收到大多数节点的Ack后,就**提交这条命令 ( commit ) **,对客户端返回Ack。然后通知所有Follower提交这一条命令 ( commit )。
在上述流程下,每条命令具有两个状态:
- 提交 ( Committed )
- 未提交 ( Uncommitted )
只有提交的数据才会执行,应用到状态机。
在日志复制的过程中,Raft协议遵循以下原则:
-
Leader Append-Only
主节点只添加新的日志,不删除,修改日志。
-
Log Matching
如果两个节点日志条目的term和index相同, 那么当前它们之前的日志是相同的,当前的状态也是相同的。
安全措施(Safety)
我们假设现在拥有五个节点的集群,集群可能会发生以下多种意外情况:
日志不一致
当Leader向Follower发送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协议。强推。