分布式数据库实战第四节 保证分布式系统中的数据库稳定

本博客深入解析分布式系统中的领导选举机制,包括Bully算法及其改进,以及如何结合故障检测确保数据同步与系统稳定性。了解领导选举算法、脑裂问题解决策略及一致性保障措施。

13 概要:分布式系统都要解决哪些问题?

在学习了存储引擎相关内容之后,从这一讲开始,我们就进入新的模块——分布式数据库最核心的部分,那就是分布式系统

分布式数据库区别于传统数据库的一个重要特性就是其分布式的特点,这些特点来源于分布式理论的发展,特别是数据分布相关理论的发展。相比于无状态分布式系统,有状态的数据库在分布式领域中将会面对更多的挑战。

本讲内容作为整个模块三的引子,我将会向你提出一系列问题,而在后续的课程中,我会逐一回答这些问题。那么现在让我们从失败模型开始,讨论分布式模式下的数据库吧。

失败模型

分布式系统是由多个节点参与其中的,它们直接通过网络进行互联。每个节点会保存本地的状态,通过网络来互相同步这些状态;同时节点需要访问时间组件来获取当前时间。对于分布式系统来说,时间分为逻辑时间与物理时间。逻辑时间一般被实现为一个单调递增的计数器,而物理时间对应的是一个真实世界的时间,一般由操作系统提供。

以上就是分布式系统所涉及的各种概念,看起很简单,实际上业界对分布式系统的共识就是上述所有环节没有一点是可靠的,“不可靠”贯穿了分布式系统的整个生命周期。而总结这些不可靠就成为失败模型所解决的问题。

在介绍失败模型的具体内容之前,让我们打开思路,看看有哪些具体的原因引起了分布式系统的可靠性问题。

引起失败的原因

当讨论分布式系统内的不稳定因素的时候,人们首先会想到网络问题,但是一个最容易让大家忽略的地方就是远程节点处理请求时也可能发生故障。一个比较常见的误区就是认为远程执行会马上返回结果,但这种假设是非常不可靠的。因为远程节点的处理能力、运行环境其实是未知的,我们不能认为它们会一直按照固定的模式去响应我们的请求。

而另一种情况是,请求到达远程节点后很可能不会被马上处理,而是放在了一个队列里面进行缓冲。这对于远程节点的吞吐量改善是有好处的,但是这在一定程度上带来了延迟,从而深刻地影响了交互模式。处理以上问题的方式就是需要引入故障检测(我会在下一讲介绍),来观察远程节点的运行情况,从而针对不同的问题采取不同的应对手段。

第二种常见的误解是所有节点时间是一致的,这种误解是非常普遍并且危险的。虽然可以使用工具去同步集群内的时间,但是要保持系统内时间一致是非常困难的。而如果我们使用不同节点产生的物理时间来进行一致性计算或排序,那么结果会非常不靠谱。所以大部分分布式数据库会用一个单独的节点来生成全局唯一的逻辑时间以解决上面的问题。而有些分布式数据库,如 Spanner 会使用原子钟这种精密服务来解决时间一致的问题。

本地物理时间的另一个问题是会产生回溯,也就是获取一个时间并执行若干步骤后,再去获取当前时间,而这个时间有可能比之前的时间还要早。也就是说我们不能认为系统的物理时间是单调递增的,这就是为什么要使用逻辑时间的另一个重要的原因。

但是本地物理时间在分布式系统中某些部分依然扮演着重要的作用,如判断远程节点超时等。但是基于以上两点,我们在实现分布式算法时应将时间因素考虑进去,从而避免潜在的问题。

以上谈到的分布式问题集中在节点层面,而另一大类问题就是网络造成的了。其中最为经典的问题就是网络分区,它指的是分布式系统的节点被网络故障分割为不同的小块。而最棘手的是,这些小块内的节点依然可以提供服务。但它们由于不能很好地感知彼此的存在,会产生不一致的问题,这个我们在模块一“05 | 一致性与 CAP 模型:为什么需要分布式一致性”有过比较详细的论述。

这里需要注意的是,网络分区带来的问题难以解决,因为它是非常难发现的。这是由于网络环境复杂的拓扑和参与者众多共同左右而导致的。故我们需要设计复杂的算法,并使用诸如混沌工程的方式来解决此类问题。

最后需要强调的一点是,一个单一读故障可能会引起大规模级联反映,从而放大故障的影响面,也就是著名的雪崩现象。这里你要注意,这种故障放大现象很可能来源于一个为了稳定系统而设计的机制。比如,当系统出现瓶颈后,一个新节点被加入进来,但它需要同步数据才能对外提供服务,而大规模同步数据很可能造成其他节点资源紧张,特别是网络带宽,从而导致整个系统都无法对外提供服务。

解决级联故障的方式有退避算法和断路。退避算法大量应用在 API 的设计中,由于上文提到远程节点会存在暂时性故障,故需要进行重试来使访问尽可能成功地完成。而频繁地重试会造成远程节点资源耗尽而崩溃,退避算法正是依靠客户端来保证服务端高可用的一种手段。而从服务端角度进行直接保护的方式就是断路,如果对服务端的访问超过阈值,那么系统会中断该服务的请求,从而缓解系统压力。

