ZooKeeper简述

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

Zookeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心(提供发布订阅服务)

zookeeper的知识点

1.会话Session

在zookeeper中,一个客户端连接指的是客户端与服务器之间的一个TCP长连接。

客户端启动时,会先与服务器建立一个TCP连接,通过这个链接,客户端能够通过心跳检测与服务器保持有效会话,也能向zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知。

Session的sessionTimeout用来设置一个客户端会话的超时事件,因为一系列原因客户端连接断开时,只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。

在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。

2.数据模型

在Zookeeper的数据结构和Unix文件系统相似,都是在根节点下挂很多子节点,但zookeeper没有文件和目录的概念,而使用了znode作为数据节点。znode是zookeeper中最小的数据单元。

每个znode都有自己的节点类型节点状态

  1. 节点类型
    • 持久节点:一旦创建就一直存在,直到将其删除
    • 持久顺序节点:一个父节点可以为其子节点 维护一个创建的先后顺序 ,这个顺序体现在 节点名称 上,是节点名称后自动添加一个由 10 位数字组成的数字串,从 0 开始计数。
    • 临时节点:临时节点的生命周期是与 客户端会话 绑定的,会话消失则节点消失 。临时节点 只能做叶子节点 ,不能创建子节点。
    • 临时顺序节点:父节点可以创建一个维持了顺序的临时节点(和前面的持久顺序性节点一样)。
  2. 节点状态,zookeeper中是使用Stat这个类来维护的
    • czxid:Created ZXID,该数据节点被创建时的事务ID
    • mzxid:Modified ZXID,节点最后一次被更新时的事务ID
    • ctime:Created Time,该节点被创建的时间
    • mtime:Modified Time,节点最后一次被修改的时间
    • version:节点的版本号。
    • cversion:子节点的版本号。
    • aversion:节点的ACL版本号。
    • ephemeralOwner:创建该节点的会话的sessionID,如果该节点为持久节点,该值为0
    • dateLength:节点数据内容的长度。
    • numChildre:该节点的子节点个数,如果为临时节点为0
    • pzxid:该节点子节点列表最后一次被修改是的事务ID,注意是子节点的列表

3.版本

在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个ZNode,Zookeeper 都会为其维护一个叫作 Stat 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是:

  1. version(当前ZNode的版本);
  2. cversion(当前ZNode子节点的版本);
  3. aversion(当前ZNode的ACL版本)。

4.Watcher

Watcher(事件监听器),是Zookeeper中的一个很重要的特性。

步骤为:

  1. 客户端向服务端注册指定的watcher
  2. 当服务端满足了watcher的某些事件则会向客户端发送事件通知
  3. 客户端收到通知后找到自己定义watcher,然后执行相应的回调方法。

5.ACL

ZooKeeper采用ACL(AccessControlLists)策略来进行权限控制。ZooKeeper定义了如下5种权限。

  • CREATE:创建子节点的权限
  • READ:获取节点数据和子节点列表的权限
  • WIRTE:更新节点数据的权限
  • DELETE:删除子节点的权限
  • ADMIN:设置节点ACL的权限

其中CREATE和ADMIN都是针对子节点的权限控制

重要概念总结

  • ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。
  • 为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。
  • ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。
  • ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)
  • ZooKeeper有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。
  • ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提供数据节点监听服务。

Zookeeper的特点

  • 顺序一致性:从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到Zookeeper中去。
  • 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
  • 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
  • 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。

ZooKeeper理论知识

1. 简单的数据模型

zookeeper允许分布式进程通过共享层次结构命名空间进行相互协调。

名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。
image.png

2.可构建集群

为了保证高可用,最好是以集群形态来部署zookeeper,这样只要集群中大部分机器时可用的,那么zookeeper本身仍然是可用的
image.png
上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic Broadcast)来保持数据的一致性。

3.顺序访问

对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper 这个特性来实现更高层次的同步原语。 这个编号也叫做时间戳——zxid(Zookeeper Transaction Id)

4.高性能

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

ZooKeeper集群角色介绍

zookeeper在传统的主备模式(Master/Slave)上引入了Leader、Follower和Observer三种角色:
image.png
要注意的有:

  1. Leader既可以为客户端提供读服务又可以提供写服务,Follower和Observer都只能提供读服务
  2. Follower和Observer的区别在于Observer机器不参与Leader的选举过程,也不参与写操作的“过半写成功”策略,因此Observer机器可以在不影响写性能的情况下提升集群的读性能。

image.png

关于Leader选举

image.png

队列是保证Follower和Observer的顺序性。 何为顺序性,比如我现在有一个写请求A,此时 Leader 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 Follower F1因为网络原因没有收到,而 Leader 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 产生数据不一致问题

当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进入恢复模式并选举产生新的Leader服务器。这个过程大致是这样的:

  1. Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。
  2. Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。
  3. Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 准 leader 才会成为真正的 leader。
  4. Broadcast(广播阶段) 到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。

Paxos算法和ZAB协议

Paxos算法

Paxos算法是基于消息传递且具有高度容错特性的一致性算法,是目前公认的解决分布式一致性问题最有效的算法之一,其解决的问题就是在分布式系统中如何就某个值达成一致。

在Paxos中主要有三个角色:

  1. Proposer提案者;
  2. Acceptor表决者;
  3. Learner学习者。

有两个阶段:1. Prepare阶段;2.Accept阶段

Prepare阶段

  • Proposer提案者:负责提出proposal,每个提案者在提出提案时都会获取到一个具有全局唯一性的、递增的提案编号N,即在整个集群中是唯一的编号N,第一阶段只是将提案编号发给表决者
  • 每个表决者会存储已经被accept的提案中编号最大的提案,假设最大的提案号为maxN,那么每个表决者仅会accept编号大于自己本地maxN的提案

