(七)漫谈分布式之一致性算法下篇:一文从根上儿理解大名鼎鼎的Raft共识算法!

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: Raft通过一致性检查,能在一定程度上保证集群的一致性,但无法保证所有情况下的一致性,毕竟分布式系统各种故障层出不穷,如何在有可能发生各类故障的分布式系统保证集群一致性,这才是Raft等一致性算法要真正解决的问题。

一、日志复制的一致性隐患

接着上篇的内容继续聊,Raft通过一致性检查,能在一定程度上保证集群的一致性,但无法保证所有情况下的一致性,毕竟分布式系统各种故障层出不穷,如何在有可能发生各类故障的分布式系统保证集群一致性,这才是Raft等一致性算法要真正解决的问题,来看Raft论文中给出的经典案例:

001.png

上图展示了第八个任期中,新Leader刚上任的集群情况,一眼望过去,大家会发现集群的日志序列混乱不堪,最上面的则的Leader的日志序列,而下面则列举了六种Leader上线可能遇到的混乱场景,其中有的多了部分日志,有的少了部分日志,为什么会造成这些现象呢?下面来逐个分析下。

PS:上图并不是一个集群,而是列出了六种Leader上线有可能遇到的混乱场景!

1.1、日志不一致场景分析

情况一:a、b比Leader少了一部分日志。这种现象经过前面的内容讲解后,其实很容易观察出问题,即Follower-a在收到term6,index9日志后掉线,Follower-b节点在收到term4,index4后掉线,从而导致两种日志落后于新Leader的场景出现。

情况二:c比Leader多了一个term6中的日志。造成这种情况的原因,是由于term6leader,刚向Follower-c发出term6,index11的日志,还没来得及给其他节点同步就发生了故障,然后开启了term7的选举,term7这轮任期,有可能没选出Leader,又或者leader刚上线没多久就挂了,所以图中的leader才会直接成为term8的领导者。

情况三:d比Leader多了term7的日志。图中的dleader多出两个term7的日志,这种情况也很好解释,即term7leader节点刚上线没多久,只将term7,index11term7,index12两条日志同步给了Follower-d,然后就挂掉了,此时就造就了d场景出现。

情况四:e比Leader多了两个term4的日志,少了term5、term6的日志。这种情况是term4的领导者,在提交完term4,index4~5日志后,刚将term4.index6~7日志同步给Follower-e,接着就挂掉了,因此eterm8leader多两个term4的日志。同时,e在收到term4,index6~7两个日志后也掉线了,所以term5~6的日志全都未同步。最后,图中leader身处term8,意味着term7没选出leader,或term7的领导者在任时间很短暂。

情况五:f比leader多了term2-3的日志,少了term4-6的日志f拥有的term2~3日志在leader上不存在,这就只能说明term2、3两个任期中,原leader只将日志同步给了Follower-f,然后就掉线了,而f在收到term3,index11这条日志后,也发生了故障从而掉线,再次恢复时,集群已经推进到term8这个任期。为此,f才会多出term2~3、缺少term4~6的日志。

好了,上面捋清楚了Raft论文中列出的五种混乱现象,造成这些现象的原因很简单,因为现实场景中节点发生故障的时间不可控,任意时间某个FollowerLeader掉线后,都会导致其日志序列与其他节点脱节,再次恢复后,其日志序列就会和最新的Leader存在差异。那么,Raft协议究竟是如何解决这么多种复杂场景的呢?

1.2、Raft如何解决一致性隐患?

其实Raft解决问题的手段很简单粗暴,在之前说过:Raft是一种强Leader的一致性协议,一旦某个节点成为Leader,那么在其任职期间,它将拥有集群中至高无上的权力!正因如此,当集群节点出现日志序列不一致问题时,Raft会强制要求存在不一致的Follower节点,直接复制在任Leader的日志序列来保持一致性

简单来说,就是当新Leader上任后,发现集群存在与自身序列不一致的Follower节点时,会使用自身序列中的日志,覆盖掉Follower节点中不一致的日志(Leader从不会丢弃自己序列里的日志)。当然,Leader是如何发现集群中存在不一致的Follower呢?大家还记得上面聊的一致性检查机制嘛?

Leader在每次发出AppendEntries-RPC时,都会携带自身上一个日志的任期号、日志下标,如果集群里存在不一致的Follower节点,在接受PRC必然无法通过一致性检查,通过这种机制,就能确认集群各节点的日志是否与自身一致。

