深入浅出一文搞懂分布式一致性算法新贵Raft协议

前言

在分布式的世界里,要说最核心最复杂的功能,一致性地实现无出其右,之前的paxos算法堪称经典,被认为是同类算法中效果最好的,基本上成为分布式一致性的代名词,但是paxos算法也是出了名的难理解,而且相当不好实现。本人也花了很多时间、看了很多材料也没有真正理解。所以基于paxos的思想进行的一致性算法的简化和实现就成为了现实的需求,在此背景下,本文的主角Raft就出现了。

Raft算法的头号目标就是容易理解(UnderStandable),这从论文中就可以看出来。当然,Raft增强了可理解性,在性能、可靠性、可用性方面是不输于Paxos的。建议大家拜读下作者的论文 Raft论文,下面将详细说明raft的思想以及实现的过程

正文

raft为了实现容易理解的目标,在paxos的基础上进行的状态简化以及问题拆分,将之前复杂的逻辑拆成若干个子问题,基本上可以总结成下面几个方面:

leader election

role

首先先说明下Raft算法中节点的角色,分为以下三种:

给出状态机,协助大家理解:

role

上面在讲解role的时候好几次说到了一个名词term,这是raft算法中的一个核心概念,很多的机制都需要依赖term。term通俗来讲就是任期,即一个leader正常工作的时间段,如果因为某些原因当前的leader不再是leader时,则该term结束,重新进行选举,开始新的任期,是不是和现实生活中的选举很像?

另外term在raft算法中也起到了逻辑时钟的作用,在raft的实现中起到了重要的作用,此处用一句话先来概括:即term大的优先级高,leader必须是拥有更大的term。用白话理解:就是当前总统和前总统的关系,总统只能有一个就是当期总统,前总统在当前总统面前就变成选民了。在Raft中,term是个整数型的值,term变化即将term的值加1

leader election process

下面就来说说leader选举的详细过程,从上面的状态机可以看出,集群初始化时,大家都是follower,当未发现leader心跳并超时后,则follower变成candidate,并发起leader election。每个candidate的动作如下:

  1. 给自己投一票
  2. 向其他节点发起RequestVote RPC来进行拉票
  3. 等待其他节点的响应

在此过程中会出现三种情况:

上述情况的产生需要满足下面几个约束:

上面的前两种情况比较容易理解,我们重点来说说第三中情况,用一个比较形象的例子来说明选举过程:

有五个小伙伴要选取一个组长,选举过程如下:

注:F,C,L分别对应的是follower,candidate和leader角色,在本例中就是组员,候选人和组长;名字是用来区分节点的标识;而括号中的数字则代表着term的值

  1. 初始状态大家都是组员,等待组长联系自己
  2. 等待一段时间后(leader heartbeat timeout),昌坦,继东和呈祥发现没有组长或者组长掉线了,这三个人就变成了候选人,然后发起了组长选举的流程
  3. 选举开始后,三个候选人都将自己的任期加1变成了2,然后投了自己一票,而组员晓通选了昌坦,组员溪泽选了呈祥,三个候选人的票数比是2:1:2,未能达到法定的半数以上的多数票,未能选出组长
  4. 漫长的等待后(election timeout),昌坦发现自己没有获得多数选票,也没有收到其他候选人当选组长的消息(leader heartbeat),意识到了此次选举失败,然后将自己的任期加1变成3,再次发起选举,组员晓通和溪泽发现该任期中未投票 ,先到先得,直接投给了昌坦,而候选人继东和呈祥发现昌坦的任期比自己大,则放弃候选人的角色变成了组员并投票给昌坦(此处从候选人变成组员也可能是收到了昌坦当选组长的消息后转变的,因为组长的当选并不需要全票,只要达到多数选票即可)
  5. 昌坦全票当选组长,并向其他组员通报了自己成为组长的消息,后续所有的组内管理以及消息同步都通过组长昌坦向其他组员传达

整个leader election流程就是这样,是不是很好理解,基本上符合现实生活中的理解。但是上述的流程可能有一个问题,分为以下两种情况:

上述的两种情况会影响到leader election的成功率和效率,在设计中应该被规避,面对这两种情况,Raft给出了自己的解决方案:

leader election

当系统有了leader后,系统就进入对外工作期了。客户端的一切请求来发送到leader,leader来调度这些并发请求的顺序,并且保证leader与followers状态的一致性。raft中的做法是,将这些请求以及执行顺序告知followers。leader和followers以相同的顺序来执行这些请求,保证状态一致。

下图就是请求写入处理的相关流程:

  1. 客户端向leader提交写入请求
  2. leader接收到客户端请求后封装RPC并行将修改发送到follower
  3. follower在接收到leader发送的RPC后,回复leader已经收到该请求
  4. 在leader接收到多数(majority,含leader)follower的回复后,回复客户端接收成功,将变更状态设置为commited,然后将变更写入到状态机,此时写入实际上已经生效,无法回滚
  5. leader与follower通信,协助follower完成变更的提交,当变更提交完毕后,follower会将变更写入到状态机,此时变更才真正的影响到节点,此时的状态可以理解为applied。此过程中,可能会出现各种问题,比如说网络连接超时,命令执行不成功等问题,leader会持续和follower进行通信,保证follower最终完成所有的操作,与leader达成最终一致性。这种最终一致性是对内的,对外部的client的透明的,外部的client只会看到leader上状态的强一致性。这种强一致性和最终一致性的配合使用,不仅降低了一致性实现的各种成本,还保证了系统的健壮性,能保证在各种异常情况下的恢复与状态同步。

