【分布式系列】一致性算法之Raft

743 阅读14分钟

前言

阅读Alibaba Nacos源码,发现Nacos中有用到了分布式一致性算法——Raft(The Raft Consensus Algorithm);于是查阅了一些资料,整理成文,希望对大家在学习Raft的时候能有一些参考。特别说明的是,Raft算法内容较多,本篇篇幅有限,会用一种极简化思维(下面会说明)来讲解,本文不涉及Raft代码的实现。

先从拜占庭将军问题说起

拜占庭将军问题(Byzantine failures),是由莱斯利·兰伯特( Leslie Lamport )提出的点对点通信中的基本问题,问题起源如下:

拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了达到防御目的,每个军队都分隔很远,将军与将军之间只能靠信差传消息。 在战争的时候,拜占庭军队内所有将军和副官必须达成一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序。在进行共识时,结果并不代表大多数人的意见。这时候,在已知有成员谋反的情况下,其余忠诚的将军在不受叛徒的影响下如何达成一致的协议,拜占庭问题就此形成。

拜占庭将军问题是分布式领域最复杂、最严格的容错模型。但在日常工作中使用的分布式系统面对的问题不会那么复杂,更多的是计算机故障挂掉了,或者网络通信问题而没法传递信息,这种情况不考虑计算机之间互相发送恶意信息,极大简化了系统对容错的要求,最主要的是达到一致性。 

一致性算法

1990年莱斯利·兰伯特( Leslie Lamport )提出了一种基于消息传递的一致性算法——Paxos,Paxos 算法解决的问题是一个分布式系统中如何就某个值(决议)达成一致。

一致性算法允许多台机器作为一个集群协同工作,并且在其中的某几台机器出故障时集群仍然能正常工作。 正因为如此,一致性算法在建立可靠的大规模软件系统方面发挥了关键作用。 在之前,Paxos 几乎主导了关于一致性算法的讨论:大多数一致性的实现都是基于 Paxos 或受其影响,Paxos 已成为用于教授学生一致性相关知识的主要工具。

不幸的是,Paxos 实在是太难以理解,尽管许多人一直在努力尝试使其更易懂。 此外,其架构需要复杂的改变来支持实际系统。 结果是,系统开发者和学生都在与 Paxos 斗争。

Raft横空出世

由于Paxos的晦涩难懂,斯坦福大学的教授在2014年发表了新的分布式一致性协议—— Raft。与 Paxos 相比,Raft 有着基本相同运行效率,但是更容易理解,也更容易被用在系统开发上。

为了使 Raft 协议更易懂,Raft 将一致性的关键元素分开,如 leader 选举、日志复制和安全性,并且它实施更强的一致性以减少必须考虑的状态的数量。接下来就让我们进入“Raft的世界”吧! 

极简化思维

如下图所示,大家先思考一个问题:甲乙两人轮流在一张圆桌上平放黑白围棋子,每次放一子,棋子不许重叠,谁先没有地方放就输。

请问怎样放才能赢?

上面的图回答了这个问题,就是先行者必胜,这里使用了三种不同的思维方式。

  1. 假如桌子只有一个围棋子那么大。

  2. 假如桌子无限大,先行者先占住圆心,由于圆是对称图形,所以只要对手还能找到位置放,你总能在对称的另一面找到位置放。

  3. 一个圆中可画单数个直径相等且互切的小圆。

三种不同的思维方式在可理解性难度上逐渐加深。第一种是极简化思维,但数学上是不严谨的。第二种是极限思维,和第一种结合起来就是数学归纳法了,在数学上是严谨的。第三种是形象思维,使用了几何学概念,但对于没有几何学基础知识的人就很难理解了。

极简化思维分析Raft

在包含若干节点Raft集群中,存在几个重要的角色:Leader、Candidate、Follower。每种角色负责的任务也不一样,正常情况下,集群中的节点只存在 Leader 与 Follower 两种角色。

1. Leader(领导者):处理所有客户端交互,日志复制等,一般一次只有一个Leader;

2. Follower(追随者):响应 Leader 的日志同步请求,响应 Candidate 的邀票请求,以及把客户端请求到 Follower 的事务转发(重定向)给 Leader;

3. Candidate(候选者):负责选举投票,集群刚启动或者 Leader 宕机时,角色为 Follower 的节点将转为 Candidate 并发起选举,选举胜出(获得超过半数节点的投票)后,从 Candidate 转为 Leader 角色;

下图展示了不同三种角色变化过程的示意图,下面我们会详细分析。

Raft选举