以上就是分布式系统比较常见的故障。虽然你可能会觉得这些故障很直观,但是如果要去解决它们思路会比较分散。还好前人已经帮我们总结了一些模型来对这些故障进行分级,从而有的放矢地解决这些问题。接下来我就要为你介绍三种典型的失败模型。

崩溃失败

当遭遇故障后,进程完全停止工作被称为崩溃失败。这是最简单的一种失败情况,同时结果也非常好预测。这种失败模式也称为崩溃停止失败,特别强调失败节点不需要再参与回分布式系统内部了。我们说这种模式是最容易预测的,是因为失败节点退出后,其他节点感知到之后可以继续提供服务,而不用考虑它重新回归所带来的复杂问题。

虽然失败停止模式有以上的优点,但实际的分布式系统很少会采用。因为它非常明显地会造成资源浪费,所以我们一般采用崩溃恢复模式,从而重复利用资源。提到崩溃节点恢复,一般都会想到将崩溃节点进行重启,而后经过一定的恢复步骤再加入网络中。虽然这是一种主流模式,但其实通过数据复制从而生成备份节点,而后进行快速热切换才是最为主流的模式。

崩溃失败可以被认为是遗漏失败的一种特殊情况。因为从其他节点看,他们很难分清一个节点服务响应是由于崩溃还是由于遗漏消息而产生的。那究竟什么是遗漏失败呢?

遗漏失败

遗漏失败相比于崩溃失败来说更为不可预测,这种模式强调的是消息有没有被远程节点所执行。

这其中的故障可能发生在:

  1. 消息发送后没有送达远程节点;

  2. 远程节点跳过消息的处理或根本无法执行(一种特例就是崩溃失败,节点无法处理消息);

  3. 后者处理的结果无法发送给其他节点。

总之,从其他节点的角度看,发送给该节点的消息石沉大海,没有任何响应了。

上文提到的网络分区是遗漏失败的典型案例,其中一部分节点间消息是能正常收发的,但是部分节点之间消息发送存在困难。而如果崩溃失败出现,集群中所有节点都将无法与其进行通讯。

另一种典型情况就是一个节点的处理速度远远慢于系统的平均水平,从而导致它的数据总是旧的,而此时它没有崩溃,依然会将这些旧数据发送给集群内的其他节点。

当远程节点遗漏消息时,我们是可以通过重发等可靠连接手段来缓解该问题的。但是如果最终还是无法将消息传递出去,同时当前节点依然在继续提供服务,那么此时遗漏失败才会产生。除了以上两种产生该失败的场景,遗漏失败还会发生在网络过载、消息队列满等场景中。

下面为你介绍最后一种失败模型,即拜占庭失败。

拜占庭失败

拜占庭失败又称为任意失败,它相比于上述两种失败是最不好预测的。所谓任意失败是,参与的节点对请求产生不一致的响应,一个说当前数据是 A,而另一个却说它是 B。

这个故障往往是程序 Bug 导致的,可以通过严格软件开发流程管理来尽可能规避。但我们都清楚,Bug 在生产系统中是很难避免的,特别是系统版本差异带来的问题是极其常见的。故在运行态,一部分系统并不信任直接从远程节点获得的数据,而是采用交叉检测的方式来尽可能得到正确的结果。

另一种任意失败是一些节点故意发送错误消息,目的是想破坏系统的正常运行,从而牟利。采用区块链技术的数字货币系统则是使用正面奖励的模式(BFT),来保证系统内大部分节点不“作恶”(做正确事的收益明显高于作恶)。

以上就是三种比较常见的失败模型。模块三的绝大部分内容主要是面向崩溃恢复的场景的。那么下面我们来梳理一下本模块接下来内容的讲解脉络。

错误侦测与领导选举

要想解决失败问题,首先就是要进行侦测。在本模块的开始部分,我们会研究使用什么手段来发现系统中的故障。目前,业界有众多方式来检测故障的产生,他们是在易用性、精确性和性能之间做平衡。

错误侦测一个重要应用领域就是领导选举。使用错误侦测技术来检测领导节点的健康状态,从而决定是否选择一个新节点来替代已经故障的领导节点。领导节点的一个主要作用就是缓解系统发生失败的可能。我们知道系统中如果进行对等同步状态的代价是很高昂的,如果能选择一个领导节点来统一进行协调,那么会大大降低系统负载,从而避免一些失败的产生。

而一旦侦测到失败的产生,如何解决它就是我们需要考虑的内容啦。

复制与一致性

故障容忍系统(Fault-tolerant)一般使用复制技术产生多个副本,来提供系统的可用性。这样可以保证当系统总部分节点发生故障后,仍然可以提供正常响应。而多个副本会产生数据同步的需求,一致性就是保证数据同步的前提。就像我在模块一中描述的那样,没有复制技术,一致性与同步就根本不存在。

模块一我们讨论的是 CAP 理论和强一致性模型,它们都是数据一致的范畴。本模块我们会接着讨论客户端一致,或称为会话一致。同时会讨论最终一致这种弱一致模型,最终一致模型允许系统中存在状态不一致的情况,但我们希望尽可能使系统保持一致,这时候会引入反熵手段来解决副本之间不一致的问题。

而后我们会接着讨论分布式事务,它与一致性存在着联系但又有很明显的区别。同时相比于模块二中的经典事务,分布式事务由于需要解决上文表述的各种失败情况,其处理是比较特殊的,比如需要进行事务协调来处理脑裂问题。