同时,Leader针对每个Follower节点,都会维护一个索引,即Next-Index,从其命名也能轻易得知其作用,就是用来记录下一个要发往Follower的日志下标,在Leader刚上任时,Next-Index默认为自己最后一个日志的下标加一。

结合前面聊到的一致性检查机制,当集群存在不一致的Follower时,Leader发出的AppendEntries-RPC就无法通过一致性检查,此时Leader上维护的对应Next-Index会减一,经过不断归纳验证,总能找到两者最后达成一致的日志位置,接着会将之后所有不一致的Log全部覆盖。当然,碰到极端场景,Next-Index可能会变成1,即Follower上的所有日志都需要重新复制。

二、Raft安全性(Safety)

上面的内容讲述了领导者选举和日志复制两个核心问题,但仅靠这两方面并不能保证集群的正确性,在很多情况下,集群仍然存在不一致风险!比如上阶段末尾提到的一致性隐患问题,尽管Raft通过强Leader特性,结合一致性检查机制,解决了提到的多种混乱场景,可这种方案真的万无一失吗?就目前而言并不够,因为到目前为止,当Leader掉线后,只要任意节点得到了大多数节点的投票支持,就有可能成为集群的新Leader,如下图所示:

002.png

目前集群LeaderS1,假设突然掉线,如果S3率先感知到,根据之前描述的选举机制,S3则有最大概率成为新Leader,此时问题来了,S3的日志明显落后于其他节点,一旦它成为新主,结合刚才所说的一致性方案,就会造成所有节点的日志回退到index4这个位置(因为Leader会覆盖掉Follower上不一致的日志)。

上面所说的这种情况,显然并不合理,因为之前的日志已经提交到index11,一旦S3上任将之前的日志覆盖,就会导致客户端读不到之前已经写入成功的数据,这对集群而言,无疑是一种不可容忍的错误。为此,想要保证集群的正确性,在做领导者选举时,必须得加些额外限制,而这就是接下来要聊到的安全性保障!

2.1、选举机制的安全性保障

仔细分析前面的问题,其实会导致集群发生不可逆转的错误,根本原因在于:没有设立成为新Leader的门槛,类比到生活,如果你部门的一把手离职,一个刚入职的应届生能否坐上他的位置呢?显然不能,因为想要坐上那个位子,有着一系列隐形门槛,如经验、为人处世等,而想要解决上面提到的问题,选举时加个限制即可。

和之前一样,率先感知到Leader掉线的节点,依旧能最快成为候选者,但它能否成为新一轮的Leader,这并不能保证,因为Raft对选举加了一条安全性限制:Candidate发出RequestVote-RPC拉票时,必须携带自己本地序列中最新的日志(term,index),当其他Follower收到对应的拉票请求时,对比其携带的日志,如果发现该日志还没有自己的新,则会拒绝给该候选人投票

将这个门槛套进前面的例子中,当S3感知到S1掉线后,尽管它最先发起拉票,可S2、S4、S5的日志都比它要新,所以不会有任何节点给它投票,就无法满足“大多数”这个条件,S3就必然无法成为新Leader。反之,如果一个节点成为了新Leader,那么它一定得到了集群大多数节点的支持,也就意味着它的日志一定不落后于大多数节点

对比的规则:如果任期号(term)不同,任期号越大的日志越新;如果任期号相同。日志号(index)越大的越新

OK,再来看前面的例子,如果不考虑超时机制的情况下,谁最有可能成为新Leader?显然是S5节点,因为它具备最全的日志!那再来看个问题,如果S3拉票失败后,S4率先超时,此时它发起拉票请求,任期被推进到7S3虽然拉票失败,但它自增的任期会保留),S4能否有机会成为新Leader呢?如下:

003.png

此时来看,S4开始向其余节点拉票时,自身最新的日志为term5,index11,各节点的回应如下:

  • S1已掉线,不会响应拉票请求;
  • S2本地最新的日志为term4, index8,会投一票;
  • S3本地最新的日志为term2, index4,会投一票;
  • S5本地最新的日志为term5, index12,会拒绝投票。

