手写RPC框架-zookeeperCAP理论和选举和同步算法

89 阅读30分钟

一、CAP理论

数据同步永远都是重中之重。因为一个集群当中会有很多节点那么客户端每次写数据的时候,是只向一个节点写入,还是向所有节点写入就成了一个问题。

如果向所有节点写入,假设节点个数为 N,那么客户端的一次写请求就会被放大 N 倍,因为每个节点都要写一遍,显然这么做是非常不明智的。因此我们应该让客户端只向一个节点写入,然后该节点再将数据同步给集群内的其它节点。

但这就产生了一个问题,如果某个节点的数据同步还没有完成,就收到了客户端的读请求,那么显然会返回旧数据。如果想让客户端看到的一定是新数据,那么就必须等到数据在所有节点之间都同步完成之后,才能让客户端访问,而这又会造成集群服务出现短暂的不可用

因此面对这种情况,我们必须要做出取舍,至于如何取舍,CAP 理论会告诉我们答案。它对分布式系统的特性进行了抽象,掌握了 CAP 理论,我们在面对分布式系统的时候就可以做到心中有数。

CAP 理论对分布式系统的特性做了高度抽象,形成了三个指标:

  • 一致性(Consistency)/kənˈsɪstənsi/;
  • 可用性(Availability);
  • 分区容错性(Partition Tolerance);

以上这三个指标就称之为 CAP,我们来分别介绍。

1、一致性,即 CAP 中的 C

一致性说的是客户端的每次读操作,不管访问哪个节点,读到的都是同一份最新的数据(或者读取失败,说明节点之间还在同步数据)。不会出现读不同节点,得到的数据不同这种情况。

所以一致性强调的不是数据完整,而是各节点间的数据一致。

为了更好地理解一致性这个指标,我们以我们的zookeeper为例。假设当前有两个zookeeper节点,路径/ttb中保存的数据是hello,此时客户端请求将数据改为hi。

如果节点 1 收到写请求后,只修改自身数据,然后返回成功给客户端,那么这个时候节点 2 的数据还是hello,此时两个节点的数据就是非一致的

如果节点 1 收到写请求后,不仅自身更新数据,还通过节点间的通讯,将更新操作发送给节点 2,等到自身和节点 2 的 数据都更新之后,再返回成功给客户端。那么当客户端完成写请求后,两个节点的数据就是一致的了。之后不管客户端访问哪个节点,读取到的都是同一份最新数据。

一致性这个指标,描述的是分布式系统非常重要的一个特性,强调的是数据的一致。也就是说,在客户端看来,访问集群和访问单机是等价的,因为两者在数据一致性上是一样的。

集群毕竟不是单机,*总会有*网络故障的时候,那么当节点之间无法通信的时候该怎么办呢?比如节点1在将写请求同步给节点2的时候,发生了网络故障,这时候如果要保证一致性,也就是让客户端访问任何一个节点都能看到相同的数据,那么就应该拒绝服务(客户端读取失败),等到数据同步完成之后再提供服务。否则客户端就可能读到旧数据,比如访问节点 2 的时候,因为网络原因数据还没有同步过来。

因此可以把一致性看成是分布式系统对客户端的一种承诺:不管访问哪个节点,返回的都是绝对一致的数据,因为数据不一致的时候会读取失败(拒绝提供服务)。所以再次强调,一致性强调的不是数据完整,而是各节点之间的数据绝对一致。

但有些服务并不追求数据的一致性,返回旧数据也是可以的。当面对这种场景时,再因为节点间出现了通讯问题(会导致节点间的数据不一致)而拒绝提供服务,就有些不合适了。

这个时候我们就需要牺牲数据的一致性,每个节点使用本地数据来响应客户端请求,保证服务可用。这就是我们要说的另外一个指标,可用性。

2、可用性,即 CAP 中的 A

可用性说的是任何来自客户端的请求,不管访问哪个节点,都能得到响应数据,但不保证是同一份最新数据。

