算法 - Raft理解

参考

  • Rart论文 https://ramcloud.stanford.edu/raft.pdf
  • 实现源码 https://raft.github.io/
  • 动画原理 http://thesecretlivesofdata.com/raft/
  • 算法解析 http://www.infoq.com/cn/articles/coreos-analyse-etcd

引用

  • Raft 为什么是更易理解的分布式一致性算法
  • raft一致性算法简单解释 by 半天妖

拜占庭将军问题

Leslie Lamport 在三十多年前发表的论文《拜占庭将军问题》(参考[1])。

拜占庭位于如今的土耳其的伊斯坦布尔,是东罗马帝国的首都。由于当时拜占庭罗马帝国国土辽阔,为了防御目的,因此每个军队都分隔很远,将军与将军之间只能靠信差传消息。在战争的时候,拜占庭军队内所有将军必需达成 一致的共识,决定是否有赢的机会才去攻打敌人的阵营。但是,在军队内有可能存有叛徒和敌军的间谍,左右将军们的决定又扰乱整体军队的秩序,在进行共识时,结果并不代表大多数人的意见。这时候,在已知有成员不可靠的情况下,其余忠诚的将军在不受叛徒或间谍的影响下如何达成一致的协议,拜占庭问题就此形成。拜占庭假设是对现实世界的模型化,由于硬件错误、网络拥塞或断开以及遭到恶意攻击,计算机和网络可能出现不可预料的行为。

Lamport 一直研究这类问题,发表了一系列论文。但综合总结一下就是回答下面三个问题:

类似拜占庭将军这样的分布式一致性问题是否有解?
如果有解的话需要满足什么样的条件?
在特定前提条件的基础上,提出一种解法。
前两个问题 Lamport 在论文《拜占庭将军问题》已经回答,而第三个问题在后来的论文 《The Part-Time Parliament》中提出了一种算法并命名为 Paxos。这篇论文使用了大量的数学证明,而我基本就看不懂了(数学符号都认不全-。-;),考虑到大家理解起来都比较困难,后来 Lamport 又写了另外一篇论文 《Paxos Made Simple》完全放弃了所有数学符号的证明,使用纯英文的逻辑推导。我勉强逐字看了一遍,然后感觉若有所悟,但你问我搞懂了吗,我的标准应该还是没懂。对我来说理解一个算法有个明确的标准,就是真的懂了会在头脑里能将算法映射为代码,而看完后面一篇论文仅仅是若有所悟还达不到能映射为代码的清晰度。

虽然 Lamport 认为 Paxos 很 simple,但也许只是针对他的头脑而言。事实是大家理解起来都还是很困难,所以 Raft 就是建立在希望得到一个更易于理解的 Paxos 算法的替代品。把可理解性作为算法的主要目标之一,从论文题目就可看出来《In Search of an Understandable Consensus Algorithm》。

Raft 协议的易理解性描述

虽然 Raft 的论文比 Paxos 简单版论文还容易读了,但论文依然发散的比较多,相对冗长。读完后掩卷沉思觉得还是整理一下才会更牢靠,变成真正属于自己的。这里我就借助前面黑白棋落子里第一种极简思维来描述和概念验证下 Raft 协议的工作方式。

在一个由 Raft 协议组织的集群中有三类角色:

  1. Leader(领袖)
  2. Follower(群众)
  3. Candidate(候选人)

就像一个民主社会,领袖由民众投票选出。刚开始没有领袖,所有集群中的参与者都是群众,那么首先开启一轮大选,在大选期间所有群众都能参与竞选,这时所有群众的角色就变成了候选人,民主投票选出领袖后就开始了这届领袖的任期,然后选举结束,所有除领袖的候选人又变回群众角色服从领袖领导。这里提到一个概念「任期」,用术语 Term 表达。关于 Raft 协议的核心概念和术语就这么多而且和现实民主制度非常匹配,所以很容易理解。三类角色的变迁图如下,结合后面的选举过程来看很容易理解。

算法 - Raft理解_第1张图片
状态转换图

Leader 选举过程

在极简的思维下,一个最小的 Raft 民主集群需要三个参与者(如下图:A、B、C),这样才可能投出多数票。初始状态 ABC 都是 Follower,然后发起选举这时有三种可能情形发生。下图中前二种都能选出 Leader,第三种则表明本轮投票无效(Split Votes),每方都投给了自己,结果没有任何一方获得多数票。之后每个参与方随机休息一阵(Election Timeout)重新发起投票直到一方获得多数票。这里的关键就是随机 timeout,最先从 timeout 中恢复发起投票的一方向还在 timeout 中的另外两方请求投票,这时它们就只能投给对方了,很快达成一致。

算法 - Raft理解_第2张图片
选举过程

选出 Leader 后,Leader 通过定期向所有 Follower 发送心跳信息维持其统治。若 Follower 一段时间未收到 Leader 的心跳则认为 Leader 可能已经挂了再次发起选主过程。

Leader 节点对一致性的影响