此时来看,尽管拥有最新日志的S5节点拒绝投票,可S2、S3节点的两票,再加上S4自身的一票,依旧能让S4的票数满足“大多数”这个条件,为此,S4毋庸置疑会成为term7的新Leader。有人或许会疑惑,S5S4日志要新呀,如果S4当选Leader,和S5节点是不是存在一致性冲突呀?没错,可是这并不影响集群的一致性,Why?大家仔细来看这张图:

004.png

其实身为原LeaderS1掉线时,集群内日志只提交到了index11这个位置,而S5节点多出来的index12这条日志,实则并未被提交,因为它并未被复制到大多数节点,所以S1也不会向客户端返回“操作成功”,这意味什么?意味着S1节点的term5,index12这条日志可以被丢弃,即使后续被覆盖了,也并不会影响客户端的“观感”,毕竟这条日志对应的b←2操作,在客户端的视角里,本来就没写入成功。

好了,说到这里大家应该也明白了,为什么封装的客户端操作日志,至少要在集群大多数节点同步完成后,才能向客户端返回操作成功的根本原因!就是因为在做领导者选举时,只要一条日志被复制到了大多数节点,那么这些已提交的日志,在选举出来的新Leader上就一定存在,这也是Raft为何能保证日志一旦提交,就一定会被Apply到状态机、且永远不会丢失的原因

2.2、日志复制的安全性保障

上小节讲到了Raft对领导者选举增加的安全性限制,可如若你觉得已经彻底解决了一致性问题,那就大错特错!再来看一个Raft论文中给出的经典问题:

005.png

上图描述了一个日志提交带来的一致性冲突问题,图中是由五个节点组成的集群,并且所有节点已具备term1,index1这条日志,而后从左到右按时间顺序描述了问题的背景,分为五个阶段。

阶段a中,S1是集群的Leader,此时客户端发来了一个操作,S1将其封装为对应的日志(term2.index2),可是刚复制给S2后,S1发生故障从而掉线。

阶段b中,S5率先超时,并获得S3、S4、S5的三票,成为term3的新Leader,然后客户端也发来了一个操作,S5刚将其封装成term3,index2这条日志,还未来得及同步,就发生了故障。

阶段c中,S1已经恢复,且最先超时,S1获得S1、S2、S3、S4四票,重新成为term4Leader,于是继续向S3复制之前的term2,index2日志,S3复制成功,此时这条日志满足了“大多数”条件,S1将其提交(commit)。

阶段d中,S1又掉线,S5恢复并携带着自身最新的日志(term3,index2)开始拉票,根据之前的对比原则,任期号越大日志越新,所以S5能获得S2、S3、S4、S5四票,从而再次成为term5Leader。这时,S5term3,index2复制给所有节点并提交。

大家注意看,在阶段d中,S5term3,index2复制给所有节点时,其实在此之前,term4中的S1,已经将term2.index2提交了,因为阶段c时,这条日志已经在S1、S2、S3完成复制。而阶段dLeaderS5,根据之前的原则:Leader在当前阶段中拥有最高的权力,有权覆盖掉与自身不一致的日志,为此,term2,index2就会被term3,index2覆盖。

问题就出在这里,term2.index2已经被提交,对客户端而言是可见的,可是到了阶段d,这条日志又被覆盖,最终又给集群带来了无法容忍的致命错误!怎么解决呢?Raft仅对日志提交加了一个小小的限制:Leader只允许提交(Commit)包含当前任期的日志

值得注意的是,这条限制里说的是“只允许提交包含当前任期的日志”,而不是“只允许提交当前任期的日志”!啥意思?套进前面的例子中,导致不一致问题出现的时间为阶段c,因为此阶段对应的任期为term4,可是却提交了term2,index2这个第二轮任期的日志,所以造成了不一致冲突。

可之前又提交过,Leader永远不会丢弃自身的日志,那么term2,index2这条日志什么时候会被提交呢?需要等到S1收到term4的操作后,并将其封装成日志复制到大多数节点时,与term4的日志一起提交。通过这条限制,term2,index2就会跟随term4的日志一起提交,如果S1担任term4的领导者期间,并未出现任何一条客户端操作,那么term2,index2就永远不会被提交。

好了,结合上述限制,再来看到阶段e,如果S1任职的term4中出现了新的客户端操作,那么term2,index2会随着term4,index3这条一同被提交,这时就算S1掉线,S5也无法成为新的Leader,因为S2、S3的日志都比它新,所以S5永远无法满足“大多数”这个选举条件。