因此可以把可用性看作是分布式系统对客户端的另一种承诺:**尽量返回数据,不会不响应,但不保证每个节点返回的数据都是最新的。**因此可用性这个指标强调的是服务可用,但不保证数据的绝对一致。

3、分区容错性,即 CAP 中的 P

最后的分区容错性说的是,当节点间出现任意数量的消息丢失或高延迟的时候,系统仍然可以继续提供服务。也就是说,分布式系统会告诉客户端:不管我的内部出现什么样的数据同步问题,我会一直运行,提供服务。这个指标,强调的是集群对分区故障的容错能力。

比如当节点 1 和节点 2 通信出问题(发生网络分区)的时候,如果系统仍能提供服务,那么两个节点是满足分区容错性的。而分布式系统与单机系统不同,它涉及到多节点之间的通讯和交互,节点间的分区故障不可能完全避免,所以在分布式系统中分区容错性是必须要考虑的。

4、CAP 不可兼得

对于一个分布式系统而言,一致性、可用性、分区容错性 3 个指标不可兼得,只能在 3 个指标中选择两个。

我们知道只要有网络交互就一定会有延迟和数据丢失,而这种状况我们必须接受,还必须保证系统不能挂掉。所以就像上面提到的,节点间的分区故障是必然发生的。也就是说,分区容错性(P)是前提,是必须要保证的,不能说某些节点之间无法正常通信(发生网络分区)就导致整个集群不可用。

现在就只剩下一致性(C)和可用性(A)可以选择了:**要么选择一致性,保证数据绝对一致;要么选择可用性,保证服务可用。**如果选择 C,那么就是 CP 模型;如果选择 A,那么就是 AP 模型。

  • 当选择一致性(C)的时候,如果因为消息丢失、延迟过高发生了网络故障,部分节点无法保证特定信息是最新的。那么这个时候,当集群节点接收到来自客户端的请求时,因为无法保证所有节点都是最新信息,所以系统将返回错误,也就是说拒绝请求。
  • 当选择可用性(A)的时候,如果发生了网络故障,一些节点将无法返回最新的特定信息,那么它们将返回自己当前相对新的信息。

这里需要再强调一点,有很多人对 CAP 理论有个误解,认为无论在什么情况下,分布式系统都只能在 C 和 A 中选择 1 个。其实在不发生网络故障的情况下,也就是分布式系统正常运行时(这也是系统在绝大部分时候所处的状态),C 和 A 是能够大致同时保证的(如果节点之间的数据同步很快的话)。只有当发生分区故障的时候,也就是说需要 P 时,才会在 C 和 A 之间做出选择。

5、CAP 总结

以上就是 CAP 理论的具体内容,以及 CAP 理论的应用,总结如下:

1)CA 模型:

不支持分区容错,只支持一致性和可用性,但这在分布式系统中不存在。因为不支持分区容错性,也就意味着不允许分区异常,设备、网络永远处于理想的可用状态,从而让整个分布式系统满足一致性和可用性。

但分布式系统是由众多节点通过网络通信连接构建的,设备故障、网络异常是客观存在的,而且分布的节点越多,范围越广,出现故障和异常的概率也越大。因此对于分布式系统而言,分区容错性(P)是无法避免的,如果避免了 P,那么只能把分布式系统回退到单机单实例系统。就比如单机版关系型数据库 MySQL,如果 MySQL 要考虑主备或集群部署时,那么它也必须考虑 P。

2)CP 模型:

因为分区容错客观存在,所以放弃系统的可用性,换取一致性。采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高而发生了网络分区,就会持续阻塞整个服务,直到分区问题解决,才恢复对外服务,这样就可以保证数据的一致性。

选择 CP 一般都是对数据一致性特别敏感,尤其是在支付交易领域,Hbase 等分布式数据库领域,都要优先保证数据的一致性,在出现网络异常时,系统就会暂停服务处理。还有用来分发及订阅元数据的 Zookeeper、Etcd 等等,也是优先保证 CP 的。

3)AP 模型:

由于分区容错 P 客观存在,所以放弃系统的数据一致性,换取可用性。在系统遇到分区异常时,某些节点之间无法通信,数据处于不一致的状态。但为了保证可用性,服务节点在收到用户请求后会立即响应,因此只能返回各自新老不同的数据。