共识

最后我们将介绍分布式系统的精华:共识算法。以上介绍的很多内容,包括错误侦测、领导选举、一致性和分布式事务都涵盖在共识算法内,它是现代分布式数据库重要的组件。

共识算法是为了解决拜占庭将军问题而产生的。简单来说,在从前,拜占庭将军问题被认为是一个逻辑上的困境,它说明了一群拜占庭将军在试图就下一步行动达成统一意见时,可能存在的沟通问题。

该困境假设每个将军都有自己的军队,每支军队都位于他们打算攻击的城市周围的不同位置,这些将军需要就攻击或撤退达成一致。只要所有将军达成共识,即协调后决定共同执行,无论是攻击还是撤退都无关紧要。

基于著名的 FLP 不可能问题的研究,拜占庭将军们面临三种困境:

  1. 将军们没有统一的时间(没法对表);

  2. 无法知道别的将军是否被击败;

  3. 将军们之间的通讯是完全异步的。

由于以上的困境,我们是没有任何办法使将军们最终在特定时间内达成一致性意见的,也就是说共识算法在上述困境下是完全不可能的。

但是共识算法使用逻辑时钟来提供统一时间,并引入错误侦测技术来确定参与节点的情况,从而在完全异步的通讯情况下可以实现分布式系统的共识。本模块最后一部分,我会介绍几种经典的共识算法,并介绍它们的使用案例。

共识可以解决遗漏失败,因为只要系统内大部分节点达成共识,剩下的节点即使遗漏该消息,也能对外提供正确的数据。

13金句.png

总结

这一讲是模块三的引导课,我首先为你介绍了失败模型的概念,它是描述分布式数据库内各种可能行为的一个准则;而后根据失败模型为你梳理了本模块的讲解思路。

分布式算法根据目标不同可能分为下面几种行为模式,这些模式与对应的课时如下表所示。

image.png
下一讲我们将进入分布式系统的正式学习,希望准时与你相见。


14 错误侦测:如何保证分布式系统稳定?

经过上一讲的学习,相信你已经了解了分布式数据库领域中,分布式系统部分所重点解决的问题,即围绕失败模型来设计算法、解决各种稳定性问题。

解决问题的前提是发现问题,所以这一讲我们来说说如何发现系统内的错误,这是之后要介绍的算法们所依赖的前置条件。比如上一讲提到的共识算法,如果没有失败侦测手段,我们是无法解决拜占庭将军问题的,也就是会陷入 FLP 假说所描述的境地中,从而无法实现一个可用的共识算法。这里同时要指明,失败不仅仅是节点崩溃,而主要从其他节点看,该节点无法响应、延迟增大,从而降低系统整体的可用性。

这一讲,我将从影响侦测算法表现的几组特性出发,为评估这些算法给出可观标准;而后从你我耳熟能详的心跳算法开始介绍,逐步探讨几种其改良变种;最后介绍大型分布式数据库,特别是无主数据库常用的 Gossip 方案。

现在让我们从影响算法表现的因素开始说起。

影响算法的因素

失败可能发生在节点之间的连接,比如丢包或者延迟增大;也可能发生在节点进程本身,比如节点崩溃或者处理缓慢。我们其实很难区分节点到底是处理慢,还是完全无法处理请求。所以所有的侦测算法需要在这两个状态中平衡,比如发现节点无法响应后,一般会在特定的延迟时间后再去侦测,从而更准确地判断节点到底处于哪种状态。

基于以上原因,我们需要通过一系列的指标来衡量算法的特性。首先是任何算法都需要遵守一组特性:活跃性与安全性,它们是算法的必要条件

  • 活跃性指的是任何失败的消息都能被安全地处理,也就是如果一个节点失败了而无法响应正常的请求,它一定会被算法检测出来,而不会产生遗漏。

  • 安全性则相反,算法不产生任何异常的消息,以至于使得正常的节点被判定为异常节点,从而将它标记为失败。也就是一个节点失败了,它是真正失败了,而不是如上文所述的只是暂时性的缓慢。

还有一个必要条件就是算法的完成性。完成性被表述为算法要在预计的时间内得到结果,也就是它最终会产生一个符合活跃性和安全性的检测结果,而不会无限制地停留在某个状态,从而得不到任何结果。这其实也是任何分布式算法需要实现的特性。

上面介绍的三个特性都是失败检测的必要条件。而下面我将介绍的这一对概念,可以根据使用场景的不同在它们之间进行取舍。

首先要介绍的就是算法执行效率,效率表现为算法能多快地获取失败检测的结果。其次就是准确性,它表示获取的结果的精确程度,这个精确程度就是上文所述的对于活跃性与安全性的实现程度。不精准的算法要么表现为不能将已经失败的节点检测出来,要么就是将并没有失败的节点标记为失败。

效率和准确被认为是不可兼得的,如果我们想提高算法的执行效率,那么必然会带来准确性的降低,反之亦然。故在设计失败侦测算法时,要对这两个特性进行权衡,针对不同的场景提出不同的取舍标准。

基于以上的标准,让我开始介绍最常用的失败检测算法——心跳检测法,及其多样的变种。

心跳检测法

