1. RAFT算法简介
1.1 Raft背景
在分布式系统中,一致性算法至关重要。在所有一致性算法中,Paxos 最负盛名,它由莱斯利·兰伯特(Leslie Lamport)于 1990 年提出,是一种基于消息传递的一致性算法,被认为是类似算法中最有效的。
Paxos 算法虽然很有效,但复杂的原理使它实现起来非常困难,截止目前,实现 Paxos 算法的开源软件很少,比较出名的有 Chubby、LibPaxos。此外,Zookeeper 采用的 ZAB(Zookeeper Atomic Broadcast)协议也是基于 Paxos 算法实现的,不过 ZAB 对 Paxos 进行了很多改进与优化,两者的设计目标也存在差异——ZAB 协议主要用于构建一个高可用的分布式数据主备系统,而 Paxos 算法则是用于构建一个分布式的一致性状态机系统。由于 Paxos 算法过于复杂、实现困难,极大地制约了其应用,而分布式系统领域又亟需一种高效而易于实现的分布式一致性算法,在此背景下,Raft 算法应运而生。
Raft是一个共识算法(consensus algorithm),所谓共识,就是多个节点对某个事情达成一致的看法,即使是在部分节点故障、网络延时、网络分割的情况下。共识算法的实现一般是基于复制状态机(Replicated state machines),何为复制状态机:简单来说:相同的初识状态 + 相同的输入 = 相同的结束状态。
1.2 Raft 角色
一个Raft集群包含若干个节点,这些节点分为三种状态:Leader、 Follower、Candidate,每种状态负责的任务也是不一样的。正常情况下,集群中的节点只存在 Leader 与 Follower 两种状态。
- leader:负责日志的同步管理,处理来自客户端的请求,与Follower保持heartBeat的联系;
- follower:响应 Leader 的日志同步请求,响应Candidate的邀票请求,以及把客户端请求到Follower的事务转发(重定向)给Leader;
- candidate:负责选举投票,集群刚启动或者Leader宕机时,状态为Follower的节点将转为Candidate并发起选举,选举胜出(获得超过半数节点的投票)后,从Candidate转为Leader状态。
还有一个关键概念:term(任期)。以选举(election)开始,每一次选举term都会自增,充当了逻辑时钟的作用。
1.3 Raft的3个子问题
为简化逻辑和实现,Raft 将一致性问题分解成了三个相对独立的子问题。
- 选举(Leader Election):当 Leader 宕机或者集群初创时,一个新的 Leader 需要被选举出来;
- 日志复制(Log Replication):Leader 接收来自客户端的请求并将其以日志条目的形式复制到集群中的其它节点,并且强制要求其它节点的日志和自己保持一致;
- 安全性(Safety):如果有任何的服务器节点已经应用了一个确定的日志条目到它的状态机中,那么其它服务器节点不能在同一个日志索引位置应用一个不同的指令。
2. 选举过程
如果follower在election timeout内没有收到来自leader的心跳,则会主动发起选举。
第一阶段:所有节点都是 Follower。
Raft 集群在刚启动(或 Leader 宕机)时,所有节点的状态都是 Follower,初始 Term(任期)为 0。同时启动选举定时器,每个节点的选举定时器超时时间都在 100~500 毫秒之间且并不一致。
第二阶段:Follower 转为 Candidate 并发起投票。
没有leader后,followers状态自动转为candidate,并向集群中所有节点发送投票请求并且重置选举定时器。投票过程有:
- 增加节点本地的 current term ,切换到candidate状态
- 投自己一票,并行给其他节点发送 RequestVote RPCs
- 等待其他节点的回复,可能出现三种结果:
- 收到majority的投票(含自己的一票),则赢得选举,成为leader
- 被告知别人已当选,那么自行切换到follower
- 一段时间内没有收到majority投票,则保持candidate状态,重新发出选举
第三阶段:投票策略
投票的约束条件有:
- 在一个term内,一个节点只允许发出一次投票;
- 候选人知道的信息不能比自己的少(这一部分,后面介绍log replication和safety的时候会详细介绍)
- first-come-first-served 先来先得
如果参加选举的节点是偶数个,raft通过randomized election timeouts来尽量避免平票情况,也要求节点的数目都是奇数个,尽量保证majority的出现。
3. log Replication原理
当leader选举成功后,客户端所有的请求都交给了leader,leader调度请求的顺序性和followers的状态一致性。在集群中,所有的节点都可能变为leader,为了保证后续leader节点变化后依然能够使集群对外保持一致,需要通过Log Replication机制来解决如下两个问题:
- Follower与Leader节点相同的顺序依次执行每个成功提案;
- 每个成功提交的提案必须有足够多的成功副本,来保证后续的访问一致
第一阶段:客户端请求提交到 Leader。
Leader 在收到client请求提案后,会将它作为日志条目(Entry)写入本地log中。需要注意的是,此时该 Entry 的状态是未提交(Uncommitted),Leader 并不会更新本地数据,因此它是不可读的。
第二阶段:Leader 将 Entry 发送到其它 Follower
Leader 与 Floolwers 之间保持着心跳联系,随心跳 Leader 将追加的 Entry(AppendEntries)并行地发送给其它的 Follower,并让它们复制这条日志条目,这一过程称为复制(Replicate)。
- 为什么 Leader 向 Follower 发送的 Entry 是 AppendEntries
因为 Leader 与 Follower 的心跳是周期性的,而一个周期间 Leader 可能接收到多条客户端的请求,因此,随心跳向 Followers 发送的大概率是多个 Entry,即 AppendEntries。
- Leader 向 Followers 发送的不仅仅是追加的 Entry(AppendEntries)
在发送追加日志条目的时候,Leader 会把新的日志条目紧接着之前条目的索引位置(prevLogIndex), Leader 任期号(Term)也包含在其中。如果 Follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么它就会拒绝接收新的日志条目,因为出现这种情况说明 Follower 和 Leader 不一致。
- 如何解决 Leader 与 Follower 不一致的问题
正常情况下,Leader 和 Follower 的日志保持一致。然而,Leader 和 Follower 一系列崩溃的情况会使它们的日志处于不一致状态。有三种情况:
- 1) Follower落后新的leader,丢失一些在新的 Leader 中有的日志条目
- 2)Follower领先新的leader,有一些 Leader 没有的日志条目,
- 3)或者两者都发生。丢失或者多出日志条目可能会持续多个任期。
要使 Follower 的日志与 Leader 恢复一致,Leader 必须找到最后两者达成一致的地方(就是回溯,找到两者最近的一致点),然后删除从那个点之后的所有日志条目,发送自己的日志给 Follower。Leader 为每一个 Follower 维护一个 nextIndex,它表示下一个需要发送给 Follower 的日志条目的索引地址。当一个 Leader 刚获得权力的时候,它初始化所有的 nextIndex 值,为自己的最后一条日志的 index 加 1。如果一个 Follower 的日志和 Leader 不一致,那么在下一次附加日志时一致性检查就会失败。在被 Follower 拒绝之后,Leader 就会减小该 Follower 对应的 nextIndex 值并进行重试。最终 nextIndex 会在某个位置使得 Leader 和 Follower 的日志达成一致。当这种情况发生,附加日志就会成功,这时就会把 Follower 冲突的日志条目全部删除并且加上 Leader 的日志。一旦附加日志成功,那么 Follower 的日志就会和 Leader 保持一致,并且在接下来的任期继续保持一致。
第三阶段:Leader 等待 Followers 回应。
Followers 接收到 Leader 发来的复制请求后,有两种可能的回应:
- 写入本地日志中,返回 Success;
- 一致性检查失败,拒绝写入,返回 False,原因和解决办法上面已做了详细说明。
当 Leader 收到大多数 Followers 的回应后,会将第一阶段写入的 Entry 标记为提交状态(Committed),并把这条日志条目应用到它的状态机中。
第四阶段:Leader 回应客户端。
完成前三个阶段后,Leader会向客户端回应 OK,表示写操作成功。
第五阶段,Leader 通知 Followers Entry 已提交
Leader 回应客户端后,将随着下一个心跳通知 Followers,Followers 收到通知后也会将 Entry 标记为提交状态。至此,Raft 集群超过半数节点已经达到一致状态,可以确保强一致性。
4. raft safety保证
1) election safety: 在一个term内,至多有一个leader被选举出来。raft算法通过
- 一个节点某一任期内最多只能投一票;
- 只有获得majority投票的节点才会成为leader。
2)log matching:说如果两个节点上的某个log entry的log index相同且term相同,那么在该index之前的所有log entry应该都是相同的。leader在某一term的任一位置只会创建一个log entry,且log entry是append-only。
3)consistency check。leader在AppendEntries中包含最新log entry之前的一个log 的term和index,如果follower在对应的term index找不到日志,那么就会告知leader不一致。当出现了leader与follower不一致的情况,leader强制follower复制自己的log。
3)leader completeness :如果一个log entry在某个任期被提交(committed),那么这条日志一定会出现在所有更高term的leader的日志里面。
- 一个日志被复制到majority节点才算committed
- 一个节点得到majority的投票才能成为leader,而节点A给节点B投票的其中一个前提是,B的日志不能比A的日志旧。
4)stale leader: 落后的leader,但在网络分割(network partition)的情况下,可能会出现两个leader,但两个leader所处的任期是不同的。而在raft的一些实现或者raft-like协议中,leader如果收不到majority节点的消息,那么可以自己step down,自行转换到follower状态。
5)leader crash:新的节点成为Leader,为了不让数据丢失,希望新Leader包含所有已经Commit的Entry。为了避免数据从Follower到Leader的反向流动带来的复杂性,Raft限制新Leader一定是当前Log最新的节点,即其拥有最多最大term的Log Entry。
6)State Machine Safety
某个leader选举成功之后,不会直接提交前任leader时期的日志,而是通过提交当前任期的日志的时候“顺手”把之前的日志也提交了,具体的实现是:如果leader被选举后没有收到客户端的请求呢,论文中有提到,在任期开始的时候发立即尝试复制、提交一条空的log。
总结:raft将共识问题分解成两个相对独立的问题,leader election,log replication。流程是先选举出leader,然后leader负责复制、提交log(log中包含command)
log replication约束:
- 一个log被复制到大多数节点,就是committed,保证不会回滚
- leader一定包含最新的committed log,因此leader只会追加日志,不会删除覆盖日志
- 不同节点,某个位置上日志相同,那么这个位置之前的所有日志一定是相同的
- Raft never commits log entries from previous terms by counting replicas.