这种舍弃一致性,而保证系统在分区异常下的可用性,在互联网系统中非常常见。比如微博多地部署,如果不同区域出现网络中断,区域内的用户仍然能发微博、相互评论和点赞,但暂时无法看到其它区域用户发布的新微博和互动状态。

还有类似 12306 这种火车购票系统,在节假日高峰期抢票时也会遇到这种情况,明明某车次有余票,但真正点击购买时,却提示说没有余票。就是因为票已经被抢光了,票的可选数量应该更新为 0,但因并发过高导致当前访问的节点还没有来得及更新就提供服务了(和发生网络分区是类似的,都是最新数据还没有同步,就对外提供服务)。因此它返回的是更新之前的旧数据,但其实已经没有票了。

所以相比 CP,采用 AP 模型的分布式系统,更注重服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误。但当出现分区故障、或者并发量过高导致数据来不及同步时,相同的读操作,访问不同的节点,得到的响应数据可能不一样。典型应用有 Cassandra, DynamoDB, Redis 等 NoSQL。

因此 CAP 理论可以帮助我们思考如何在一致性和可用性之间进行妥协折中,设计出满足场景特点的分布式系统。

最后再提一点,在分布式系统开发中,延迟是非常重要的一个指标。比如名字路由系统,通过延迟评估服务可用性,进行负载均衡和容灾;再比如在 Raft 实现中,通过延迟评估领导者节点的服务可用性,以及决定是否发起领导者选举;再比如类似 Redis 这种查询量非常大的分布式缓存,它的目的是能够快速地返回结果,所以它是 AP 模型。

所以在分布式系统的开发中,要能意识到延迟的重要性,能通过延迟来衡量服务的可用性。总之能否容忍短暂的延迟是关键。

6、BASE 理论

BASE 理论是 CAP 理论中的 AP 的延伸,所以它强调的是可用性,这个理论广泛应用在大型互联网的后台当中。它的核心思想就是基本可用(Basically Available)和最终一致性(Eventually consistent)

首先「基本可用」指的是,当分布式系统在出现不可预知的故障时,允许损失部分功能的可用性,来保障核心功能的可用性。说白了就是服务降级,在服务器资源不够、或者说压力过大时,将一些非核心服务暂停,优先保证核心服务的运行。比如:

  • 当业务应用访问的是非核心数据(例如电商商品属性)时,拒绝服务,或者直接返回预定义信息、空值或错误信息;当业务应用访问的是核心数据(例如电商商品库存)时,正常查询结果并返回;
  • 还可以对用户体验进行降级,比如用小图片来替代原始图片,通过降低图片的清晰度和大小,提升系统的处理能力;

所以基本可用本质上是一种妥协,也就是在出现节点故障或系统过载的时候,通过牺牲非核心功能的可用性,保障核心功能的稳定运行。而手段也有很多,比如服务降级、体验降级、流量削峰、延迟响应、接口限流、服务熔断等等。

然后是最终一致性,它指的是系统中所有的数据副本在经过一段时间的同步后,最终能够达到一致的状态。也就是说在数据一致性上,存在一个短暂的延迟,几乎所有的互联网系统采用的都是最终一致性。比如 12306 买票,票明明卖光了,但还是显示有余票,说明此时数据不一致。但当你在真正购买的时候,又会提示你票卖光了,说明数据最终是一致的。

因此最终一致性应该不难理解,就是节点间的数据存在短暂的不一致,但经过一段时间后,最终会达到一致的状态。所以 BASE 理论除了引入一个基本可用之外,它和 AP 模型本质上没太大区别。

只有对数据有强一致性要求,才考虑 CP 模型或分布式事务,比如:决定系统运行的敏感元数据,需要考虑采用强一致性;与钱有关的支付系统或金融系统的数据,需要考虑采用事务保证一致性。因此,尽管事务型的分布式系统和强一致性的分布式系统,使用起来很方便,不需要考虑太多,就像使用单机系统一样。但是我们要知道,想在分布式系统中实现强一致性,必然会影响可用性。

