Raft协议详解
1 Raft节点状态
Raft协议的每个节点都会处于三种状态之一:Leader、Follower、Candidate。
- Leader(领导者)
接受客户端请求,并向Follower同步请求日志,当大多数节点通知Leader日志同步成功,则Leader提交日志 - Follower(追随者)
接受并持久化来自Leader同步的日志,Leader通知可以提交则提交日志 - Candidate(候选者)
Leader选举过程中出现的临时角色,如果Follower没有收到Leader的心跳响应超过150~300ms,会进行Leader选举
状态转移图如下
2 Raft中的RPC
节点之间通过RPC进行沟通。Raft下总共有三种RPC,经常使用的有两种
- RequestVote RPC:在选举期间由Candidate节点发起
- AppendEntries RPC:在任期内由Leader发起,将日志项(log entries)发送给Follower用于同步,同时也用于Leader向Follower发送心跳包用于保证Leader合法性
- SnapShoot RPC:在传输节点快照(Snapshot)时使用
3 Raft一致性问题的拆解
Raft将一致性问题分解为3个独立的子问题
- Leader选举 Election
Leader 进程失效后能够自动选举出一个新的 Leader - 日志复制 Replication
Leader 保证其他节点的日志与其保持一致 - 状态安全 Safety
Leader 保证状态机执行指令的顺序与内容完全一致
4 Leader选举
4.1 选举条件
所有节点初始化状态为follower,当follower超时(election timeout)未收到心跳包(不带log的AppendEntriesRPC),则认为leader失效,需要(重新)选举 。
4.2 选举过程
- follower将自己维护的
current_term_id加1 - 状态改成candidate,
- 向其它server发起
RequestVote RPC请求(带上current_term_id) candidate保持在该状态直到该candidate成功成为leader
4.3 选举结果
被选成了leader
当candidate获得了大多数的选票 ,状态切换leader。并且定期给其它所有server发心跳包,告诉对方自己是current_term_id所标识的任期的leader
没有被选成leader
收到了任期 >= 本地任期current_term_id的心跳包,则将自己的state切成follower,并且更新本地的current_term_id
没有被出leader
如果同时有多个Candidate发出竞选,并且都没有获得大多数投票,没有leader被选出。这种情况下,每个candidate等待的投票的过程就超时了,接着candidates都会将本地的current_term_id再加1,发起新一轮竞选,直到选出leader。
4.4 投票原则
Raft协议为了保证选举投票的有效性,规定了一系列的投票原则
- 在任一任期内,单个节点最多只能投一票
- 候选人知道的信息不能比自己的少优先:投票节点通过对比Term(任期)和CommitId来判断是否投“同意”票。
- first-come-first-served 先来先得:收到多个
RequestVote RPC选票,对首先到达的进行投票 - Raft采取Randomized election timeouts保证平票发生概率很低,即每个节点选举超时时间是随机的
5 日志复制
5.1 日志同步过程
-
leader接受到客户端的请求,将请求作为
log entry追加到本地日志,同时向所有follower发起AppendEntries RPC,请求follower同步该log entry -
follower收到请求将日志复制到本地,返回成功给leader
-
当leader收到大多数的follower响应的成功,将本地日志提交,同时发出
commit命令给 follower -
follower收到
commit后,进行事务提交
5.2 日志一致性的保证
Raft协议定义的日志格式
(TermId, LogIndex, LogValue)
// 其中 (TermId, LogIndex) 能唯一确定一条日志
一致性检查
leader发出的 AppendEntries RPC中会额外携带前一条日志的唯一标识(prevTermId, prevLogIndex),如果 follower在本地找不到相同的日志,则拒绝接收这次新的日志。
不一致的场景
正常情况下一致性检查不会失败。然而,leader节点的崩溃可能会导致日志不一致:
- follower比leader缺少一些日志
- follower比leader多了一些未提交的日志(注:Follower 不可能比 leader 多出一些已提交日志,这一点是通过选举上的限制来达成的)
- 旧的leader可能没有commit日志宕机,醒来成为follower后比leader少了一些日志,又多了一些日志
如何处理日志不一致
当出现了leader与follower不一致的情况,Raft强制follower必须复制leader的日志,即leader 从来不会覆盖或者删除自己的日志,而是强制 follower 与它保持一致。
Leader 针对每个 follower 都维护一个 next index,表示下一条需要发送给follower 的日志索引。当leader刚刚上任时,初始化
next index为自己最后一条日志的 index+1。 如果follower的日志跟 leader 不一致,AppendEntriesRPC 的一致性检查就会失败。
在被 follower 拒绝这次AppendEntries RPC后,leader 会减小next index的值并进行重试,直到确定一个next index的值满足一致性。 于是leader便开始从该一致的 next index开始同步日志,follower会删除掉现存的不一致的日志,保留 leader最新同步过来的。
6 状态安全
一致性问题本质就是replicated state machines,即所有节点都从同一个state出发,都经过同样的一些操作序列(log),最后到达同样的state。 其中保证各个节点执行相同的操作序列就是raft算法所要实现的。在“选主+日志复制”这套机制上,还不能保证整个Raft机制的数据的顺序一致性,比如下列场景:
- Leader 将一些日志复制到了大多数节点上,进行 commit 后发生了宕机。
- 某个 follower 并没有被复制到这些日志,但它参与选举并当选了下一任 leader。
- 新的 leader 又同步并 commit 了一些日志,这些日志覆盖掉了其它节点上的上一任 committed 日志。
- 各个节点的状态机可能 apply 了不同的日志序列,出现了不一致的情况。
为了使各个节点执行相同的操作序列,保证状态机的安全性,raft加了一些额外的限制。
6.1 对选举的限制
Raft需要保证拥有最新的已提交日志条目的follower才能被选举为leader
- 该点是Raft需要保证的安全特性,其保证了leader从不会会滚已提交的日志
- 选举限制是由
RequestVote RPC实现
candidate必须在RequestVote RPC中携带自己本地日志的最新 (term, index);
followers接收到请求后,如果自己的日志更新,则拒绝投票给该 candidate;
日志比较原则:term编号大的更新;如果term编号一样大,则log index更大的更新。
6.2 对提交的限制
限制提交先前任期的日志条目
Raft认为,之前任期的日志条目不应该根据多数follower同步原则提交,而应该等到有一个当前任期的日志条目提交时,之前任期的日志条目再一起提交。