ZooKeeper

杨大大...大约 18 分钟

1.基本功能

ZooKeeper 是一个开源的分布式协调服务,它的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。

ZooKeeper 为我们提供了高可用、高性能、稳定的分布式数据一致性解决方案,通常被用于实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。这些功能的实现主要依赖于 ZooKeeper 提供的 数据存储+事件监听 功能。

ZooKeeper 将数据保存在内存中,性能是不错的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景)。

另外,很多顶级的开源项目都用到了 ZooKeeper,比如:

  • Kafka : ZooKeeper 主要为 Kafka 提供 Broker 和 Topic 的注册以及多个 Partition 的负载均衡等功能。不过,在 Kafka 2.8 之后,引入了基于 Raft 协议的 KRaft 模式,不再依赖 Zookeeper,大大简化了 Kafka 的架构。
  • Kafka严重依赖于ZooKeeper集群。所有的broker在启动的时候都会往zookeeper进行注册,目的就是选举出一个controller,controller会读取注册上来的从节点的数据(通过监听机制),生成集群的元数据信息,之后把这些信息都分发给其他的服务器,让其他服务器能感知到集群中其它成员的存在 。通过这个方法让整个集群都得知这个分区方案,此时从节点就各自创建好目录等待创建分区副本即可。这也是整个集群的管理机制。

kafka已经取消了依赖zookeeper

最新版的Kafka 2.8.0,移除了对Zookeeper的依赖,通过KRaft进行自己的集群管理。

Kafka作为一个消息队列,竟然要依赖一个重量级的协调系统ZooKeeper,不得不说是一个笑话。同样作为消息队列,人家RabbitMQ早早的就实现了自我管理。

Zookeeper非常笨重,还要求奇数个节点的集群配置,扩容和缩容也不方便。Zk的配置方式,也和kafka的完全不一样,要按照调优Kafka,竟然还要兼顾另外一个系统。

Kafka要想往轻量级,开箱即用的方向发展,就不得不干掉Zk。

2.Paxos协议

3.Raft协议

3.1什么是分布式一致性 ?

分布式系统通常由异步网络连接的多个节点构成,每个节点有独立的计算和存储,节点之间通过网络通信进行协作。分布式一致性指多个节点对某一变量的取值达成一致,一旦达成一致,则变量的本次取值即被确定。

在大量客户端并发请求读/写的情况下,维护数据多副本的一致性无疑非常重要,且富有挑战。因此,分布式一致性在我们生产环境中显得尤为重要。

总结来讲,分布式一致性就是为了解决以下两个问题:

  • 数据不能存在单个节点(主机)上,否则可能出现单点故障。
  • 多个节点(主机)需要保证具有相同的数据。

3.2常见分布式一致性算法

常见的一致性算法包括Paxos算法,Raft算法,ZAB算法等,

  • Paxos算法是Lamport宗师提出的一种基于消息传递的分布式一致性算法,使其获得2013年图灵奖。自Paxos问世以来就持续垄断了分布式一致性算法,Paxos这个名词几乎等同于分布式一致性, 很多分布式一致性算法都由Paxos演变而来。
  • Paxos是出了名的难懂,而Raft正是为了探索一种更易于理解的一致性算法而产生的。它的首要设计目的就是易于理解,所以在选主的冲突处理等方式上它都选择了非常简单明了的解决方案。
  • ZAB 协议全称:Zookeeper Atomic Broadcast(Zookeeper 原子广播协议), 它应该是所有一致性协议中生产环境中应用最多的了。为什么呢?因为他是为 Zookeeper 设计的分布式一致性协议!

3.3Raft算法基础

Raft算法和其他分布式一致算法一样,内部采用如下图所示的复制状态机模型,在这个模型中,会利用多台服务器构成一个集群,工作流程如下图所示:

Raft算法在具体实现中,将分布式一致性问题分解为了Leader选举日志同步安全性保证三大子问题。

Raft 正常工作时的流程如下图,也就是正常情况下日志复制的流程。Raft 中使用日志来记录所有操作,所有结点都有自己的日志列表来记录所有请求。算法将机器分成三种角色:LeaderFollowerCandidate。正常情况下只存在一个 Leader,其他均为 Follower,所有客户端都与 Leader 进行交互。