Raft 协议强依赖 Leader 节点的可用性来确保集群数据的一致性。数据的流向只能从 Leader 节点向 Follower 节点转移。当 Client 向集群 Leader 节点提交数据后,Leader 节点接收到的数据处于未提交状态(Uncommitted),接着 Leader 节点会并发向所有 Follower 节点复制数据并等待接收响应,确保至少集群中超过半数节点已接收到数据后再向 Client 确认数据已接收。一旦向 Client 发出数据接收 Ack 响应后,表明此时数据状态进入已提交(Committed),Leader 节点再向 Follower 节点发通知告知该数据状态已提交。

算法 - Raft理解_第3张图片
提交过程

在这个过程中,主节点可能在任意阶段挂掉,看下 Raft 协议如何针对不同阶段保障数据一致性的。

1. 数据到达 Leader 节点前

这个阶段 Leader 挂掉不影响一致性,不多说。

算法 - Raft理解_第4张图片

2. 数据到达 Leader 节点,但未复制到 Follower 节点

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

算法 - Raft理解_第5张图片

3. 数据到达 Leader 节点,成功复制到 Follower 所有节点,但还未向 Leader 响应接收

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

算法 - Raft理解_第6张图片

4. 数据到达 Leader 节点,成功复制到 Follower 部分节点,但还未向 Leader 响应接收

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

算法 - Raft理解_第7张图片

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

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

算法 - Raft理解_第8张图片

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

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

算法 - Raft理解_第9张图片

7. 网络分区导致的脑裂情况,出现双 Leader

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

算法 - Raft理解_第10张图片

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

日志复制

当一个节点成为 leader 之后,它就会负责接收客户端的请求。客户端的每个请求都是一个指令,replicate state machine(复制状态机) 会执行这个指令,修改自己的状态。

主节点收到请求之后,把它作为新纪录写入到自己的日志中,然后发送请求给所有的从节点,让它们进行日志复制,等到日志复制完成,leader 节点返回结果给客户端。如果有从节点失败或者比较慢,主节点会一直重试,直到所有的节点保存了所有的日志记录(达到统一的状态)。

每个日志记录都要保存一个状态机的指令,同时还保存主节点接受请求时候的任期值,此外还有一个 index 表示它在日志文件中的位置。

当日志记录被状态机执行后,就称它为已提交(commited)。当主节点知道日志记录已经复制到大多数节点时,会把当前记录提交到本地的状态机(因为日志已经更新到大多数节点,所有数据是安全的),也就是更改数据的值。

leader 节点会记录已经提交(commited)的最大日志 index,然后后续的心跳和日志复制请求会带上这个值,这样从节点就能知道哪些记录已经提交了,自己也会让状态机开始执行日志中的记录。从而达到所有状态机数据的一致性!

这样的日志机制保证了如果不同节点的日志文件某个记录的 index 和任期都相同,那么它们的内容也一定相同,而且之前的日志记录也一定是一样的。

当主节点发送日志复制的请求时,它会带上前一个日志记录的 index 和 term,如果从节点发现自己的日志中不存在这个记录,则会拒绝这个请求。

算法 - Raft理解_第11张图片

正常情况下,每次日志复制都能正常完成,而且节点都能保证日志记录都是完全一致的。但如果 leader 节点崩溃掉,可能会出现日志不一致的情况(奔溃的主节点还没有完全把自己日志文件中的记录复制到其他节点,因此有些节点的日志比另外一些节点内容更多)。

对于日志内容不一致的请求,raft 采取用主节点日志内容覆盖 follower 节点日志的做法,先找到从节点日志和自己日志记录第一个不一致的地方,然后一直覆盖到最后。

整个过程是这样的:当某个节点当选 leader 之后,会发送日志复制请求到从节点,并带着 nextIndex(主节点要发送的下一个日志记录的 index),如果从节点出现日志记录不一致的情况,会拒绝该请求,那么主节点知道发生了不一致,递减 nextIndex,然后重新发送请求,直到日志一致的地方,一切回复正常,然后继续发送日志复制请求,就会把从节点的日志覆盖为主节点的日志内容。

安全 safety

前面提到的选主和日志复制是 raft 算法的核心,能够保证日志里面记录最终是一致的,但是还不能够保证所有节点的状态机能够按顺序执行命令。raft 对选主做出了限制,从而实现算法的正确性。

总的来说,这个限制只有一句话:只有保存了最新日志的节点才能选举成为 leader,选举的时候如果节点发现候选节点日志没有自己的新,则拒绝投票给它。因为保存了最新日志,因此新 leader 产生之后,follower 节点和它保持同步就不会出现数据冲突的问题。这样也能保证 leader 节点不会覆盖日志中的记录。

上面最新日志指的是保存了所有的已提交日志记录,因为已提交已经包含了集群中大多数节点都会有对应的日志记录,因此能保证没有最新记录的候选人选不上(因为大多数节点会拒绝投票给它),而且至少有一个节点符合条件(只要集群节点数超过 3)。

你可能感兴趣的:(算法 - Raft理解)