反之,如果term4中没有客户端操作到来,term2,index2就不会被提交,这时担任term5领导者的S5,就算将这条日志覆盖,也不会对客户端造成不一致的观感,因为未提交的日志不会应用于状态机。

2.3、领导者选举时的细节问题

好了,前面已经将Raft分解出的领导者选举、日志复制、安全性这三个子问题阐述完毕,下面来讨论一个选举时的细节问题:

目前由S1、S2、S3、S4、S5五个节点组成集群,现任LeaderS5S5如果在发出心跳后,由于S2节点网络较差,导致接收心跳包出现延迟,从而造成S2的随机选举时间出现超时,然后发起一轮新选举怎么办?

这时S5是否会被S2替换掉呢?这个问题要结合前面所有知识来分析,因为发起新一轮选举会自增任期号,而Follower在投票时,如果发现对方的任期号要比自身大,且日志不小于自身最新的日志,就会为其投票。假设这时S2具备最新的日志,尽管原本身为LeaderS5节点很正常,S2也会成为新Leader

有人或许会说,在这种情况下,是S2、S5之间的网络存在波动、不稳定导致的,S2就将正常的S5节点挤下线,这太不公平了!的确有点不公平,但却无伤大雅,毕竟作为新LeaderS2,也具备完整的已提交日志,并不会影响集群正常运行。

同时,如果是S2自身的网络一直存在问题,比如网络带宽延迟较高,网络传输速度不够稳定等等,那么它肯定不会有机会当选Leader!为啥?因为网络存在问题的节点,永远不可能具备最新的日志,毕竟Raft也是基于网络来发送RPC,如果一个节点网络本身有问题,那么其同步日志的效率必然很缓慢。

三、Raft日志压缩机制

前面已将Raft算法的核心内容阐述完毕,但如果想要将其应用实际的工程中,那么还需要考虑一些现实因素带来的问题,首先来看看日志增长的问题。因为Raft是基于日志复制工作的一致性算法,并且该算法主要服务于分布式存储的集群领域,任何一款分布式存储组件,一旦将其部署后,持续运行的时间必然不短。

也正因如此,Raft要面临的并非单次、几次一致性决策,而是数以几百万、几千万,甚至几十亿次决策,在上面讲述的内容中,Raft为了让一个客户端的操作,在集群内达成一致,会先由Leader将其封装成日志条目,接着同步给其余节点。那么,我们可以将这个关系简单描述为:一次决策等于一条日志

既然集群的长时间运行会触发无数次决策,这代表着对应的日志会呈现无上限式增长,而现实中的硬件设施并不支持这么做,毕竟一台机器的存储空间再大,也总会有被存满的一天,无限制的日志增长,会占用不可预估的存储空间。其次,Raft状态机依赖于日志,这就意味着当机器重启时,又或者新的节点加入集群,需要重放之前的所有日志,才能将拥有集群最新的数据,这无疑会极大程度上拖慢集群的可用性。

综上,如何控制日志的无限增长,这成为了Raft在工程实践中第一道坎,而这个问题也是许多分布式存储组件面临的问题,对应成熟的解决方案叫做:日志压缩技术

3.1、什么是日志压缩?

压缩技术相信大家都有所接触,日常传输一个文件时,如果源文件较大,必然会影响传输效率,为了缩短传输时间,大家都会将其打成.zip、.rar、.tar等格式的压缩包。同理,这个思想也可以用于Raft中,当日志序列较大时,我们可以通过压缩技术对其进下瘦身工作。

可是,传统的压缩技术并不能解决Raft所面临的难题,因为传统的压缩技术,最多只能在原大小的基础上“瘦身”30~40%,这对无限增长的日志而言用处不大。因此,该如何有效解决客户端持续性操作,带来的日志无限增长问题呢?答案很简单,依靠Snapshot快照技术

Snapshot快照技术是编程领域最常用、最简单的日志压缩机制,Zookeeper、Redis底层都有用到此技术。快照就是将系统某一时刻的状态Dump下来,在此之前的所有操作日志都可以舍弃。这是啥意思呢?来看个例子:

006.png

上图左边是四条日志,而右边则是这四条日志对应的快照文件,很明显,诸位会发现快照比日志“小”了许多,为什么呢?因为左边的四条日志,依次Apply于状态机后,得到的最终结果就是x=5,所以,我们只需要保留最终的结果,从而达到缓解无限制增长带来的存储压力。