Accept阶段

当一个提案被Proposer提出后,如果Proposer收到了半数的Acceptor的批准,那么此时Proposer会给所有的Acceptor发送真正的提案(之前只是发提案号),该提案包括内容和提案编号。

表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 大于等于 已经批准过的最大提案编号,那么就 accept 该提案(此时执行提案内容但不提交),随后将情况返回给 Proposer 。如果不满足则不回应或者返回 NO 。

Proposer 收到超过半数的 accept

  1. Proposer 对于已批准提案的Acceptor,让其提交就行了。
  2. 其他没有批准的并没有执行该提案内容,所以这个时候需要向未批准的 acceptor 发送提案内容和提案编号并让它无条件执行和提交

而如果 Proposer 如果没有收到超过半数的 accept 那么它将会将 递增Proposal 的编号,然后 重新进入 Prepare 阶段

Paxos死循环问题

比如说:

  1. 提案者 P1 提出一个方案 M1,完成了 Prepare 阶段的工作,这个时候 acceptor 则批准了 M1。
  2. 但是此时提案者 P2 同时也提出了一个方案 M2,它也完成了 Prepare 阶段的工作。然后 P1 的方案已经不能在第二阶段被批准了(因为 acceptor 已经批准了比 M1 更大的 M2)
  3. 所以 P1 自增方案变为 M3 重新进入 Prepare 阶段,然后 acceptor ,又批准了新的 M3 方案,它又不能批准 M2 了
  4. 这个时候 M2 又自增进入 Prepare 阶段。。。

解决的方法就是允许一个能提案就行了,故引出ZAB

ZAB 协议介绍

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

ZAB特性

  1. 之前介绍过了顺序性的重要性

Follower1因为网络原因没有接受到请求A,但是Leader又广播了一个请求B,所以Follower1先接收到了请求B再接收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而产生数据不一致的问题。

  1. 次在ZAB中还定义了一个全局单调递增的事务IDZXID,它是一个64位long型,其中高32位表示epoch年代,低32位表示事务id。
    • 高32位代表epoch,epoch会根据Leader的变化而变化的,当一个Leader挂了,新的Leader上位时,年代epoch就变了。
    • 低32位可以理解为递增的事务id

定义这个的原因也是为了顺序性,每个 proposalLeader 中生成后需要 通过其 ZXID 来进行排序 ,才能得到处理。

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

ZAB协议包括两种基本的模式,分别是 崩溃恢复和消息广播

崩溃恢复模式

Leader重新选举分为Leader宕机需要重新选举,Zookeeper启动时需要进行系统的Leader初始化选举。

  1. 初始化选举:
    • 假设集群有三台机器,意味着两台以上同意。这时启动了server1,首先会投票给自己,投票内容为服务器的myid和ZXID,因为初始化所有ZXID都为0,所以server1的投票内容为(1,0)。server1的投票仅为1,不能作为Leader,所以整个集群暂时除以Looking阶段。
    • 接着Server2启动,并且会投票给自己,并将投票广播(2,0)发出
    • Server1在收到后会与自己的投票信息进行比较:首先会比较ZXID,ZXID大的优先为Leader,若相同则比较myid,myid大的优先作为Leader。所以server1将投票信息改为(2,0)并广播。
    • Server2收到广播后,发现投票数已过半,确定server2为Leader。
    • Server1会将服务器从Following变为Follower。整个服务器从Looking变为正常状态。
    • server3启动发现集群没有处于Looking,会直接以Follower身份加入集群。
  2. 崩溃重新选举:
    • 接着上面的例子,如果server2挂了,Server1和Server3会从Following变为looking。然后每个server会给自己投票。
    • server1和server3会受到彼此的投票,进经过比较后,发现其他server比自己更适合,则修改自己的选票再次广播。
    • 过半则把自己设为Leader。

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

消息广播模式

当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进入消息广播模式了。 当一台同样遵守ZAB协议的服务器启动后加入到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。

Zookeeper的经典应用场景

选主

因为zookeeper的一致性,能够保证在高并发的情况下保证节点创建的全局唯一性。

  • 利用这个特性,可以让多个客户端创建一个指定的临时节点,创建成功的就是master。
  • 如果该master挂了,意味着会话断了,也就意味着这个节点没了(因为是临时节点)
  • 我们可以让其他节点利用watcher监听节点的状态。比如监听master的父节点,如果该节点的子节点书变了,则触发回调函数进行重新选举,或者直接监听节点状态,可以通过节点是否失去连接来判断master是否挂了。

image.png

分布式锁

因为创建节点的唯一性,我们可以让多个客户端同时创建一个临时节点,创建成功的就说明获取到了锁 。然后没有获取到锁的客户端也像上面选主的非主节点创建一个 watcher 进行节点状态的监听,如果这个互斥锁被释放了(可能获取锁的客户端宕机了,或者那个客户端主动释放了锁)可以调用回调函数重新获得锁。

命名服务

我们之前提到过 zookeeper 是通过 树形结构 来存储数据节点的,那也就是说,对于每个节点的 全路径,它必定是唯一的,我们可以使用节点的全路径作为命名方式了。而且更重要的是,路径是我们可以自己定义的,这对于我们对有些有语意的对象的ID设置可以更加便于理解。

集群管理和注册中心

zookeeper 天然支持的 watcher 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 watcher 进行状态监控和回调。

至于注册中心也很简单,我们同样也是让 服务提供者zookeeper 中创建一个临时节点并且将自己的 ip、port、调用方式 写入节点,当 服务消费者 需要进行调用的时候会 通过注册中心找到相应的服务的地址列表(IP端口什么的) ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。

当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 Eureka 会先试错,然后再更新)。