如果换个角度思考,我们可以将强一致性理解为最终一致性的特例,也就是说可以把强一致性看作是不存在延迟的一致性。因此在实践中我们也可以这样思考:如果业务的某功能无法容忍一致性的延迟(比如分布式锁对应的数据),就需要强一致性;如果能容忍短暂的一致性的延迟(比如APP用户的状态数据),就可以考虑最终一致性。

所以我们之前介绍基于 Redis 实现分布式锁的时候,说过 Redis 在主从切换的时候会出问题,就是因为分布式锁需要的是 CP 模型,而 Redis 是 AP 模型。

小结:

BASE 理论是对 CAP 中一致性和可用性权衡的结果,它来源于对大规模互联网分布式系统实践的总结,是基于 CAP 定理逐步演化而来的。它的核心思想是,如果不是必须的话,不推荐使用事务或强一致性,鼓励可用性和性能优先,根据业务的场景特点,来实现非常弹性的基本可用,以及实现数据的最终一致性。

BASE 理论主张通过牺牲部分功能的可用性,实现整体的基本可用,也就是说通过服务降级的方式,努力保障极端情况下的系统可用性。

说到 BASE 理论,应该会有人想到 ACID 理论。ACID 是传统数据库常用的设计理念,追求强一致性模型;而 BASE 理论支持的是大型分布式系统,通过牺牲强一致性获得高可用性。BASE 理论在很大程度上,解决了事务型系统在性能、容错、可用性等方面的痛点。此外 BASE 理论在 NoSQL 中也应用广泛,是 NoSQL 系统设计的理论支撑。

对于任何集群而言,不可预知的故障的最终后果,都是系统过载。如何设计过载保护,实现系统在过载时的基本可用,是开发和运营互联网后台的分布式系统的重点。因此在开发实现分布式系统,要充分考虑如何实现基本可用。

八、选举和同步算法

1、写操作的具体流程

我们假设现在有一个写操作,需要在ZooKeeper集群服务中执行写操作,创建一个/ydlclass节点,其大致流程如下:

  1. 客户端连接:首先,要创建节点的客户端需要与ZooKeeper集群中的任何一个服务器建立连接。

  2. 发起写请求:客户端向Leader发送写请求,请求创建一个新的节点。

  3. Leader处理写请求:Leader接收到写请求后,将生成一个全局唯一的ZooKeeper事务ID(ZXID),用来标识这个写操作。

  4. 创建节点过程:Leader将写请求转发给其他Follower节点,并协调它们来完成创建节点的过程。具体步骤如下:

    a. Leader将写请求转发给Follower节点。

    b. Follower节点接收到写请求后,会记录下这个写操作的ZXID,并执行节点的创建操作。

    c. Follower节点将创建节点的操作结果返回给Leader。

    d. Leader收集Follower节点的操作结果,并基于大多数原则决定最终的写操作结果。

  5. 数据同步过程:一旦写操作成功并被大多数节点接受,数据同步将在ZooKeeper集群中进行。具体步骤如下:

    a. Leader将写请求成功的结果通知给所有的Follower节点。

    b. Follower节点在收到通知后,会将Leader上的数据进行复制,确保自己的数据与Leader上的数据保持一致。

    c. Follower节点完成数据复制后,会向Leader发送确认通知。

    d. Leader在收到所有Follower节点的确认通知后,确定数据同步成功。

  6. 客户端响应:一旦数据同步成功,Leader将向客户端发送操作成功的响应,表示节点创建完成。

已经执行了写操作还要数据同步吗?

在ZooKeeper中,Follower节点执行写操作并返回成功结果给Leader是为了保证写操作的一致性和持久性。数据同步是确保在整个集群中所有节点的数据是一致的关键步骤

尽管Follower节点会在接收到写请求后立即执行对应的操作,但在写操作的结果被确认之前,数据同步的过程是必要的。这是因为ZooKeeper使用了多数原则来决定写操作的最终结果,只有在大多数节点都完成写操作并确认成功后,Leader才会确认写操作成功。此时,数据同步的过程才会开始。