心跳检测法使用非常广泛,最主要的原因是它非常简单且直观。我们可以直接将它理解为一个随身心率检测仪,一旦该仪器检测不到心跳,就会报警。

心跳检测有许多实现手段,这里我会介绍基于超时和不基于超时的检测法,以及为了提高检测精准度的间接检测法。

基于超时

基于超时的心跳检测法一般包括两种方法。

  1. 发送一个 ping 包到远程节点,如果该节点可以在规定的时间内返回正确的响应,我们认为它就是在线节点;否则,就会将它标记为失败。

  2. 一个节点向周围节点以一个固定的频率发送特定的数据包(称为心跳包),周围节点根据接收的频率判断该节点的健康状态。如果超出规定时间,未收到数据包,则认为该节点已经离线。

可以看到这两种方法虽然实现细节不同,但都包含了一个所谓“规定时间”的概念,那就是超时机制。我们现在以第一种模式来详细介绍这种算法,请看下面这张图片。

Drawing 0.png

图 1 模拟两个连续心跳访问

上面的图模拟了两个连续心跳访问,节点 1 发送 ping 包,在规定的时间内节点 2 返回了 pong 包。从而节点 1 判断节点 2 是存活的。但在现实场景中经常会发生图 2 所示的情况。

Drawing 2.png

图 2 现实场景下的心跳访问

可以看到节点 1 发送 ping 后,节点没有在规定时间内返回 pong,此时节点 1 又发送了另外的 ping。此种情况表明,节点 2 存在延迟情况。偶尔的延迟在分布式场景中是极其常见的,故基于超时的心跳检测算法需要设置一个超时总数阈值。当超时次数超过该阈值后,才判断远程节点是离线状态,从而避免偶尔产生的延迟影响算法的准确性。

由上面的描述可知,基于超时的心跳检测法会为了调高算法的准确度,从而牺牲算法的效率。那有没有什么办法能改善算法的效率呢?下面我就要介绍一种不基于超时的心跳检测算法。

不基于超时

不基于超时的心跳检测算法是基于异步系统理论的。它保存一个全局节点的心跳列表,上面记录了每一个节点的心跳状态,从而可以直观地看到系统中节点的健康度。由此可知,该算法除了可以提高检测的效率外,还可以非常容易地获得所有节点的健康状态。那么这个全局列表是如何生成的呢?下图展示了该列表在节点之间的流转过程。

Drawing 4.png

图 3 全局列表在节点之间的流转过程

由图可知,该算法需要生成一个节点间的主要路径,该路径就是数据流在节点间最常经过的一条路径,该路径同时要包含集群内的所有节点。如上图所示,这条路径就是从节点 1 经过节点 2,最后到达节点 3。

算法开始的时候,节点首先将自己记录到表格中,然后将表格发送给节点 2;节点 2 首先将表格中的节点 1 的计数器加 1,然后将自己记录在表格中,而后发送给节点 3;节点 3 如节点 2 一样,将其中的所有节点计数器加 1,再把自己记录进去。一旦节点 3 发现所有节点全部被记录了,就停止这个表格的传播。

在一个真实的环境中,节点不是如例子中那样是线性排布的,而很可能是一个节点会与许多节点连接。这个算法的一个优点是,即使两个节点连接偶尔不通,只要这个远程节点可以至少被一个节点访问,它就有机会被记录在列表中。

这个算法是不基于超时设计的,故可以很快获取集群内的失败节点。并可以知道节点的健康度是由哪些节点给出的判断。但是它同时存在需要压制异常计算节点的问题,这些异常记录的计数器会将一个正常的节点标记为异常,从而使算法的精准度下降。

那么有没有方法能提高对于单一节点的判断呢?现在我就来介绍一种间接的检测方法。

间接检测

间接检测法可以有效提高算法的稳定性。它是将整个网络进行分组,我们不需要知道网络中所有节点的健康度,而只需要在子网中选取部分节点,它们会告知其相邻节点的健康状态。

Drawing 6.png

图 4 间接检测法

如图所示,节点 1 无法直接去判断节点 2 是否存活,这个时候它转而询问其相邻节点 3。由节点 3 去询问节点 2 的健康情况,最后将此信息由节点 3 返回给节点 1。

这种算法的好处是不需要将心跳检测进行广播,而是通过有限的网络连接,就可以检测到集群中各个分组内的健康情况,从而得知整个集群的健康情况。此种方法由于使用了组内的多个节点进行检测,其算法的准确度相比于一个节点去检测提高了很多。同时我们可以并行进行检测,算法的收敛速度也是很快的。因此可以说,间接检测法在准确度和效率上取得了比较好的平衡

但是在大规模分布式数据库中,心跳检测法会面临效率上的挑战,那么何种算法比较好处理这种挑战呢?下面我要为你介绍 Gossip 协议检测法。

Gossip 协议检测

除了心跳检测外,在大型分布式数据库中一个比较常用的检测方案就是 Gossip 协议检测法。Gossip 的原理是每个节点都检测与它相邻的节点,从而可以非常迅速地发现系统内的异常节点。

算法的细节是每个节点都有一份全局节点列表,从中选择一些节点进行检测。如果成功就增加成功计数器,同时记录最近一次的检测时间;而后该节点把自己的检测列表的周期性同步给邻居节点,邻居节点获得这份列表后会与自己本地的列表进行合并;最终系统内所有节点都会知道整个系统的健康状态。