注意:我们可以把日志序列压缩成一个快照文件,但却无法根据快照文件提取出原本的日志序列。

3.2、Raft快照技术

经过上小节讲述大家会发现,所谓的快照技术,就是“省略过程,保留结果”的产物,当然,因为客户端操作会一直持续,因此,只要系统还在运行,就始终无法得到一个永久有效的快照文件,为了尽可能减小日志增长带来的额外空间压力,我们需要定期保留系统某个时刻的快照,再来看个例子:

007.png

这是一个包含多任期的日志序列,上图描述了term2~5、index1~12转换出的快照文件,实际上就是“当下时刻”的状态机。如果对Redis-AOF日志重写机制较为熟悉的小伙伴,看这个例子同样会异常亲切,毕竟它两之间有着异曲同工之妙。RedisAOF日志,记录着自启动后、运行期间内收到的所有客户端操作指令,为了有效解决无限制增长的难题,当AOF文件大小达到一定阈值后,Redis会对其进行重写。

重写AOF日志,就是对其进行一次压缩,重写动作发生时,会先生成当下时刻的内存快照,而后将快照中的每个数据,反向生成出每个数据的写入指令,接着不断追加到新的AOF文件中,当快照文件全部被转换为AOF指令后,最后就能用新的AOF覆盖原本体积较大的AOF文件。

Raft日志压缩亦是同理,但Raft并不会用快照反向生成日志序列,而是只留存快照文件、丢弃生成快照之前的日志,所以,一个快照文件中包含两种信息:

  • ①生成快照时,当前Leader节点的状态机(数据);
  • ②生成快照时,最后一条被应用于状态机的日志元数据(term、index)。

第一种信息比较好理解,而第二种信息主要是为了兼容原有的日志复制功能,比如当一个新加入的节点,需要很早同步之前的日志,这时可能已经被压缩成了快照文件,就可以根据快照的日志元术据来判断,如果该节点需要的日志,要老于生成快照时,最后一条被Apply到状态机的日志,这时Leader可以把整个快照发给新节点(这种方式还能减少重放日志带来的耗时)。

PS:同步快照并不是通过AppendEntries-RPC完成,而是通过另一种新的InstallSnapshot-RPC来实现。

最后,由于运行期间内会不断生成新的快照,而每当生成一个新的快照文件时,在之前的老快照文件都可以被舍弃,因为新的快照文件总能兼容旧的快照文件,如果新快照比旧快照少了部分数据,这只能说明两次快照间隔期间,客户端出现“删除”操作,因此少的那部分数据也并不重要。

四、Raft动态伸缩机制

聊完日志压缩技术后,下面来看看另一个较为核心的问题,即集群成员变更机制,一套系统部署后,没有人能保证部署这套系统的机器一直正常,在现实场景中,往往会因为诸多因素,造成集群成员出现变更,比如原本集群中的A节点,因为所在的机器硬件设施太落后了,所以有一天想要使用配置更优的D来取代它,这就是一种典型的集群成员变更。

除上述情况外,在如今云技术横行的时代,为了拥抱各种不定性的业务场景,许多云平台都提供了弹性扩容、动态伸缩等机制的支持,那如果一种采用Raft的技术部署在云环境中,由于业务访问量突然暴增,触发了云平台的弹性扩容机制,将原本的3节点规模,提升到5节点规模,这时就会多出两个新节点,而这也是一种成员变更的情况(节点收缩亦是同理)。

正如上面所说,Raft想要真正在工程中实践,如果不去考虑成员变更的问题,那就只能如“旧时代”的模式一样,一旦集群要发生成员变更,就先停止整个集群,接着人工介入完成成员变更,最后重新启动整个集群。这种方式很简单,不过最大的问题是:系统在变更期间必定不能对外服务,这个苛刻的条件对许多大型系统而言是无法容忍的。

怎么办?不用担心,Raft的作者也想到的这个问题,因此在论文中也给出了一种运行期间内、自动完成成员变更的机制,也就是将集群成员变更的信息,也封装成一种特殊的日志(Configuration Log Entry),再由Leader同步给集群原本的其他节点,下面来展开聊聊。

4.1、集群成员变更造成的脑裂问题

在聊Raft提供的成员变更机制之前,我们先来看看集群中经典的脑裂问题,所谓脑裂,即是指中心化的集群中,同一时刻出现了两个Leader/Master节点,脑裂问题通常发生于网络分区场景中,而集群成员变更则是最容易导致网络分区产生的一类场景,来看具体例子:

008.png

上图是Raft论文中给出的一张集群成员变更图,不过较为抽象,有点难以让人理解,所以我们可以将其拆解为如下四个阶段:

009.png

上图演示了对三个节点组成的集群,动态扩容两个节点后遇到的成员变更情况,其中也逐步说明了脑裂问题的产生,我们依旧按时间顺序,从左到右挨个讲解。

在第一阶段中,集群由S1、S2、S3三个节点组成集群,其中S3为现阶段的Leader,当然,在Raft论文中,这组配置被称之为C-old,代表老集群的节点配置。

到了第二阶段,集群扩容S4、S5两个节点,集群出现成员变更场景,此次变更仅告知给了身为领导者的S3节点,再由S3同步给原集群中的S1、S2两个节点(目前集群变成S1~S5五个节点组成,这组新的节点配置称为C-new)。

来到第三阶段,此时S3还未来得及将S4、S5已加入集群的消息同步给S1、S2S3就突然出现短暂的故障(如网络不可用),导致其未及时向集群所有节点发送心跳,最终引发S1、S5两个节点的超时,S1、S5各自发起新一轮选举。

在第四阶段里,因为S1还不知道集群节点已经增加到了五个(S3未来得及告知),所以它只会向S2、S3节点拉票,又因为S3是原本的主节点,S1的日志肯定不可能比S3要新,因此S3会拒绝给S1投票,而S2会投出自己的一票,此时再加上S1持有自身的一票,顺理成章当选新Leader

S5作为新加入的节点,它已知现在集群里有五个节点,所以会同时向S1~S4发起拉票,因为S5S3具备相同的日志(S3宕机前的最后一条日志,就是S4、S5加入集群),所以S3会将自己的一票投给S5,而作为一同加入集群的S4节点,也必然会将票数投给S5,此时加上S5自身的一票,总共获得三票,满足“大多数”这个选举条件,最终S5也会宣告自己是新Leader

经过第四个阶段后,大家会发现此时集群中出现了S1、S5两个Leader,这就是经典的脑裂问题,也是任何主从复制集群零容忍的致命错误。到这里,我们讲述清楚了脑裂问题的产生背景,那Raft中是如何解决这个致命错误的呢?

4.2、Raft的联合共识变更机制

首先记住,因为成员变更也需要依靠日志同步机制,来告知给所有的Follower节点,而日志同步需要借助网络发送RPC,所以旧集群中的所有节点,不可能在同一时刻共同感知集群成员发生了变更。正因如此,在Follower同步成员变更日志这个期间,就可能会存在“不同节点看到的集群配置(视图)不一样”的情况,如果这期间Leader发生故障,或许就会引发脑裂情况发生。

Raft论文中表明:任何直接将集群从C-old(旧配置)直接切换成C-new新配置的方式都不可靠,即直接切换都有可能导致脑裂现象,为此,Raft提出一种两阶段式的成员变更机制,这种机制在论文中被称为:联合共识(Joint Consensus)策略,该策略对应的两个阶段为:

  • 阶段一:由Leader先将发生集群成员变更的消息通知给所有节点;
  • 阶段二:等大多数节点都收到成员变更的消息后,再正式切换到新的集群配置。

先来细说一下阶段一中的具体流程:

  • ①客户端触发成员变更动作,先将C-new发给Leader节点,LeaderC-old、C-new两组配置中取并集,表示为C-old,new
  • Leader将新旧两组集群配置的并集C-old,new,封装成特殊的日志同步给所有Follower节点;
  • ③当大多数Follower收到并集后,Leader将该并集对应的日志提交。

这是第一个阶段的流程,稍微说明一下其中的并集概念:

并集:由两个或多个集合之间,所有非重复元素所组成的集合;

比如{A,B,C}{D,E}的并集就为{A,B,C,D,E},而{A,B,C}{A,C,D}的并集则为{A,B,C,D},在Raft算法中,C-old、C-new这两组节点配置可以视为两个集合,两者的并集则被表示为C-old,new

接着来聊下阶段二的详细流程:

  • C-old,new的日志提交后,Leader继续将C-new封装为日志同步给所有Follower节点;
  • ②一个Follower收到C-new后,如果发现自己不在C-new集合中,就主动从集群中退出;
  • ③当大多数节点都将C-new同步完成后,代表集群正式切换到新配置,Leader向客户端返回变更成功。