上述流程中,有点类似于两阶段提交(2PC),这种提交方式的好处就是能简化分布式事务的复杂性,在不直接使用分布式锁的前提下,能最大程度保证分布式事务的实现。但是这里和标准的2PC还是有差别的,差别就是这里leader只需要多数(majority)的follower答复即可,那么在这种情况下,raft是怎么保证一致性以及数据的完整性,尤其在集群主节点发生故障的时候呢?此处先留下这个问题,后续在safety模块进行解答

最后看下log长什么样子吧:

log由三部分组成:

log为了配合raft实现leader选举以及状态同步,需要具备某些特性 ,满足相关的约束:

safety

上面大部分说的都是正常情况下raft是怎么工作和运行的,但是一个分布式不仅需要在正常情况下能稳定高效的运行,也需要在任何情况下,系统都不能出现不可逆的错误,也不能向客户端返回错误的内容。raft也是如此,上面两个章节已经说明了为了达到safety目标raft所采取的一些策略和约束,下面就来以几个典型场景来说明下raft是怎样容错的。

follower crash

follower crash处理起来相对简单,只要重新上线加入集群后 ,发现了已经存在的leader,直接加入集群并且接收来自leader的消息,然后在leader的指导下完成数据和状态的同步,在此过程中,leader与follower通信使用的是AppendEntries RPC,借用官方的结构图:

另外,每个节点上的日志都是通过状态机(State Machine)进行管理的,节点上applied的command会提交到状态机进行保存,用于后续节点(leader-follower)之间进行数据和状态同步,下面是状态机中状态相关的结构图:

从上图可以看出对于每个follower,leader保持2个属性,一个就是nextIndex即leader要发给该follower的下一个entry的index,另一个就是matchIndex即follower发给leader的确认index。

实现过程如下:

leader当选成功后,会进行部分属性的初始化:

nextIndex=leader的log的最大index+1
matchIndex=0

然后开始准备AppendEntries RPC请求的参数:

prevLogIndex=nextIndex-1
prevLogTerm=从log中得到prevLogIndex对应的term

初始化entries数组:

初始化以及成功当选的第一次心跳的时候是空
后续的值是从本地log中取prevLogIndex+1到最后的所有log entry

最后是leaderCommit赋值:

leaderCommit=leader上的commit indexentry

至此,所有参数准备完毕,发送RPC请求到所有的follower,follower再接收到这样的请求之后,处理如下:

nextIndex=follower传回来的index+1
matchIndex=follower传回来的index
nextIndex=follower传回来的index+1
matchIndex=follower传回来的index
prevLogIndex=follower传回来的index
prevLogTerm=从log中得到上述prevLogIndex对应的term
entry[]=从本地log中取prevLogIndex+1到最后的所有log entry
leaderCommit=leader上的commit index

leader crash

leader crash相对来说比较复杂,需要针对几个固定场景进行具体分析,详细参考这篇文章: Leader节点的一致性保障

leader与follower同步数据

在集群出现异常重新选取leader后都需要面临数据同步的问题,下面这张图列举了集中场景:

上面列举了6钟follower可能存在的状态,前两种follower的数据比leader少,中间两种follower数据比leader多,最后两种leader和follower数据有多有少。这里需要注意一下:

图中的虚线框代表着uncommitted的log entry;

另外,此时leader的term是8(如果这7个节点是一个集群的话),后续分析中这一点很重要。

具体分析下上述几种场景中follower状态产生的原因:

上面的几种情况虽然不太具备现实意义,但是还是对于我们理解整个raft的各种机制有很大的帮助,建议大家好好梳理下,肯定会有收获的。

针对上面的所有情况,处理方式就就是,leader选举时所有的uncommitted的log entry都会回滚(所以图中所有的uncommitted的log entry其实在term 8 leader选举后就不存在了,此处画出来仅仅是为了方便大家理解整个数据和状态的演变过程),然后当出现了leader与follower不一致的情况,leader强制follower复制自己的log,复制的过程参加follower crash这一章节

raft算法如何保证leader的数据永远是最新的且不会丢数据

这个问题其实是由上面的几个majority以及几个约束共同来完成的:

majority

约束

综上,commit majority代表着最新数据的持有;而vote majority代表着最后的leader会从这里面产生(candidate肯定会投自己的票,所以也在vote majority之中),而这两个majority肯定会有1到(N/2+1)个节点是重复的,也就是这些节点既有最新的数据,也有机会成为leader,再看那个约束条件,这个约束条件将vote majority中不具有最新数据的那些节点排除,那么最后的leader节点一定会在两个majority重复的节点中产生,而这些节点持有最新的数据,保证了leader的数据永远是最新的且不会丢数据。

raft与其他协议(Viewstamped Replication、mongodb)不同,raft始终保证leader包含最新的已提交的日志,因此leader不会从follower catchup日志,这也大大简化了系统的复杂度。

总结

raft将一致性问题分解成两个相对独立的问题,leader election,log replication。流程是先选举出leader,然后leader负责复制、提交log(log中包含command),为了在任何异常情况下系统不出错,即满足safety属性,对leader election,log replication两个子问题有诸多约束,在满足功能和性能的基础上,尽量简化模型以及状态,提高了整个算法的易理解性和易实现性,堪称经典且实用的算法。

想想 Paxos 算法是 Leslie Lamport 在 1990 年就公开发表在了自己的网站上,想想我们是什么时候才听说的?什么时候才有一个可用的实现?而 Raft 算法是 2013 年发表的,现在可以看到有许多不同语言开源的实现库了。而且很多raft-like算法在raft的基础上进行了定制化扩展,用以满足各种系统需求(如elasticsearch 7中的集群协调子系统中定制化raft的实现),这就是易理解性和易实现性的重要性。

展开阅读全文

页面更新:2024-03-20

标签:算法   组员   选票   深入浅出   任期   分布式   新贵   节点   集群   组长   状态   协议   情况   数据

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top