如果某些节点没有进行正确响应,那么它们就会被标记为失败,从而进行后续的处理。这里注意,要设置合适的阈值来防止将正常的节点标记为错误。Gossip 算法广泛应用在无主的分布式系统中,比较著名的 Cassandra 就是采用了这种检测手法。

我们会发现,这种检测方法吸收了上文提到的间接检测方法的一些优势。每个节点是否应该被认为失败,是由多个节点判断的结果推导出的,并不是由单一节点做出的判断,这大大提高了系统的稳定性。但是,此种检测方法会极大增加系统内消息数量,故选择合适的数据包成为优化该模式的关键。这个问题我会在“17 | 数据可靠传播:反熵理论如何帮助数据库可靠工作”中详细介绍 Gossip 协议时给出答案。

Cassandra 作为 Gossip 检测法的主要案例,它同时还使用了另外一种方式去评价节点是否失败,那就是 φ 值检测法。

φ 值检测

以上提到的大部分检测方法都是使用二元数值来表示检测的结果,也就是一个节点不是健康的就是失败了,非黑即白。而 φ 值检测法引入了一个变量,它是一个数值,用来评价节点失败的可能性。现在我们来看看这个数值是如何计算的。

首先,我们需要生成一个检测消息到达的时间窗口,这个窗口保存着最近到的检测消息的延迟情况。根据这个窗口内的数值,我们使用一定的算法来“预测”未来消息的延迟。当消息实际到达时,我们用真实值与预测值来计算这个 φ 值。

其次,给 φ 设置一个阈值,一旦它超过这个阈值,我们就可以将节点设置为失败。这种检测模式可以根据实际情况动态调整阈值,故可以动态优化检测方案。同时,如果配合 Gossip 检测法,可以保证窗口内的数据更加有代表性,而不会由于个别节点的异常而影响 φ 值的计算。故这种评估检测法与 Gossip 检测具有某种天然的联系。

从以上算法的细节出发,我们很容易设计出该算法所需的多个组件。

  1. 延迟搜集器:搜集节点的延迟情况,用来构建延迟窗口。

  2. 分析器:根据搜集数据计算 φ 值,并根据阈值判断节点是否失败。

  3. 结果执行器:一旦节点被标记为失败,后续处理流程由结果执行器去触发。

你可以发现,这种检测模式将一个二元判断变为了一个连续值判断,也就是将一个开关变成了一个进度条。这种模式其实广泛应用在状态判断领域,比如 APM 领域中的 Apdex 指标,它也是将应用健康度抽象为一个评分,从而更细粒度地判断应用性能。我们看到,虽然这类算法有点复杂,但可以更加有效地判断系统的状态。

总结

这一讲内容比较简单、易理解,但是却非常重要且应用广泛。作为大部分分布式算法的基础,之后我要介绍的所有算法都包含今天所说的失败检测环节。

这一讲的算法都是在准确性与效率上直接进行平衡的。有些会使用点对点的心跳模式,有些会使用 Gossip 和消息广播模式,有些会使用单一的指标判断,而有些则使用估算的连续变换的数值……它们有各自的优缺点,但都是在以上两种特点之间去平衡的。当然简单性也被用作衡量算法实用程度的一个指标,这符合 UNIX 哲学,简单往往是应对复杂最佳的利器。

大部分分布式数据库都是主从模式,故一般由主节点进行失败检测,这样做的好处是能够有效控制集群内的消息数量,下一讲我会为你介绍如何在集群中选择领导节点。

好了,这一讲就到这里,希望你每天都能强大一点,我们下一讲见。


15 领导选举:如何在分布式系统内安全地协调操作?

这一讲我们来聊聊如何在分布式数据库,乃至一般性的分布式系统内同步数据。

不知道你是否发现这样一种事实:同步数据是一种代价非常高昂的操作,如果同步过程中需要所有参与的节点互相进行操作,那么其通信开销会非常巨大。

如下图所示,随着参与节点的增加,其通信成本逐步提高,最终一定会导致数据在集群内不一致。尤其在超大型和地理空间上分散的集群网络中,此现象会进一步被放大。

图片8.png

为了减少同步通信开销和参与节点的数量,一些算法引入了“领导者”(有时称为协调者),负责协调分布式系统内的数据同步。

领导选举

通常,分布式系统中所有节点都是平等的关系,任何节点都可以承担领导角色。节点一旦成为领导,一般在相当长的时间内会一直承担领导的角色,但这不是一个永久性的角色。原因也比较容易想到:节点会崩溃,从而不能履行领导职责。

现实生活中,如果你的领导由于个人变故无法履职,组织内会重新选择一个人来替代他。同样,在领导节点崩溃之后,任何其他节点都可以开始新一轮的选举。如果当选,就承担领导责任,并继续从前一个领导节点退出的位置开始工作。

领导节点起到协调整个集群的作用,其一般职责包括:

  • 控制广播消息的总顺序;

  • 收集并保存全局状态;

  • 接收消息,并在节点之间传播和同步它们;

  • 进行系统重置,一般是在发生故障后、初始化期间,或重要系统状态更新时操作。

集群并不会经常进行领导选举流程,一般会在如下两个场景中触发选举:

  • 在初始化时触发选举,称为首次选举领导;

  • 当前一位领导者崩溃或无法通信时。