注意看这个过程,在大多数节点收到并集的日志后,Leader就会着手将集群切换到新的节点配置,主要看第二步操作,如果一个Follower收到C-new日志后,发现自己并不在节点列表中,这就说明本次成员变更,自己就是要被替换掉的一员,因此当前节点就需要从集群主动退出,从而让集群从旧配置切换到新配置。

010.png

这仍是一张摘自Raft论文的原图,其中描述了整个Joint Consensus的过程,我们再来分析下基于联合共识实现成员变更后,是否还存在脑裂问题。

4.3、Joint Consensus为什么能避免脑裂?

大家一起分析下,在整个联合共识的过程中,集群Leader在哪些时间点可能掉线呢?

  • Leader收到客户端的成员变更操作后,还未取得并集就掉线;
  • ②并集C-old,new的日志还未提交,Leader掉线;
  • ③并集C-old,new的日志已提交,C-new还未封装成日志,Leader掉线;
  • C-new还未提交,即日志只在少数节点完成同步,Leader掉线;
  • C-new在大多数节点同步成功,日志已提交,Leader掉线。

上述列出了集群成员变更时,所有Leader可能掉线的时间节点,接着对其逐个分析下。

先来看第一种情况,因为Leader刚收到成员变更操作,都还未来得及取新旧两组配置的并集就挂了,这时集群所有Follower节点必然都只能看到C-old配置,所以这种情况不可能选出两个新Leader

说明:所谓的“看到”,就是指某个节点所身处的配置,比如A能看到C-old,代表A在老的集群配置中。

再看第二种情况,并集C-old,new的日志还未提交,意味着Leader已经提取出了新旧配置的并集,并且封装成C-old,new日志并开始向其他节点同步,但日志还未提交,说明集群内只有少数节点同步成功,等价于集群内有少数节点能看到C-old,new这组并集配置,还有大多数节点只能看到C-old旧配置。可不管并集配置也好,旧配置也罢,实则都包含C-old这组旧节点列表,意味着任何一个节点想成为新Leader,必须获得C-old中大多数节点的认可。

一个新加入集群的节点,能否拿到C-old里的大多数投票呢?答案是不能,因为C-old,new日志还未提交,这意味着集群大多数Follower节点,并不知道有新节点加入,那它们必然不会将票数投给新加入的节点,因此,新加入集群的节点肯定没机会在这种情况下成为新Leader

接着看到第三种情况,C-old,new已提交、C-new未封装成日志,这代表集群大多数节点都能看到C-old,new配置,这时Leader,一个节点触发超时选举后,能成功变为新Leader的节点,肯定已经同步C-old,new日志,为什么?因为大多数节点已同步此日志,没有同步该日志的节点拉票会被拒绝,为此,这种情况同时只会有一个节点拿到C-old,new的大多数投票,也只会产生一个Leader

在第四种情况中,C-new还未提交,代表Leader已经开始将C-new日志同步给其他节点,但只有少数节点完成了同步,接着Leader挂掉。这时,如果只能看到C-old的节点发起拉票,肯定无法满足大多数,因为目前大多数节点都已经能看到C-old,new,这意味着新Leader必须要获得大多数C-new的票选才能胜任。

最后看到第五种情况,C-new日志已经提交,那代表集群已经切换到了新配置,大多数节点都能看到C-new,这时Leader掉线,新Leader也只能从C-new里选出来,同一轮任期中,也只会有一个节点拿到C-new的大多数投票,自然就不存在多Leader出现的场景。

综上所述,Raft通过取C-old、C-new的并集,来作为成员变更期间的过渡,如果Leader在成员变更期间宕机,根据不同的时间点,上任新Leader的节点,需要满足C-old、C-new两组配置中的联合共识,这就是Raft中的Joint Consensus策略!

五、Raft算法总结

截止到现在,我们围绕着Raft算法,从起初的一致性定义,到领导者选举、日志同步、集群安全性三个子问题,再到后来的日志压缩、集群成员变更等进行了全面剖析。相较于前面聊的Paxos算法,Raft显得更加成熟与完善,它补全了Paxos算法里存在的许多不足之处,整个算法过程,包括各处细节都经得起推敲,这也是为何如今越来越多开源组件转身拥抱Raft的原因。