所有操作采用类似两阶段提交的方式,Leader 在收到来自客户端的请求后并不会执行,只是将其写入自己的日志列表中,然后将该操作发送给所有的 Follower。Follower 在收到请求后也只是写入自己的日志列表中然后回复 Leader,当有超过半数的结点写入后 Leader 才会提交该操作并返回给客户端,同时通知所有其他结点提交该操作。

通过这一流程保证了只要提交过后的操作一定在多数结点上留有记录(在日志列表中),从而保证了该数据不会丢失。

Raft 是一个非拜占庭的一致性算法,即所有通信是正确的而非伪造的。N个结点的情况下(N为奇数)可以最多容忍(N-1)/2个结点故障。如果更多的节点故障,后续的Leader选举和日志同步将无法进行。

参考:https://juejin.cn/post/7218915344130359351open in new window

3.ZAB协议

3.1ZAB 协议介绍

ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。这里的主备系统架构模型,就是指只有一台客户端(Leader)负责处理外部的写事务请求,然后Leader客户端将数据同步到其他Follower节点。

客户端的读取流程:客户端会随机的链接到 zookeeper 集群中的一个节点,如果是读请求,就直接从当前节点中读取数据;如果是写请求,那么节点就会向 Leader 提交事务,Leader 接收到事务提交,会广播该事务,只要超过半数节点写入成功,该事务就会被提交。

ZAB 协议两种基本的模式:崩溃恢复和消息广播

  • 崩溃恢复 :当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的 Leader 服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该 Leader 服务器完成了状态同步之后,ZAB 协议就会退出恢复模式。其中,所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和 Leader 服务器的数据状态保持一致。说白了就是 ZAB 协议是如何处理写请求的。那么 FollowerObserver 也需要 同步更新数据

    第一步 Leader 将写请求 广播 出去,让 Leader 问问 Followers 是否同意更新,如果超过半数以上的同意那么就进行 FollowerObserver 的更新。

    Leader 这端,它为每个其他的 zkServer 准备了一个 队列 ,采用先进先出的方式发送消息。由于协议是 通过 TCP 来进行网络通信的,保证了消息的发送顺序性,接受顺序性也得到了保证。

    除此之外,在 ZAB 中还定义了一个 全局单调递增的事务ID ZXID ,它是一个64位long型,其中高32位表示 epoch 年代,低32位表示事务id。epoch 是会根据 Leader 的变化而变化的,当一个 Leader 挂了,新的 Leader 上位的时候,年代(epoch)就变了。而低32位可以简单理解为递增的事务id。

  • 消息广播当集群中已经有过半的 Follower 服务器完成了和 Leader 服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。 当一台同样遵守 ZAB 协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个 Leader 服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到 Leader 所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。

3.2消息广播

Zookeeper集群中,存在以下三种角色的节点: Leader:Zookeeper集群的核心角色,在集群启动或崩溃恢复中通过Follower参与选举产生,为客户端提供读写服务,并对事务请求进行处理Follower:Zookeeper集群的核心角色,在集群启动或崩溃恢复中参加选举,没有被选上就是这个角色,为客户端提供读取服务,也就是处理非事务请求,Follower不能处理事务请求,对于收到的事务请求会转发给Leader。 Observer:观察者角色,不参加选举,为客户端提供读取服务,处理非事务请求,对于收到的事务请求会转发给Leader。使用Observer的目的是为了扩展系统,提高读取性能。

  1. Leader 接收到消息请求后,将消息赋予一个全局唯一的 64 位自增 id,叫做:zxid,通过 zxid 的大小比较即可实现因果有序这一特性。
  2. Leader 通过先进先出队列(通过 TCP 协议来实现,以此实现了全局有序这一特性)将带有 zxid 的消息作为一个提案(proposal)分发给所有 follower。
  3. 当 follower 接收到 proposal,先将 proposal 写到硬盘,写硬盘成功后再向 leader 回一个 ACK。
  4. 当 leader 接收到合法数量的 ACKs 后,leader 就向所有 follower 发送 COMMIT 命令,同时会在本地执行该消息。
  5. 当 follower 收到消息的 COMMIT 命令时,就会执行该消息。