就像是一个民主社会,领袖由民众投票选出。刚开始没有领袖,所有集群中的参与者都是群众,在大选期间所有的群众都可以参与竞选,这时所有群众的角色就变成了候选人。

每个群众手里都分配了一个倒计时器,这个倒计时器设置的时间是随机的,在150ms~300ms之间,谁的倒计时器先结束谁就开始有优先拉票权。比如张三倒计时结束,张三会先投自己一票,然后向集群中的其他人发起拉票(RequestVote RPC),当集群中大多数人(N/2+1)都把票投给了张三,那么张三就成了领袖,这时候张三会向集群中所有人发起心跳来表示自己的权威,告诉大家,你们都要听我的。

我们来看一个实际的一个过程,如果是用极简化思维来分析,那么一个最小的Raft集群至少有三个节点(A、B、C),假设A的倒计时先结束,这个时候A向B、C发起拉票请求。

第一种情形:B、C都把票投给了A,这个时候A就成为了Leader

第二种情形:B把票投给了A,C投给了自己,这个时候A也能成为了Leader,因为获得了大多数票。

第三种情形:A、B、C都把票投给了自己,这个时候没有选出Leader

这种情况则表明本轮投票无效(Split Votes),每方都投给了自己,结果没有任何一方获得多数票。之后每个参与方会继续随机休息一阵(Election Timeout)重新发起投票直到一方获得多数票,理论上,如果每次都是平票,选举将会一直往下执行。

选举完成后,Leader会向所有的Follower节点发起心跳,若 Follower 一段时间未收到 Leader 的心跳则认为 Leader 可能已经挂了会再次发起新选主过程。

任期

Raft算法将时间分为一个个的任期(term),充当逻辑时钟的作用,每一个term的开始都是Leader选举。在成功选举Leader之后,Leader会在整个term内管理整个集群。如果Leader选举失败,该term就会因为没有Leader而结束,如下图所示: 

日志复制

Leader 一旦被选举出来,就开始为客户端请求提供服务。客户端的每一个请求都包含一条将被复制状态机执行的指令。Leader 把该指令作为一个新的条目追加到日志中去,然后并行的发起 AppendEntries RPC 给其他的服务器,让它们复制该条目。当该条目被安全地复制,leader 会应用该条目到它的状态机中(状态机执行该指令)然后把执行的结果返回给客户端。如果 follower 崩溃或者运行缓慢,或者网络丢包,leader 会不断地重试 AppendEntries RPC(即使已经回复了客户端)直到所有的 follower 最终都存储了所有的日志条目。

复制过程

  1. 客户端的每一个请求都包含被复制状态机执行的指令。

  2. leader 把这个指令作为一条新的日志条目添加到日志中,然后并行发起 RPC 给其他的服务器,让他们复制这条信息。

  3. 假如这条日志被安全的复制,Leader就应用这条日志到自己的状态机中,并返回给客户端。

  4. 如果 follower 宕机或者运行缓慢或者丢包,领导人会不断的重试,直到所有的 follower 最终都存储了所有的日志条目。

日志的数据结构:

  1. 创建日志时的任期号(用来检查节点日志是否出现不一致的情况)

  2. 状态机需要执行的指令(真正的内容)

  3. 索引:整数索引表示日志条目在日志中位置

在发送 AppendEntries RPC 的时候,leader 会将前一个日志条目的索引位置和任期号包含在里面。如果 follower 在它的日志中找不到包含相同索引位置和任期号的条目,那么他就会拒绝该新的日志条目。一致性检查就像一个归纳步骤:一开始空的日志状态肯定是满足 Log Matching Property(日志匹配特性) 的,然后一致性检查保证了日志扩展时的日志匹配特性。因此,每当 AppendEntries RPC 返回成功时,leader 就知道 follower 的日志一定和自己相同(从第一个日志条目到最新条目)。

Raft 对日志有以下保证:如果 2 个日志的相同的索引位置的日志条目的任期号相同,那么 Raft 就认为这个日志从头到这个索引之间全部相同。

依据这个保证,当 leader 和 follower 日志冲突的时候,leader 将校验 follower 最后一条日志是否和 leader 匹配,如果不匹配,将递减查询,直到匹配,匹配后,删除所有冲突的日志。这样就实现了主从日志的一致性。

