一、日志复制的一致性隐患
接着上篇的内容继续聊,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中的日志。造成这种情况的原因,是由于term6
的leader
,刚向Follower-c
发出term6,index11
的日志,还没来得及给其他节点同步就发生了故障,然后开启了term7
的选举,term7
这轮任期,有可能没选出Leader
,又或者leader
刚上线没多久就挂了,所以图中的leader
才会直接成为term8
的领导者。
情况三:d比Leader多了term7的日志。图中的d
比leader
多出两个term7
的日志,这种情况也很好解释,即term7
的leader
节点刚上线没多久,只将term7,index11
、term7,index12
两条日志同步给了Follower-d
,然后就挂掉了,此时就造就了d
场景出现。
情况四:e比Leader多了两个term4的日志,少了term5、term6的日志。这种情况是term4
的领导者,在提交完term4,index4~5
日志后,刚将term4.index6~7
日志同步给Follower-e
,接着就挂掉了,因此e
比term8
的leader
多两个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
论文中列出的五种混乱现象,造成这些现象的原因很简单,因为现实场景中节点发生故障的时间不可控,任意时间某个Follower
或Leader
掉线后,都会导致其日志序列与其他节点脱节,再次恢复后,其日志序列就会和最新的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
,如下图所示:
目前集群Leader
是S1
,假设突然掉线,如果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
率先超时,此时它发起拉票请求,任期被推进到7
(S3
虽然拉票失败,但它自增的任期会保留),S4
能否有机会成为新Leader
呢?如下:
此时来看,S4
开始向其余节点拉票时,自身最新的日志为term5,index11
,各节点的回应如下:
S1
已掉线,不会响应拉票请求;S2
本地最新的日志为term4, index8
,会投一票;S3
本地最新的日志为term2, index4
,会投一票;S5
本地最新的日志为term5, index12
,会拒绝投票。
此时来看,尽管拥有最新日志的S5
节点拒绝投票,可S2、S3
节点的两票,再加上S4
自身的一票,依旧能让S4
的票数满足“大多数”这个条件,为此,S4
毋庸置疑会成为term7
的新Leader
。有人或许会疑惑,S5
比S4
日志要新呀,如果S4
当选Leader
,和S5
节点是不是存在一致性冲突呀?没错,可是这并不影响集群的一致性,Why
?大家仔细来看这张图:
其实身为原Leader
的S1
掉线时,集群内日志只提交到了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
四票,重新成为term4
的Leader
,于是继续向S3
复制之前的term2,index2
日志,S3
复制成功,此时这条日志满足了“大多数”条件,S1
将其提交(commit
)。
阶段d
中,S1
又掉线,S5
恢复并携带着自身最新的日志(term3,index2
)开始拉票,根据之前的对比原则,任期号越大日志越新,所以S5
能获得S2、S3、S4、S5
四票,从而再次成为term5
的Leader
。这时,S5
将term3,index2
复制给所有节点并提交。
大家注意看,在阶段d
中,S5
将term3,index2
复制给所有节点时,其实在此之前,term4
中的S1
,已经将term2.index2
提交了,因为阶段c
时,这条日志已经在S1、S2、S3
完成复制。而阶段d
的Leader
是S5
,根据之前的原则: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
五个节点组成集群,现任Leader
是S5
,S5
如果在发出心跳后,由于S2
节点网络较差,导致接收心跳包出现延迟,从而造成S2
的随机选举时间出现超时,然后发起一轮新选举怎么办?
这时S5
是否会被S2
替换掉呢?这个问题要结合前面所有知识来分析,因为发起新一轮选举会自增任期号,而Follower
在投票时,如果发现对方的任期号要比自身大,且日志不小于自身最新的日志,就会为其投票。假设这时S2
具备最新的日志,尽管原本身为Leader
的S5
节点很正常,S2
也会成为新Leader
。
有人或许会说,在这种情况下,是S2、S5
之间的网络存在波动、不稳定导致的,S2
就将正常的S5
节点挤下线,这太不公平了!的确有点不公平,但却无伤大雅,毕竟作为新Leader
的S2
,也具备完整的已提交日志,并不会影响集群正常运行。
同时,如果是
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快照技术
经过上小节讲述大家会发现,所谓的快照技术,就是“省略过程,保留结果”的产物,当然,因为客户端操作会一直持续,因此,只要系统还在运行,就始终无法得到一个永久有效的快照文件,为了尽可能减小日志增长带来的额外空间压力,我们需要定期保留系统某个时刻的快照,再来看个例子:
这是一个包含多任期的日志序列,上图描述了term2~5、index1~12
转换出的快照文件,实际上就是“当下时刻”的状态机。如果对Redis-AOF日志重写机制较为熟悉的小伙伴,看这个例子同样会异常亲切,毕竟它两之间有着异曲同工之妙。Redis
的AOF
日志,记录着自启动后、运行期间内收到的所有客户端操作指令,为了有效解决无限制增长的难题,当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
论文中给出的一张集群成员变更图,不过较为抽象,有点难以让人理解,所以我们可以将其拆解为如下四个阶段:
上图演示了对三个节点组成的集群,动态扩容两个节点后遇到的成员变更情况,其中也逐步说明了脑裂问题的产生,我们依旧按时间顺序,从左到右挨个讲解。
在第一阶段中,集群由S1、S2、S3
三个节点组成集群,其中S3
为现阶段的Leader
,当然,在Raft
论文中,这组配置被称之为C-old
,代表老集群的节点配置。
到了第二阶段,集群扩容S4、S5
两个节点,集群出现成员变更场景,此次变更仅告知给了身为领导者的S3
节点,再由S3
同步给原集群中的S1、S2
两个节点(目前集群变成S1~S5
五个节点组成,这组新的节点配置称为C-new
)。
来到第三阶段,此时S3
还未来得及将S4、S5
已加入集群的消息同步给S1、S2
,S3
就突然出现短暂的故障(如网络不可用),导致其未及时向集群所有节点发送心跳,最终引发S1、S5
两个节点的超时,S1、S5
各自发起新一轮选举。
在第四阶段里,因为S1
还不知道集群节点已经增加到了五个(S3未来得及告知),所以它只会向S2、S3
节点拉票,又因为S3
是原本的主节点,S1
的日志肯定不可能比S3
要新,因此S3
会拒绝给S1
投票,而S2
会投出自己的一票,此时再加上S1
持有自身的一票,顺理成章当选新Leader
。
而S5
作为新加入的节点,它已知现在集群里有五个节点,所以会同时向S1~S4
发起拉票,因为S5
和S3
具备相同的日志(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
节点,Leader
在C-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
考虑的细节问题要多出很多,学起来知识密度会更大,但对比Paxos
,Raft
定义概念并没有那么抽象,并且各个子问题划分的十分明确,一点点啃下来之后,其实会发现比Paxos
要更容易令人接纳~
PS:关于其他较为重要的一致性落地实现,如霸道的
Bully
选举算法、Zookeeper
中的ZAB
协议、Redis
中应用广泛的GossIP
协议等,我们暂时不做分析,后续大家感兴趣再出单独的章节撰写。
最后,看完最近几篇一致性相关的章节,有的人可能会感觉,这好像都是针对主从架构的算法呀,现在都是纯分布式(分片模式)的组件,谁还会用这些算法呢?其实数之不尽,但凡有热备机制的存储组件,内部必然保证了一致性,例如当下分布式系统都离不开的注册中心等等,因此掌握这些著名的一致性知识,也是每位技术人绕不开的坎,只有明白这些底层理论后,才能帮助大家更好的理解技术原理,从而脱离“只会用”的新手阶段!
所有文章已开始陆续同步至微信公众号:竹子爱熊猫,想在微信上便捷阅读的小伙伴可搜索关注~