相比于完整的二阶段提交,Zab 协议最大的区别就是不能终止事务,follower 要么回 ACK 给 leader,要么抛弃 leader,在某一时刻,leader 的状态与 follower 的状态很可能不一致,因此它不能处理 leader 挂掉的情况,所以 Zab 协议引入了恢复模式来处理这一问题。

从另一角度看,正因为 Zab 的广播过程不需要终止事务,也就是说不需要所有 follower 都返回 ACK 才能进行 COMMIT,而是只需要合法数量(2n+1 台服务器中的 n+1 台) 的follower,也提升了整体的性能。

Leader 服务器与每一个 Follower 服务器之间都维护了一个单独的 FIFO 消息队列进行收发消息,使用队列消息可以做到异步解耦。 Leader 和 Follower 之间只需要往队列中发消息即可。如果使用同步的方式会引起阻塞,性能要下降很多。

3.3崩溃恢复

3.3.1选举 Leader

当系统出现崩溃影响最大应该是 Leader 的崩溃,因为我们只有一个 Leader ,所以当 Leader 出现问题的时候我们势必需要重新选举 Leader

Leader 选举可以分为两个不同的阶段,第一个是我们提到的 Leader 宕机需要重新选举,第二则是当 Zookeeper 启动时需要进行系统的 Leader 初始化选举。下面我先来介绍一下 ZAB 是如何进行初始化选举的。

假设我们集群中有3台机器,那也就意味着我们需要两台以上同意(超过半数)。比如这个时候我们启动了 server1 ,它会首先 投票给自己 ,投票内容为服务器的 myidZXID ,因为初始化所以 ZXID 都为0,此时 server1 发出的投票为 (1,0)。但此时 server1 的投票仅为1,所以不能作为 Leader ,此时还在选举阶段所以整个集群处于 Looking 状态

接着 server2 启动了,它首先也会将投票选给自己(2,0),并将投票信息广播出去(server1也会,只是它那时没有其他的服务器了),server1 在收到 server2 的投票信息后会将投票信息与自己的作比较。首先它会比较 ZXIDZXID 大的优先为 Leader,如果相同则比较 myidmyid 大的优先作为 Leader。所以此时server1 发现 server2 更适合做 Leader,它就会将自己的投票信息更改为(2,0)然后再广播出去,之后server2 收到之后发现和自己的一样无需做更改,并且自己的 投票已经超过半数 ,则 确定 server2Leaderserver1 也会将自己服务器设置为 Following 变为 Follower。整个服务器就从 Looking 变为了正常状态。

server3 启动发现集群没有处于 Looking 状态时,它会直接以 Follower 的身份加入集群。

还是前面三个 server 的例子,如果在整个集群运行的过程中 server2 挂了,那么整个集群会如何重新选举 Leader 呢?其实和初始化选举差不多。

首先毫无疑问的是剩下的两个 Follower 会将自己的状态 Following 变为 Looking 状态 ,然后每个 server 会向初始化投票一样首先给自己投票(这不过这里的 zxid 可能不是0了,这里为了方便随便取个数字)。

假设 server1 给自己投票为(1,99),然后广播给其他 serverserver3 首先也会给自己投票(3,95),然后也广播给其他 serverserver1server3 此时会收到彼此的投票信息,和一开始选举一样,他们也会比较自己的投票和收到的投票(zxid 大的优先,如果相同那么就 myid 大的优先)。这个时候 server1 收到了 server3 的投票发现没自己的合适故不变,server3 收到 server1 的投票结果后发现比自己的合适于是更改投票为(1,99)然后广播出去,最后 server1 收到了发现自己的投票已经超过半数就把自己设为 Leaderserver3 也随之变为 Follower

请注意 ZooKeeper 为什么要设置奇数个结点?比如这里我们是三个,挂了一个我们还能正常工作,挂了两个我们就不能正常工作了(已经没有超过半数的节点数了,所以无法进行投票等操作了)。而假设我们现在有四个,挂了一个也能工作,但是挂了两个也不能正常工作了,这是和三个一样的,而三个比四个还少一个,带来的效益是一样的,所以 Zookeeper 推荐奇数个 server