正常操作期间,leader 和 follower 的日志保持一致,所以 AppendEntries RPC 的一致性检查从来不会失败。然而,leader 崩溃的情况会使日志处于不一致的状态(老的 leader 可能还没有完全复制它日志里的所有条目)。这种不一致会在一系列的 leader 和 follower 崩溃的情况下加剧。Follower 可能缺少一些在新 leader 中有的日志条目,也可能拥有一些新 leader 没有的日志条目,或者同时发生。缺失或多出日志条目的情况可能会涉及到多个任期。

下图展示了Leader和Follower日志冲突的情况:

上面这种情况Raft 如何处理呢?

leader 为每一个 follower 维护一个下标,称之为 nextIndex,表示下一个需要发送给 follower 的日志条目的索引。

当一个新 leader 刚获得权力的时候,他将自己的最后一条日志的 index + 1,如果一个 follower 的日志和 leader 不一致,那么在下一次 RPC 附加日志请求中,一致性检查就会失败(不会插入数据)。

当这种情况发生,leader 就会把 nextIndex 递减进行重试,直到遇到匹配到正确的日志。

当匹配成功之后,follower 就会把冲突的日志全部删除,此时,follower 和 leader 的日志就达成一致。

Leader 节点对一致性的影响

日志复制过程中,Leader可能在任意阶段挂掉,看下 Raft 协议如何针对不同阶段保障数据一致性的。

先看下正常情况下的示意图。

再看异常情况的几个情形。

**情形一:数据到达 Leader 节点前:**这个阶段 Leader 挂掉不影响一致性。

情形二:数据到达 Leader 节点,但未复制到 Follower 节点

这个阶段 Leader 挂掉,数据在Leader中属于未提交状态,Client 不会收到响应,会认为超时失败可安全发起重试。Follower 节点上没有该数据,重新选主后 Client 重试重新提交可成功。原来的 Leader 节点恢复后作为 Follower 加入集群重新从当前任期的新 Leader 处同步数据,强制保持和 Leader 数据一致。

情形三:数据到达 Leader 节点,成功复制到 Follower 所有节点,但还未向 Leader 响应接收

这个阶段 Leader 挂掉,虽然数据在 Follower 节点处于未提交状态,但保持一致,重新选出 Leader 后可完成数据提交,此时 Client 由于不知到底提交成功没有,可重试提交。针对这种情况 Raft 要求 RPC 请求实现幂等性,也就是要实现内部去重机制。

情形四:数据到达 Leader 节点,成功复制到 Follower 部分节点,但还未向 Leader 响应接收

这个阶段 Leader 挂掉,数据在 Follower 节点处于未提交状态且不一致,Raft 协议要求投票只能投给拥有最新数据的节点。所以拥有最新数据的节点会被选为 Leader 再强制同步数据到 Follower,数据不会丢失并最终一致。

情形五:数据到达 Leader 节点,成功复制到 Follower 所有或多数节点,数据在 Leader 处于已提交状态,但在 Follower 处于未提交状态

这个阶段 Leader 挂掉,重新选出新 Leader 后的处理流程和阶段 3 一样。

情形六:数据到达 Leader 节点,成功复制到 Follower 所有或多数节点,数据在所有节点都处于已提交状态,但还未响应 Client

这个阶段 Leader 挂掉,集群内部数据其实已经是一致的,Client 重复重试基于幂等策略对一致性无影响。

情形七:网络分区导致的脑裂情况,出现双 Leader

网络分区将原先的 Leader 节点和 Follower 节点分隔开,Follower 收不到 Leader 的心跳将发起选举产生新的 Leader。这时就产生了双 Leader,原先的 Leader 独自在一个区,向它提交数据不可能复制到多数节点所以永远提交不成功。向新的 Leader 提交数据可以提交成功,网络恢复后旧的 Leader 发现集群中有更新任期(Term)的新 Leader 则自动降级为 Follower 并从新 Leader 处同步数据达成集群数据一致。

综上穷举分析了最小集群(3 节点)面临的所有情况,可以看出 Raft 协议都能很好的应对一致性问题,并且很容易理解。

结尾

Raft是分布式中比较经典也是易于理解的共识算法,应用非常广泛。他的设计思想在一定程度上能给我们带来很多启发,上面我主要分析了选举和日志复制,实际上还有很多内容没有提到,比如安全性。如果要对Raft有一个更加深入的理解,建议去看看Raft论文,本文只能带大家简单入门,更多内容还需要大家自己多多钻研。

**Raft算法动画演示:**thesecretlivesofdata.com/raft/

本文参考的部分资料:

  1. Raft github: raft.github.io/

  2. Raft paper: raft.github.io/raft.pdf

  3. Raft为什么是更易理解的分布式一致性算法:www.cnblogs.com/mindwind/p/…