选举算法中的关键属性

当集群进入选举流程后,其中的节点会应用选举算法来进行领导选举,而这些选举算法一般包含两个属性:“安全性”(Safety)和“活跃性”(Liveness)。它们是两个非常重要且比较基础的属性,最早由莱斯利·兰伯特( L.Lamport——分布式计算的开创者)提出。

在解释这两个属性的含义之前,我们先想象一下工作生活中是如何选举领导的?领导通常来源于一组候选人,选举规则需包含如下两点。

  1. 选举必须产生一个领导。如果有两个领导,那么下属应该听从他们中谁的指示呢?领导选举本来是解决协调问题的,而多个领导不仅没有解决这个问题,反而带来了更大问题。

  2. 选举必须有结果。较为理想的状态是:领导选举需要在大家可以接受的时间内有结果。如果领导长时间没有被选举出来,那么必然造成该组织无法开展正常的工作。因为没人来协调和安排工作,整个组织内部会变得混乱无序。

以上两个规则正好对应到算法的两个属性上。

其中第一个规则对应了算法的“安全性”(Safety),它保证一次最多只有一个领导者,同时完全消除“脑裂”(Split Brain)情况的可能性(集群被分成两个以上部分,并产生多个互相不知道对方存在的领导节点)。然而,在实践中,许多领导人选举算法违反了这个属性。下面在介绍“脑裂”的时候会详细讲解如何解决这个问题。

第二个规则代表了选举算法的“活跃性”(Liveness),它保证了在绝大多数时候,集群内都会有一个领导者,选举最终会完成并产生这个领导,即系统不应无限期地处于选举状态。

满足了以上两个属性的算法,我们才称其为有效的领导选举算法。

领导选举与分布式锁

领导选举和分布式锁在算法层面有很高的重合性,前者选择一个节点作为领导,而后者则是作为锁持有者,因此很多研发人员经常将二者混为一谈。那么现在,让我们比较一下领导者选举和分布式锁的区别。

分布式锁是保证在并发环境中,一些互斥的资源,比如事务、共享变量等,同一时间内只能有一个节点进行操作。它也需要满足上文提到的安全性和活跃性,即排他锁每次只能分配给一个节点,同时该节点不会无限期持有锁。

从理论上看,虽然它们有很多相似之处,但也有比较明显的区别。如果一个节点持有排他锁,那么对于其他节点来说,不需要知道谁现在持有这个锁,只要满足锁最终将被释放,允许其他人获得它,这就是前文所说的“活跃性”。

与此相反,选举过程完全不是这样,集群中的节点必须要知道目前系统中谁是领导节点,因为集群中其他节点需要感知领导节点的活性,从而判断是否需要进入到选举流程中。因此,新当选的领导人必须将自己的角色告知给它的下属。

另一个差异是:如果分布式锁算法对特定的节点或节点组有偏好,也就是非公平锁,它最终会导致一些非优先节点永远都获得不了共享资源,这与“活跃性”是矛盾的。但与其相反,我们一般希望领导节点尽可能长时间地担任领导角色,直到它停止或崩溃,因为“老”领导者更受大家的欢迎。

解决单点问题

在分布式系统中,具有稳定性的领导者有助于减小远程节点的状态同步消耗,减少交换消息的数量;同时一些操作可以在单一的领导节点内部进行,避免在集群内进行同步操作。在采用领导机制的系统中,一个潜在的问题是由于领导者是单节点,故其可能成为性能瓶颈。

为了克服这一点,许多系统将数据划分为不相交的独立副本集,每个副本集都有自己的领导者,而不是只有一个全局领导者,使用这种方法的系统代表是 Spanner(将在第 17 讲“分布式事务”中介绍)。由于每个领导节点都有失败的可能,因此必须检测、报告,当发生此种情况时,一个系统必须选出另一个领导人来取代失败的领导人。

上面整体介绍了领导选举的使用场景和算法特点,那么领导选举是怎样操作的呢?

典型的算法包括:Bully 算法、ZAB(Zookeeper Atomic Broadcast)、Multi-Paxos 和 RAFT 等。但是除了 Bully 算法外,其他算法都使用自己独特的方法来同时解决领导者选举、故障检测和解决竞争领导者节点之间的冲突。所以它们的内涵要远远大于领导选举这个范畴,限于篇幅问题,我将会在下一讲详细介绍。

基于以上的原因,下面我将使用 Bully 算法及其改进算法来举例说明典型的领导选举流程。Bully 算法简单且容易进行收敛,可以很好地满足“活跃性”;同时在无网络分区的情况下,也能很好地满足“安全性”。

经典领导选举算法:Bully 算法

这是最常用的一种领导选举算法,它使用节点 ID的大小来选举新领导者。在所有活跃的节点中,选取节点 ID 最大或者最小的节点为主节点。

以下采用“ID 越大优先级越高”的逻辑来解释算法:

每个节点都会获得分配给它的唯一 ID。在选举期间,ID 最大的节点成为领导者。因为 ID 最大的节点“逼迫”其他节点接受它成为领导者,它也被称为君主制领导人选举:类似于各国王室中的领导人继承顺位,由顺位最高的皇室成员来继承皇位。如果某个节点意识到系统中没有领导者,则开始选举,或者先前的领导者已经停止响应请求。