值得注意的是,数据同步是在写操作完成后才开始的,这意味着在数据同步期间的某个时间点,集群中的不同节点的数据可能是不一致的。但是一旦数据同步完成,所有节点都将具有相同的数据,并保持一致性。这样做是为了在写操作期间保持高可用性,并在数据同步完成后确保数据的一致性。

数据同步是增量同步还是全量同步?

全量数据同步可能会对性能产生一定的影响,尤其是当数据量较大或者集群规模较大时。因为全量数据同步需要将所有相关节点上的数据进行复制,网络传输和处理的开销都可能会比较大。

为了减小全量数据同步的性能开销,ZooKeeper在设计上采取了一些优化措施:

  1. 增量更新:ZooKeeper中的数据是以事务日志的形式进行持久化的。当数据有更新时,只会记录变更的内容,而不会每次都全量复制所有的数据。这样可以减小数据同步的开销。
  2. 快速同步:当一个Follower节点加入集群时,它可以通过快速同步(Snapshot)的方式来获取最新的数据。只需要将Leader节点上的整个数据文件传输给新加入的Follower节点,而不是逐个复制增量更新。这样可以加快新节点的数据同步过程。

尽管全量数据同步可能对性能产生一定的影响,但这是为了保证集群中所有节点数据的一致性和可靠性。在实际应用中,可以通过合理的配置和优化集群硬件设施来提高性能,例如使用高性能的网络、增加机器的处理能力等,从而减少全量数据同步的性能开销。

2、选举流程

ZooKeeper是一个分布式协调服务,它的选举过程是为了确保集群中的某个节点成为leader,负责处理客户端的请求。下面是ZooKeeper选取leader的详细过程:

  1. 初始化:当一个ZooKeeper服务器加入集群时,它会初始化自己的状态。这包括为自己分配一个唯一的标识符(myid)、创建临时节点(通过在ZooKeeper中创建一个带有自己标识符的临时顺序节点)以及与其他服务器建立连接等。
  2. 选举过程的启动:当服务器初始化完成后,它会启动选举过程。在选举过程中,服务器将参与一个Leader选举协议。
  3. 选举协议:ZooKeeper使用的是基于Paxos的Zab协议。在选举协议中,每个服务器都会成为候选者,并向其他服务器发送选举请求。集群中的服务器会根据规则进行投票。
  4. 初始化投票:当一个服务器成为候选者时,它会初始化一个投票。这个投票包括服务器的标识符(myid)、ZXID(ZooKeeper事务ID)和一个状态(如LOOKING)等。
  5. 发送投票请求:候选者会向其他服务器发送投票请求。每个服务器会将自己的投票响应发送回给候选者。
  6. 收集和处理投票:候选者会收集所有服务器发送的投票,并根据投票的规则进行处理。通常情况下,候选者会根据ZXID和服务器的标识符进行投票的计算和比较。
  7. 更改状态:候选者会根据投票结果修改自己的状态。如果它得到了大多数投票(超过半数),则会更改状态为LEADING,并成为leader;否则,它将继续参与选举过程。
  8. 选举结果通知:当一个服务器成为leader后,它会将选举结果通知给其他服务器。其他服务器在收到通知后会更新自己的状态,并认可该服务器为leader。
  9. 与客户端连接:leader将会为客户端提供服务,并处理客户端的请求。

需要注意的是,如果leader节点宕机或网络分区发生,ZooKeeper会触发新的选举过程以选取新的leader。这样可以确保集群的高可用性和持续的服务。

以上就是ZooKeeper选取leader的详细过程。选举过程保证了在集群中始终有一个可靠的leader节点,同时提供了良好的容错性和可用性。

