掘金 后端 ( ) • 2024-05-07 13:53

theme: channing-cyan

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

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

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

日志混乱

上图展示了第八个任期中,新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,如下图所示:

选举风险

目前集群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呢?如下:

S4拉票

此时来看,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?大家仔细来看这张图:

已提交的日志

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

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

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

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

日志提交带来的不一致冲突

上图描述了一个日志提交带来的一致性冲突问题,图中是由五个节点组成的集群,并且所有节点已具备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下来,在此之前的所有操作日志都可以舍弃。这是啥意思呢?来看个例子:

快照机制

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

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

3.2、Raft快照技术

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

Raft快照

这是一个包含多任期的日志序列,上图描述了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节点,脑裂问题通常发生于网络分区场景中,而集群成员变更则是最容易导致网络分区产生的一类场景,来看具体例子:

Raft成员变更

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

集群成员变更

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

在第一阶段中,集群由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日志后,发现自己并不在节点列表中,这就说明本次成员变更,自己就是要被替换掉的一员,因此当前节点就需要从集群主动退出,从而让集群从旧配置切换到新配置。

联合共识

这仍是一张摘自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协议等,我们暂时不做分析,后续大家感兴趣再出单独的章节撰写。

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

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