算法包含 4 个步骤:

  1. 集群中每个活着的节点查找比自己 ID 大的节点,如果不存在则向其他节点发送 Victory 消息,表明自己为领导节点;

  2. 如果存在比自己 ID 大的节点,则向这些节点发送 Election 消息,并等待响应;

  3. 如果在给定的时间内,没有收到这些节点回复的消息,则自己成为领导节点,并向比自己  ID 小的节点发送 Victory 消息;

  4. 节点收到比自己 ID 小的节点发送的 Election 消息,则回复 Alive 消息。

图片7.png

![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0d6720413722ea70ccc6a6c403601ba2.png#pic_center)

上图举例说明了 Bully 领导者选举算法,其中:

  • 节点3 注意到先前的领导者 6 已经崩溃,并且通过向比自己 ID 更大的节点发送选举消息来开始新的选举;

  • 4 和 5 以 Alive 响应,因为它们的 ID 比 3 更大;

  • 3 通知在这一轮中作出响应的最大 ID 节点是5;

  • 5 被选为新领导人,它广播选举信息,通知排名较低的节点选举结果。

这种算法的一个明显问题是:它违反了“安全性”原则(即一次最多只能选出一位领导人)。在存在网络分区的情况下,在节点被分成两个或多个独立工作的子集的情况下,每个子集选举其领导者。

该算法的另一个问题是:它对 ID较大的节点有强烈的偏好,但是如果它们不稳定,会严重威胁选举的稳定性,并可能导致不稳定节点永久性地连任。即不稳定的高排名节点提出自己作为领导者,不久之后失败,但是在新一轮选举中又赢得选举,然后再次失败,选举过程就会如此重复而不能结束。这种情况,可以通过监控节点的存活性指标,并在选举期间根据这些指标来评价节点的活性,从而解决该问题。

Bully 算法的改进

Bully 算法虽然经典,但由于其相对简单,在实际应用中往往不能得到良好的效果。因此在分布式数据库中,我们会看到如下所述的多种演进版本来解决真实环境中的一些问题,但需要注意的是,其核心依然是经典的 Bully 算法。

改进一:故障转移节点列表

有许多版本的 Bully 算法的变种,用来改善它在各种场景下的表现。例如,我们可以使用多个备选节点作为在发生领导节点崩溃后的故障转移目标,从而缩短重选时间。每个当选的领导者都提供一个故障转移节点列表。当集群中的节点检测到领导者异常时,它通过向该领导节点提供的候选列表中排名最高的候选人发送信息,开始新一轮选举。如果其中一位候选人当选,它就会成为新的领导人,而无须经历完整的选举。

如果已经检测到领导者故障的进程本身是列表中排名最高的进程,它可以立即通知其他节点自己就是新的领导者。

图片6.png

上图显示了采用这种优化方式的过程,其中:

  • 6是具有指定候选列表 {5,4} 的领导者,它崩溃退出,3 注意到该故障,并与列表中具有最高等级的备选节点5 联系;

  • 5 响应 3,表示它是 Alive 的,从而防止 3 与备选列表中的其他节点联系;

  • 5 通知其他节点它是新的领导者。

因此,如果备选列表中,第一个节点是活跃的,我们在选举期间需要的步骤就会更少。

改进二:节点分角色

另一种算法试图通过将节点分成候选普通两个子集来降低消息数量,其中只有一个候选节点可以最终成为领导者。普通节点联系候选节点、从它们之中选择优先级最高的节点作为领导者,然后将选举结果通知其余节点。

为了解决并发选举的问题,该算法引入了一个随机的启动延迟,从而使不同节点产生了不同的启动时间,最终导致其中一个节点在其他节点之前发起了选举。该延迟时间通常大于消息在节点间往返时间。具有较高优先级的节点具有较低的延迟,较低优先级节点延迟往往很大。

图片5.png
上图显示了选举过程的步骤,其中:

  • 节点4 来自普通的集合,它发现了崩溃的领导者 6,于是通过联系候选集合中的所有剩余节点来开始新一轮选举;

  • 候选节点响应并告知 4 它们仍然活着;

  • 4通知所有节点新的领导者是 2。

该算法减小了领导选举中参与节点的数量,从而加快了在大型集群中该算法收敛的速度。

改进三:邀请算法

邀请算法允许节点“邀请”其他进程加入它们的组,而不是进行组间优先级排序。该算法允许定义多个领导者,从而形成每个组都有其自己的领导者的局面。每个节点开始时都是一个新组的领导者,其中唯一的成员就是该节点本身。

组领导者联系不属于它们组内的其他节点,并邀请它们加入该组。如果受邀节点本身是领导者,则合并两个组;否则,受邀节点回复它所在组的组长 ID,允许两个组长直接取得联系并合并组,这样大大减少了合并的操作步骤。

图片4.png
上图显示了邀请算法的执行步骤,其中:

  • 四个节点形成四个独立组,每个节点都是所在组的领导,1 邀请 2 加入其组,3 邀请 4 加入其组;

  • 2 加入节点 1的组,并且 4 加入节点3的组,1 为第一组组长,联系人另一组组长 3,剩余组成员(在本例中为 4个)获知了新的组长 1;

  • 合并两个组,并且 1 成为扩展组的领导者。

由于组被合并,不管是发起合并的组长成为新的领导,还是另一个组长成为新的领导。为了将合并组所需的消息数量保持在最小,一般选择具有较大 ID 的组长的领导者成为新组的领导者,这样,只有来自较小 ID 组的节点需要更新领导者。

与所讨论的其他算法类似,该算法采用“分而治之”的方法来收敛领导选举。邀请算法允许创建节点组并合并它们,而不必从头开始触发新的选举,这样就减少了完成选举所需的消息数量。

改进四:环形算法

在环形算法中,系统中的所有节点形成环,并且每个节点都知道该环形拓扑结构,了解其前后邻居。当节点检测到领导者失败时,它开始新的选举,选举消息在整个环中转发,方式为:每个节点联系它的后继节点(环中离它最近的下一节点)。如果该节点不可用,则跳过该节点,并尝试联系环中其后的节点,直到最终它们中的一个有回应。

节点联系它们的兄弟节点,收集所有活跃的节点从而形成可用的节点集。在将该节点集传递到下一个节点之前,该节点将自己添加到集合中。

该算法通过完全遍历该环来进行。当消息返回到开始选举的节点时,从活跃集合中选择排名最高的节点作为领导者。

图片3.png
如上图所示,你可以看到这样一个遍历的例子:

  • 先前的领导 6失败了,环中每个节点都从自己的角度保存了一份当前环的拓扑结构;

  • 以 3 为例,说明查找新领导的流程,3 通过开始遍历来发起选举轮次,在每一步中,节点都按照既定路线进行遍历操作,5 不能到 6,所以跳过,直接到 1;

  • 由于 5 是具有最高等级的节点,3 发起另一轮消息,分发关于新领导者的信息。

该算法的一个优化方法是每个节点只发布它认为排名最高的节点,而不是一组活跃的节点,以节省空间:因为 Max 最大值函数是遵循交换率的,也就是知道一个最大值就足够了。当算法返回到已经开始选举的节点时,最后就得到了 ID 最大的节点。

另外由于环可以被拆分为两个或更多个部分,每个部分就会选举自己的领导者,这种算法也不具备“安全性”。

如前所述,要使具有领导的系统正常运行,我们需要知道当前领导的状态。因此,为了系统整体的稳定性,领导者必须保证是一直活跃的,并且能够履行其职责。为了检测领导是否崩溃,可以使用我上一讲介绍过的故障检测算法。

解决选举算法中的脑裂问题

我们需要注意到,在本讲中讨论的所有算法都容易出现脑裂的问题,即最终可能会在独立的两个子网中出现两个领导者,而这两个领导并不知道对方的存在。

为了避免脑裂问题,我们一般需要引入法定人数来选举领导。比如 Elasticsearch 选举集群领导,就使用Bully 算法结合最小法定人数来解决脑裂问题。

图片1.png
如上图所示,目前有 2 个网络、5 个节点,假定最小法定人数是3。A 目前作为集群的领导,A、B 在一个网络,C、D 和 E 在另外一个网络,两个网络被连接在一起。

当这个连接失败后,A、B 还能连接彼此,但与 C、D 和 E 失去了联系。同样, C、D 和 E 也能知道彼此,但无法连接到A 和B。

图片2.png
此时,C、D 和 E 无法连接原有的领导 A。同时它们三个满足最小法定人数3,故开始进行新一轮的选举。假设 C 被选举为新的领导,这三个节点就可以正常进行工作了。

而在另外一个网络中,虽然 A 是曾经的领导,但是这个网络内节点数量是 2,小于最小法定人数。故 A 会主动放弃其领导角色,从而导致该网络中的节点被标记为不可用,从而拒绝提供服务。这样就有效地避免了脑裂带来的问题。

总结

领导选举是分布式系统中的一个重要课题,这是因为使用固定的领导者非常有助于减少协调开销并提高系统的性能。选举过程可能成本很高,但由于不是很频繁,因此不会对整个系统性能产生严重影响。单一的领导者可能成为瓶颈,但我们可以通过对数据进行分区并使用每个分区的领导者来解决这个问题,或对不同的操作使用不同的领导者。

许多共识算法,包括Multi-Paxos 和 RAFT一般都有选举的过程。但是共识算法的内涵相比于单纯的选举算法更为丰富,所以我在“19 | 共识算法:一次性说清楚 Paxos、Raft 等算法的区别”中会专门来和你讨论。

能力越强,责任越大。领导节点虽然解决了系统内数据同步的问题,但由于其承担重大责任,一旦发生问题将会产生严重的影响。故一个稳定高效的选举算法是领导模式的关键。

领导者的状态可能在节点不知道的情况下发生变化,所以集群内节点需要及时了解领导节点是否仍然活跃。为了实现这一点,我们需要将领导选举与故障检测结合起来。例如,稳定领导者选举算法使用具有独特的稳定领导者和基于超时的故障检测的轮次,以保证领导者可以被重新选举,从而保留它的领导地位。前提是只要它不会崩溃,并且可以访问。

教学相长

以上就是本讲的内容了,我希望你在课下思考一个问题:除了领导选举算法外,还有哪些方法可以可靠的在分布式系统内同步数据呢?

下一讲,我会为你介绍除了CAP 之外的一致性模型还有哪些,下次见。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

办公模板库 素材蛙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值