3.3.2确保已经被Leader提交的提案最终能够被所有的Follower提交:

假设 Leader (server2) 发送 commit 请求,他发送给了 server3,然后要发给 server1 的时候突然挂了。这个时候重新选举的时候我们如果把 server1 作为 Leader 的话,那么肯定会产生数据不一致性,因为 server3 肯定会提交刚刚 server2 发送的 commit 请求的提案,而 server1 根本没收到所以会丢弃。

那怎么解决呢?

这个时候 server1 已经不可能成为 Leader 了,因为 server1server3 进行投票选举的时候会比较 ZXID ,而此时 server3**ZXID 肯定比 server1 的大了。

3.3.3跳过那些已经被丢弃的提案:

假设 Leader (server2) 此时同意了提案N1,自身提交了这个事务并且要发送给所有 Followercommit 的请求,却在这个时候挂了,此时肯定要重新进行 Leader 的选举,比如说此时选 server1Leader 。但是过了一会,这个 挂掉的 Leader 又重新恢复了 ,此时它肯定会作为 Follower 的身份进入集群中,需要注意的是刚刚 server2 已经同意提交了提案N1,但其他 server 并没有收到它的 commit 信息,所以其他 server 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 该提案N1最终需要被抛弃掉

3.4Zookeeper 架构

作为一个优秀高效且可靠的分布式协调框架,ZooKeeper 在解决分布式数据一致性问题时并没有直接使用 Paxos ,而是专门定制了一致性协议叫做 ZAB(ZooKeeper Atomic Broadcast) 原子广播协议,该协议能够很好地支持 崩溃恢复

ZAB 中的三个角色

ZAB 中三个主要的角色,Leader 领导者Follower跟随者Observer观察者

  • Leader :集群中 唯一的写请求处理者 ,能够发起投票(投票也是为了进行写请求)。
  • Follower:能够接收客户端的请求,如果是读请求则可以自己处理,如果是写请求则要转发给 Leader 。在选举过程中会参与投票,有选举权和被选举权
  • Observer :就是没有选举权和被选举权的 Follower

4.CAP

4.1简介

CAP 也就是 Consistency(一致性)Availability(可用性)Partition Tolerance(分区容错性) 这三个单词首字母组合。

在理论计算机科学中,CAP 定理指出对于一个分布式系统来说,当设计读写操作时,只能同时满足以下三点中的两个:

  • 一致性(Consistency) : 所有节点访问同一份最新的数据副本
  • 可用性(Availability): 非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
  • 分区容错性(Partition Tolerance) : 分布式系统出现网络分区的时候,仍然能够对外提供服务。

什么是网络分区?

分布式系统中,多个节点之前的网络本来是连通的,但是因为某些故障(比如部分节点网络出了问题)某些节点之间不连通了,整个网络就分成了几块区域,这就叫 网络分区

4.2不是所谓的“3 选 2”

大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法,而且在 CAP 理论诞生 12 年之后,CAP 之父也在 2012 年重写了之前的论文。

当发生网络分区的时候,如果我们要继续服务,那么强一致性和可用性只能 2 选 1。也就是说当网络分区之后 P 是前提,决定了 P 之后才有 C 和 A 的选择。也就是说分区容错性(Partition tolerance)我们是必须要实现的。

简而言之就是:CAP 理论中分区容错性 P 是一定要满足的,在此基础上,只能满足可用性 A 或者一致性 C。

因此,分布式系统理论上不可能选择 CA 架构,只能选择 CP 或者 AP 架构。 比如 ZooKeeper 就是 CP 架构,Cassandra、Eureka 就是 AP 架构,Nacos 不仅支持 CP 架构也支持 AP 架构。

为啥不可能选择 CA 架构呢? 举个例子:若系统出现“分区”,系统中的某个节点在进行写操作。为了保证 C, 必须要禁止其他节点的读写操作,这就和 A 发生冲突了。如果为了保证 A,其他节点的读写操作正常的话,那就和 C 发生冲突了。

选择 CP 还是 AP 的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP 。

另外,需要补充说明的一点是: 如果网络分区正常的话(系统在绝大部分时候所处的状态),也就说不需要保证 P 的时候,C 和 A 能够同时保证。