解读Raft算法一:Raft算法简介与一致性实现原理
1 什么是Raft算法
Raft算法官方演示: https://raft.github.io/raftscope/index.html
Raft算法是一种分布式共识算法,目的是为了保持集群的一致性。比如在分布式储存领域,一份文件会被分为多个分片(shard)存储在不同的服务器上,以实现扩容的目的,但是这样会造成文件产生损坏的概率更高(比如某个服务器坏掉),因此通常会在多台服务器上保存同一个shard的副本,同时用Raft这种共识算法来组建集群,以保证每台服务器上的一致性。这样在某个服务器损坏时,集群中其他备份节点就可以升级为主节点对外部提供服务,提升了系统的可靠性。
相比于更早的分布式共识算法Paxos,Raft算法更容易理解,具有以下明显的优势:
- 问题分解:将问题分为领导者选举(Leader Election)、日志复制(Log Replication)和安全性(Safety)
- 状态简化:相比于Paxos做出了很多限制,减少了很多状态转换和边界条件。
- 任何时刻,节点都处于Leader,Candidate,Follower三种状态之一,Raft算法只需要关注几种状态之间的转化即可。
- Raft将时间分割为连续的任期(term),每个任期最多只有一个Leader,也可能没有Leader(多个Candidate在投票阶段获取到了相同的票数)
- 各服务器节点之间只使用RPC进行通讯,分为请求投票(RequestVote)和追加条目(AppendEntries)两种。其中RequestVote由Candidate在请求投票阶段发起,AppendEntries由Leader在复制日志和心跳时发送。
- 服务器之间通讯会交换任期号
- 如果一个服务器发现任期号滞后会自动更新任期号
- 如果一个Candidate或Leader发现任期号滞后,会自动切换为Follwer状态
- 如果一个服务器接收到任期号滞后的Leader发送的日志,会选择拒绝
2 问题分解
2.1 领导者选举
集群中的每个节点都拥有自己的超时机制,可以理解为倒计时,Leader会周期性的向Follower发送心跳(一种AppendEntries Rpc)来告诉集群中的所有节点自己的存在,同时重启Follower中的倒计时时间。
一旦Follower节点在倒计时完毕仍然没有收到Leader的信息,那么它会首先更新自己的任期号,同时自动更新自己的身份为Candidate,并发起Leader选取投票,并行地将请求发送给集群中的每一个节点,同时投给自己选票。其他节点投票时是按照先来先得的原则进行投票的,同时投票时会遵循安全性原则。
RequestVote请求和RequestVote相应结构体分别为:
typedef struct {
int term; // candidate当前任期号,用于判断是否能被接受
int candidate; // 本节点id
int lastLogIndex; // 最后一个日志号,安全性保证
int lastLogTerm; // 最后一个日志的任期号,安全性保证
} RequestVoteRequest;
typedef struct {
int term; //当前任期号
bool voteGranted; // 是否投给请求的candidate
} RequestVoteResponse;
一轮选举结束后会有以下几种情况:
- 该节点获得了半数以上选票,成为Leader并开始发送心跳
- 其他节点成为Leader,收到的新Leader任期号心跳,如果宣称为Leader的节点任期号不小于当前任期号,那么节点自动变为Follower;如果任期号更小,那么请求会被拒绝
- 没有任何获胜者,等待下一轮超时后重新进行选举(随机选取时间为150-300ms)
2.2 日志复制
集群在Leader选举产生后开始对外提供服务,而客户端得知Leader的方式有三种:
- 客户端请求对象刚好是Leader
- 客户端请求对象是Follower,Follower会回复给客户端Leader的ID信息
- 客户端请求的对象刚好宕机,则隔一段时间后会自动请求下一个
日志中含有的主要信息有3项:复制状态机指令,日志号(日志索引)和Leader的任期号
其中日志号和任期号一起才能唯一确定一条日志,这是为了防止一些宕机情况的发生。日志的发送和响应RPC格式:
typedef struct {
int term; // Leader当前任期号
int leaderId; // Leader的id
int prevLogIndex; // 上一条日志的索引(一致性校验)
int prevLogTerm; // 上一条日志的任期(一致性校验)
byte entries[]; // 日志内容
int leaderCommitId; // Leader已提交的日志ID(Follwer日志小于该ID,可以全部提交)
} AppendEntriesRequest;
typedef struct {
int term; // Follower当前任期号(只有小于等于Leader才会接受该RPC)
bool success; // 如果Follower包含前一个日志,返回true
} AppendEntriesResponse;
日志的两个重要概念分别是复制和提交。复制是指Leader接收请求以及Follower接收到AppendEntriesRPC之后,将该日志加入到自己的Log队列的过程;提交是指节点将日志队列中的日志应用到自己的复制状态机的过程,提交后的复制状态机状态对外部才是可见的。日志的复制和提交过程如下:
- Leader接收到请求,首先在自己Log队列中复制一份日志
- Leader向集群所有节点发送AppendEntriesRPC,通知所有节点复制一份同样的日志
- Follower收到AppendEntriesRPC后,会在自己的Log队列复制一份日志
- Follower将复制结果返回RPC给Leader节点
- Leader节点在确认收到半数以上成功的结果后,会提交该日志给本地复制状态机
- Leader发送提交指令,通知所有Follower提交自己的日志
集群在运行过程中,会产生节点崩溃的现象,这样就会导致日志不一致的情况,此时会有多种机制保证日志的一致性:
- Follower节点:如果Follower节点没有回复某个AppendEntriesRPC,那么Leader会持续重试发送该RPC,直至收到响应(不管Leader是否已经回复过客户端);如果某个Follower是崩溃后恢复,那么该Follower可能会已经落后很多日志,一致性校验原则就是为了保证这种情况下不出错的,它是指Leader节点的AppendEntriesRPC中会带有自己的Log队列中上一条日志的索引号和任期号,Follower节点会检查这两项,如果不一致会拒绝该日志,Leader在收到拒绝信息后回尝试发送上一条日志,直到发送到第一条可以被接受的日志为止。
- Leader节点:如果Leader节点崩溃,那么它恢复后,可能已经产生了新的Leader。此时,可能会出现日志不一致的问题,即Leader可能已经将日志复制到了一部分Follower节点,但是新选举出的Leader可能并没有这些日志(Leader选举过程中是不考虑未提交日志的!),这种情况下Leader会通过强制复制的方式覆盖这些冲突日志来保证一致性,同时由于冲突日志还未提交,所以不会影响集群对外一致性。
2.3 安全性
Raft算法的边界情况较少,但是在出现一些异常状况的时候难免需要处理一些边界情况,而安全性原则就是为了避免这些边界情况造成的问题的。
2.3.1 Leader宕机
-
选举规则的限制
Leader宕机后会重新选取新Leader。在投票时,Candidate在发送RequestVoteRequest时会带有自己Log队列中最后一个Log中的索引号和任期号,如果其他Follower发现RequestVoteRequest中的Log索引号和任期号比自己的更加
up-to-date
时,才会投票给该Candidate,否则会拒绝。up-to-date
定义很明确:首先根据任期号进行判断,任期号更高的更新;如果任期号一致,那么索引号更大的更新。 -
新Leader是否提交已复制日志
首先明确一下日志的提交规则:Leader收到一个消息后,会通过AppendEntriesRPC向所有的节点发送,Follower收到这个RPC后会把日志首先复制到自己的Log队列中,并返回一个复制是否成功的RPC消息给Leader,而不是直接提交;当Leader确认收到半数以上的复制成功RPC后,便会在本节点提交该日志;Leader在下一次发送AppendEntriesRPC时(心跳或者另外一条消息的请求),会将AppendEntriesRPC中的已提交日志号进行更新,Follower在收到后可以发觉Leader已经提交了上一条日志,那么它们也将会在本节点提交上一个日志。
如果Leader在复制完一条日志A后,它发生宕机,下一次选举中,已经复制过A的Follower当选为Leader,那么它会发现Log队列中存在一条没有提交的日志A,那么它将会持续尝试将日志A复制到大多数节点中。需要注意的是,即使该新任Leader已经将日志A复制到大多数节点,也不会通过心跳产生的AppendEntriesRPC让Follower来提交日志,因为根据领导者选举规则和一致性原则,使用心跳提交RPC可能会产生已提交日志被覆盖的情况,这种情况是不能容许的!。而是只能等到下一次新的请求到达之后,新消息的AppendEntriesRPC对应的日志提交后,才会提交日志A,因为这样相当于对日志A覆盖了一层保护日志,后续选举的新Leader不能越过保护日志覆盖日志A,保证了安全性。
2.3.2 Candidate与Follower宕机处理
Candidate或Follower宕机后,后续发送给它们的AppendEntriesRPC和RequestVoteRPC都会失效,如果这两种RPC发送后没有收到回复,发送者会一直持续尝试重试发送RPC直至接收到响应消息为止。
2.3.3 时间与可用性限制
Raft算法不依赖客观的时间,而是依赖于内部的通讯与同步。主要满足通讯时间(0.5~20ms)<<选举超时时间(10-500ms)<<故障发生时间(几个月甚至更长)