一致性算法 |Raft协议详解

120 阅读6分钟

Raft协议详解

1 Raft节点状态

Raft协议的每个节点都会处于三种状态之一:Leader、Follower、Candidate。

  • Leader(领导者)
    接受客户端请求,并向Follower同步请求日志,当大多数节点通知Leader日志同步成功,则Leader提交日志
  • Follower(追随者)
    接受并持久化来自Leader同步的日志,Leader通知可以提交则提交日志
  • Candidate(候选者)
    Leader选举过程中出现的临时角色,如果Follower没有收到Leader的心跳响应超过150~300ms,会进行Leader选举

状态转移图如下

image.png

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_id1
  • 状态改成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 日志同步过程

  1. leader接受到客户端的请求,将请求作为log entry追加到本地日志,同时向所有follower发起AppendEntries RPC,请求follower同步该log entry

  2. follower收到请求将日志复制到本地,返回成功给leader

  3. 当leader收到大多数的follower响应的成功,将本地日志提交,同时发出commit命令给 follower

  4. follower收到commit后,进行事务提交

image.png

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 不一致, AppendEntries RPC 的一致性检查就会失败。
在被 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同步原则提交,而应该等到有一个当前任期的日志条目提交时,之前任期的日志条目再一起提交。