假设现在有myid为1、2、3的三台ZooKeeper服务器,分别启动1、2、3三台服务,下面是集群启动时的选举过程的详细描述:

  1. 初始状态:集群中的所有服务器处于LOOKING状态,即正在寻找leader节点。
  2. 服务器1启动:服务器1作为第一台启动的服务器,它将成为Leader选举的候选者,并向其他服务器发送选举请求。
  3. 服务器1的选举投票:服务器1将自己的投票信息(包括标识符myid=1、ZXID和状态LOOKING)发送给服务器2和服务器3。
  4. 服务器2和服务器3收到选举请求:服务器2和服务器3收到来自服务器1的选举请求,并检查自己的状态。
  5. 服务器2和服务器3的选举投票:服务器2和服务器3会分别初始化自己的投票,并将自己的投票信息发送回给服务器1。
  6. 服务器1收集和处理投票:服务器1收到来自服务器2和服务器3的投票,它将根据规则进行投票的计算和比较。如果服务器1得到了大多数的投票(超过半数,即两票),它将更改自己的状态为LEADING,并成为leader。
  7. 选举结果通知:服务器1成为leader后,它将选举结果通知给服务器2和服务器3。服务器2和服务器3会更新自己的状态,并认可服务器1作为leader。
  8. 集群状态稳定:现在,服务器1成为了leader,而服务器2和服务器3成为了follower。整个集群的状态稳定下来,Leader将开始处理客户端的请求。

请注意,以上是一种可能的选举过程。在实际情况中,选举的结果可能因为网络延迟、服务器响应速度等原因而有所不同。此外,在初始启动期间,如果没有绝大多数的服务器参与选举(如只有两台服务器启动),选举过程可能无法达到一致,集群无法选取到leader。

在ZooKeeper中,选举是通过基于Paxos的Zab协议来实现的,它提供了强一致性和高可用性。选举过程确保了在集群中始终有一个leader节点,以提供可靠的服务和数据一致性。

3、脑裂问题

什么是脑裂?

在Elasticsearch、ZooKeeper这些集群环境中,有一个共同的特点,就是它们有一个“大脑”。比如,Elasticsearch集群中有Master节点,ZooKeeper集群中有Leader节点。

集群中的Master或Leader节点往往是通过选举产生的。在网络正常的情况下,可以顺利的选举出Leader(后续以Zookeeper命名为例)。但当两个机房之间的网络通信出现故障时,选举机制就有可能在不同的网络分区中选出两个Leader。当网络恢复时,这两个Leader该如何处理数据同步?又该听谁的?这也就出现了“脑裂”现象。

通俗的讲,脑裂(split-brain)就是“大脑分裂”,本来一个“大脑”被拆分成两个或多个。试想,如果一个人有多个脑,且相互独立,就会导致人体“手舞足蹈”,“不听使唤”。

了解了脑裂的基本概念,下面就以zookeeper集群的场景为例,来分析一下脑裂的发生。

zookeeper集群中的脑裂

我们在使用zookeeper时,很少遇到脑裂现象,是因为zookeeper已经采取了相应的措施来减少或避免脑裂的发生,这个后面会讲到Zookeeper的具体解决方案。现在呢,先假设zookeeper没有采取这些防止脑裂的措施。在这种情况下,看看脑裂问题是如何发生的。

现有6台zkServer服务组成了一个集群,部署在2个机房:

brain-01-6a4525d0.jpg

正常情况下,该集群只有会有个Leader,当Leader宕掉时,其他5个服务会重新选举出一个新的Leader。

如果机房1和机房2之间的网络出现故障,暂时不考虑Zookeeper的过半机制,那么就会出现下图的情况:

brain-02-35fae7ca.jpg

也就是说机房2的三台服务检测到没有Leader了,于是开始重新选举,选举出一个新Leader来。原本一个集群,被分成了两个集群,同时出现了两个“大脑”,这就是所谓的“脑裂”现象。

由于原本的一个集群变成了两个,都对外提供服务。一段时间之后,两个集群之间的数据可能会变得不一致了。当网络恢复时,就面临着谁当Leader,数据怎么合并,数据冲突怎么解决等问题。

当然,上面的过程只是我们假设Zookeeper不做任何预防脑裂措施时会出现的问题。那么,针对脑裂问题,Zookeeper是如何进行处理的呢?

Zookeeper的过半原则