当然,在一致性领域,Raft属于后起之秀,Raft能做到这种程度,主要还是因为它站在了“巨人的肩膀上”,算法立项之初,就容纳了百家之长。或许,很多人或许会觉得它比Paxos更难,原因很简单,毕竟它比Paxos考虑的细节问题要多出很多,学起来知识密度会更大,但对比PaxosRaft定义概念并没有那么抽象,并且各个子问题划分的十分明确,一点点啃下来之后,其实会发现比Paxos要更容易令人接纳~

PS:关于其他较为重要的一致性落地实现,如霸道的Bully选举算法、Zookeeper中的ZAB协议、Redis中应用广泛的GossIP协议等,我们暂时不做分析,后续大家感兴趣再出单独的章节撰写。

最后,看完最近几篇一致性相关的章节,有的人可能会感觉,这好像都是针对主从架构的算法呀,现在都是纯分布式(分片模式)的组件,谁还会用这些算法呢?其实数之不尽,但凡有热备机制的存储组件,内部必然保证了一致性,例如当下分布式系统都离不开的注册中心等等,因此掌握这些著名的一致性知识,也是每位技术人绕不开的坎,只有明白这些底层理论后,才能帮助大家更好的理解技术原理,从而脱离“只会用”的新手阶段!

所有文章已开始陆续同步至微信公众号:竹子爱熊猫,想在微信上便捷阅读的小伙伴可搜索关注~

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
算法 Go
[go 面试] 雪花算法与分布式ID生成
[go 面试] 雪花算法与分布式ID生成
|
16天前
|
算法 关系型数据库 MySQL
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
在分布式系统中,确保每个节点生成的 ID 唯一且高效至关重要。Snowflake 算法由 Twitter 开发,通过 64 位 long 型数字生成全局唯一 ID,包括 1 位标识位、41 位时间戳、10 位机器 ID 和 12 位序列号。该算法具备全局唯一性、递增性、高可用性和高性能,适用于高并发场景,如电商促销时的大量订单生成。本文介绍了使用 Go 语言的 `bwmarrin/snowflake` 和 `sony/sonyflake` 库实现 Snowflake 算法的方法。
27 1
分布式唯一ID生成:深入理解Snowflake算法在Go中的实现
|
28天前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
28天前
|
分布式计算 Java 开发工具
阿里云MaxCompute-XGBoost on Spark 极限梯度提升算法的分布式训练与模型持久化oss的实现与代码浅析
本文介绍了XGBoost在MaxCompute+OSS架构下模型持久化遇到的问题及其解决方案。首先简要介绍了XGBoost的特点和应用场景,随后详细描述了客户在将XGBoost on Spark任务从HDFS迁移到OSS时遇到的异常情况。通过分析异常堆栈和源代码,发现使用的`nativeBooster.saveModel`方法不支持OSS路径,而使用`write.overwrite().save`方法则能成功保存模型。最后提供了完整的Scala代码示例、Maven配置和提交命令,帮助用户顺利迁移模型存储路径。
|
2月前
|
算法
基于粒子群算法的分布式电源配电网重构优化matlab仿真
本研究利用粒子群算法(PSO)优化分布式电源配电网重构,通过Matlab仿真验证优化效果,对比重构前后的节点电压、网损、负荷均衡度、电压偏离及线路传输功率,并记录开关状态变化。PSO算法通过迭代更新粒子位置寻找最优解,旨在最小化网络损耗并提升供电可靠性。仿真结果显示优化后各项指标均有显著改善。
|
2月前
|
算法
raft算法的自我理解
本文介绍了Raft算法的基本概念和工作原理,包括它如何通过日志复制和领导选举来实现分布式系统中不同机器的强一致性。
29 2
|
2月前
|
消息中间件 缓存 算法
分布式系列第一弹:分布式一致性!
分布式系列第一弹:分布式一致性!
|
2月前
|
算法 Java 关系型数据库
漫谈分布式数据复制和一致性!
漫谈分布式数据复制和一致性!
|
4月前
|
Oracle 关系型数据库
分布式锁设计问题之Oracle RAC保证多个节点写入内存Page的一致性如何解决
分布式锁设计问题之Oracle RAC保证多个节点写入内存Page的一致性如何解决
|
4月前
|
消息中间件 存储 监控
消息队列在分布式系统中如何保证数据的一致性和顺序?
消息队列在分布式系统中如何保证数据的一致性和顺序?