防止脑裂的措施有多种,Zookeeper默认采用的是“过半原则”。所谓的过半原则就是:在Leader选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。

底层源码实现如下:

public class QuorumMaj implements QuorumVerifier {

    int half;

    // QuorumMaj构造方法。
    // 其中,参数n表示集群中zkServer的个数,不包括观察者节点
    public QuorumMaj(int n){
        this.half = n/2;
    }

    // 验证是否符合过半机制
    public boolean containsQuorum(Set<Long> set){

        // half是在构造方法里赋值的
        // set.size()表示某台zkServer获得的票数
        return (set.size() > half);
    }
}

上述代码在构建QuorumMaj对象时,传入了集群中有效节点的个数;containsQuorum方法提供了判断某台

zkServer获得的票数是否超过半数,其中set.size表示某台zkServer获得的票数。

上述代码核心点两个:第一,如何计算半数;第二,投票属于半数的比较。

以上图6台服务器为例来进行说明:half = 6 / 2 = 3,也就是说选举的时候,要成为Leader至少要有4台机器投票才能够选举成功。那么,针对上面2个机房断网的情况,由于机房1和机房2都只有3台服务器,根本无法选举出Leader。这种情况下整个集群将没有Leader。

brain-03-f385f13c.jpg

在没有Leader的情况下,会导致Zookeeper无法对外提供服务,所以在设计的时候,我们在集群搭建的时候,要避免这种情况的出现。

brain-04-20e452da.jpg

对于上图,颠倒过来也一样,比如机房1只有2台服务器,机房2有三台服务器,当网络断开时,选举情况如下:

Zookeeper集群通过过半机制,达到了要么没有Leader,要没只有1个Leader,这样就避免了脑裂问题。

对于过半机制除了能够防止脑裂,还可以实现快速的选举。因为过半机制不需要等待所有zkServer都投了同一个zkServer就可以选举出一个Leader,所以也叫快速领导者选举算法。

新旧Leader争夺

通过过半原则可以防止机房分区时导致脑裂现象,但还有一种情况就是Leader假死。

假设某个Leader假死,其余的followers选举出了一个新的Leader。这时,旧的Leader复活并且仍然认为自己是Leader,向其他followers发出写请求也是会被拒绝的。

因为ZooKeeper维护了一个叫epoch的变量,每当新Leader产生时,会生成一个epoch标号(标识当前属于那个Leader的统治时期),epoch是递增的,followers如果确认了新的Leader存在,知道其epoch,就会拒绝epoch小于现任leader epoch的所有请求。

那有没有follower不知道新的Leader存在呢,有可能,但肯定不是大多数,否则新Leader无法产生。ZooKeeper的写也遵循quorum机制,因此,得不到大多数支持的写是无效的,旧leader即使各种认为自己是Leader,依然没有什么作用。

ZooKeeper集群节点为什么要部署成奇数

上面讲了过半原则,由于Zookeeper默认采用的就是这种策略,那就带来另外一个问题。集群的数量设置为多少合适呢?而我们所看到的Zookeeper节点数一般都是奇数,这是为什么呢?

首先,只要集群中有过半的机器是正常工作的,那么整个集群就可对外服务。那么我们列举一些情况,来看看在这些情况下集群的容错性。

如果有2个节点,那么只要挂掉1个节点,集群就不可用了。此时,集群对的容忍度为0;

如果有3个节点,那么挂掉1个节点,还有剩下2个正常节点,超过半数,可以重新选举,正常服务。此时,集群的容忍度为1;

如果有4个节点,那么挂掉1个节点,剩下3个,超过半数,可以重新选举。但如果再挂掉1个,只剩下2个,就无法正常选举和服务了。此时,集群的容忍度为1;

依次类推,5个节点,容忍度为2;6个节点容忍度同样为2;

既然3个节点和4个节点、5个节点和6个节点,也就是2n和2n-1的容忍度是一样的,都是n-1。那么,为了节省资源,为了更加高效(更多节点参与选举和通信),为什么不少一个节点呢?这就是为什么集群要部署成奇数的原因。