zookeeper使用场景

介绍

  zookeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架。

作用:

  1. 高性能使得ZooKeeper能够应用于对系统吞吐有明确要求的大型分布式系统。
  2. 高可用可以解决分布式的单点问题。
  3. 具有严格的顺序访问控制能力,主要是针对写操作的严格顺序性,使得客户端可以基于ZooKeeper来实现一些复杂的同步原语。

概念:

  ZooKeepr提供基于类似于文件系统的目录节点树方式的数据存储,这是一个共享的内存中的树型结构。
有几个概念需要关注一下:

  1. Session会话,客户端启动会与服务端建立一个TCP长连接,通过这个连接可以发送请求并接受响应,以及接受服务端的Watcher事件通知。
  2. Znode数据节点,/xxxx就是一个Znode,会保存自己的数据内容和属性信息,分为持久和临时节点,节点有SEQUENTIAL属性。
  3. Version版本,Stat数据结构包含version,cversion,aversion。
  4. Watcher事件监听器,客户端可以在Znode上注册Watcher,服务端将事件通知已注册的客户端。

使用场景:

利用zookeeper可以非常构建一系列分布式应用中都会涉及到的核心功能:

  1. 数据发布/订阅

  2. 负载均衡

  3. 命名服务

  4. 分布式协调/通知

  5. 集群管理

  6. Master选举

  7. 分布式锁

  8. 分布式队列

    多个开源项目中都用到了,如dubbo,kafka等。

数据发布与订阅(采用watche机制)

  数据发布订阅等一个常见场景是配置中心,发布者将数据发布到zookeeper的一个或一系列节点上,供订阅者进行数据订阅,达到动态获取数据的目的。

配置信息一般有几个特点:

  1. 数据量小的KV
  2. 数据内容在运行时会发生动态变化
  3. 集权机器共享,配置一致

zookeeper采用的是推拉结合的方式:

  1. 推:服务器会推给注册了监控节点的客户端Watcher时间通知。
  2. 拉:客户端获得了通知后,然后主动到服务端拉取最新的数据。

实现的思路如下:

  1. 把配置信息写到一个znode上
  2. 客户端启动初始化阶段读取服务端节点的数据,并注册一个数据变更的Watcher
  3. 配置变更只需要对Znode数据进行set操作,数据变更的通知会发送到客户端,客户端重新获取数据,完成配置动态修改。

负载均衡

  负载均衡是一种手段,用来把对某种资源的访问分摊给不同的设备,从而减轻单点的压力。

实现的思路:

  1. 首先建立servers节点,并建立监听器监视servers子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)
  2. 在每个服务器启动时,在Servers节点下面建立临时子节点Worker Server,并在对应的子节点下面存入服务器的相关信息,包括服务器的地址,ip,端口等。
  3. 可以自定义一个负载均衡算法,在每个请求过来时从zookeeper服务器中获取当前集群服务器列表,根据算法选出其中一个服务器来处理请求。

命名服务

  命名服务就是提供名城的服务,zookeeper的命名服务主要有两个应用方面。

  1. 提供类JNDI功能,可以把系统中各种服务的名称、地址以及目录信息存放在zookeeper,需要的时候从zookeeper中读取。

  2. 制作分布式的序列号生成器。

      利用zookeeper顺序节点的特性,制作分布式的序列号生成器,或叫做ID生成器,分布式环境下使用作为数据库ID,另一种是UUID(缺点没有规律),zookeeper可以生成有顺序的容易理解的同时支持分布式环境的编号。

  在创建节点时,如果设置节点有序的,则zookeeper会自动在你的节点名后面加上序号。

分布式协调/通知

  一种典型的分布式系统机器间的通信方式是心跳。

  心跳检测是指分布式环境中,不同机器之间需要检测彼此之间是否正常运行。传统的方法时通过主机之间相互ping来实现,又或者时建立TCP长连接,通过TCP连接中固有的心跳检测机制来实现上层机器间的心跳检测。

  如果使用zookeeper,可以基于其临时节点的特性,不同机器在zookeeper的一个指定节点下创建临时子节点,不同机器之间可以根据这个临时节点来判断客户端机器是否存活。

  好处就是检测系统和被检系统不需要直接关联,而是通过zookeeper节点来关联,大大减少系统的耦合。

集群管理

  集群管理主要指集群监控和集群控制两个方面,前者侧重于集群运行时的状态的收集,后者则是进行集群的操作与控制。开发和运维中,面对集群,经常有如下需求:

  1. 希望知道集群中究竟有多少机器在工作。
  2. 对集群中的每台机器的运行时状态进行数据收集。
  3. 对集群中的机器进行上下线的操作。

  分布式集群管理体系中,有一种传统的基于Agent的方式,就是在集群每台机器部署Agent来收集机器的CPU、内存等指标。但是如果需要深入到业务状态进行监控,比如一个分布式消息中间件中,希望监控每个消费者对消息的消费状态,或在一个分布式任务调度系统中,需要对每个机器中的任务执行情况进行监控。对这些业务紧密耦合的监控需求,统一的Agent是不太合适的。

利用zookeeper实现集群管理监控组件的思路是:

  在管理机器上线/下线的场景中,为了实现自动化的线上运维,我们必须对机器的上下线情况有一个全局的监控。通常在新增机器的时候,需要首先将指定的Agent部署到这些机器上去。Agent部署启动之后,会首先向zookeeper的指定节点进行注册,具体的做法就是机器列表节点下面创建一个临时子节点。当Agent建立完这个临时子节点后,监控中心就会收到“子节点变更”的事件通知,即上线通知,于是就可以对这个新加入的机器开启相应的后台管理逻辑。另一方面,监控中心同样可以获取到机器的下线通知,这样便实现了对机器上下线的检测,同时能够很容易获取到在线的机器列表,对于大规模的扩容合容量评估都有很大帮助。

Master选举

  分布式系统中Master是用来协调集群中其他系统单元,具有对分布式系统状态更改的决定权。比如一些读写分离的应用场景,客户端写请求往往是Master来实现的。

  利用常见关系型数据库中的主键特性来实现也是可以的,集群中所有机器都向数据库中插入一条相同主键ID的记录,数据库会帮助我们自动进行主键冲突检查,可以保证只有一台机器能够成功。

  但是有一个问题,如果插入成功的和护短机器成为Master后挂了的话,如何通知集群重新选举Master?

  利用ZooKeeper创建节点API接口,提供了强一致性,能够很好保证在分布式高并发情况下节点的创建一定是全局唯一性。

  集群机器都尝试创建节点,创建成功的客户端机器就会成为Master,失败的客户端机器就在该节点上注册一个Watcher用于监控当前Master机器是否存活,一旦发现Master挂了,其余客户端就可以进行选举了。

分布式锁

  分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,一般需要通过一些互斥的手段来防止彼此之间的干扰,以保证一致性。

排他锁

  如果事务T1对数据对象O1加上了排他锁,那么加锁期间,只允许事务T1对O1进行读取和更新操作。核心是保证当前有且仅有一个事务获得锁,并且锁释放后,所有正在等待获取锁的事务都能够被通知到。

通过ZooKeeper上的Znode可以表示一个锁,/x_lock/lock。

  1. 获取锁,所有客户端都会通过调用create()接口尝试在/x_lock,创建临时子节点/x_lock/lock。最终只有一个客户端创建成功,那么该客户端就获取了锁。同时没有获取到锁的其他客户端,注册一个子节点变更的 Watcher 监听。
  2. 释放锁,获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。

共享锁

  如果事务T1对数据对象O1加上了共享锁,那么当前事务T1只能对O1 进行读取操作,其他事务也只能对这个数据对象加共享锁,直到数据对象上的所有共享锁都被释放。

通过ZooKeeper上的Znode表示一个锁,/s_lock/[HOSTNAME]-请求类型-序号。

  1. 获取锁,需要获得共享锁的客户端都会在s_lock这个节点下面创建一个临时顺序节点,如果当前是读请求,就创建类型为R的临时节点,如果是写请求,就创建类型为W的临时节点。
  2. 判断读写顺序,共享锁下不同事务可以同时对同一个数据对象进行读取操作,而更新操作必须在当前没有任何事务进行读写操作的情况下进行。
    1. 创建完节点后,获取s_lock的所有子节点,并对该节点注册子节点变更的Watcher监听。
    2. 然后确定自己的节点序号在所有的子节点中的顺序。
    3. 对于读请求,如果没有比自己小的子节点,那么表名自己已经成功获取到了共享锁,同时开始执行读取逻辑,如果有比自己序号小的写请求,那么就需要进行等待。
    4. 接收到Watcher通知后重复2.1。
  3. 释放锁 获取锁的客户端发生宕机或者正常完成业务逻辑后,就会把临时节点删除。临时子节点删除后,其他客户端又开始新的一轮获取锁的过程。

羊群效应

  在2介绍的共享锁中,在判断读写顺序的时候会出现一个问题,假如host4在移除自己的节点的时候,后面host5-7都需要接收Watcher事件通知,但是实际上,只有host5接收到事件就可以了。因此以上的实现方式会产生大量的Watcher通知。这样会对ZooKeeper服务器造成了巨大的性能影响和网络冲击,这就是羊群效应。

  改进的一步在于,调用getChildren接口的时候获取到所有已经创建的子节点列表,但是这个时候不要注册任何的Watcher。当无法获取共享锁的时候,调用exist()来对比自己小的那个节点注册Wathcer。而对于读写请求,会有不同的定义:

  读请求:在比自己序号小的最后一个写请求节点注册Watcher。 写请求:向比自己序号小的最后一个节点注册Watcher。

分布式队列

FIFO

  使用ZooKeeper实现FIFO队列,入队操作就是在queue_fifo 下创建自增序的子节点,并把数据(队列大小)放入节点内。出队操作就是先找到queue_fifo下序号最下的那个节点,取出数据,然后删除此节点。

创建完节点后,根据以下步骤确定执行顺序:

  1. 通过get_chldren()接口获取/queue_fifo节点下所有子节点。
  2. 判断自己的节点顺序,在所有子节点中的顺序。
  3. 如果不是最小的子节点,那么进入等待,同时向比自己序号小的最后一个子节点注册Watcher监听。
  4. 接受到Watchert通知后重复1。

Barrier

  Barrier就是栅栏或者屏障,适用于这样的业务场景:当有些操作需要并行执行,但后续操作又需要串行执行,此时必须等待所有并行执行的线程全部结束,才开始串行,于是就需要一个屏障,来控制所有线程同时开始,并等待所有线程全部结束。

如何控制所有线程同时开始?

  所有的线程启动时在ZooKeeper节点/queue_barrier下插入顺序临时节点,然后检查/queue/barrier下所有children 节点的数量是否为所有的线程数,如果不是,则等待,如果是,则开始执行。具体的步骤如下:

  1. getData()获取/queue_barrier节点的数据内容。
  2. getChildren()获取/queue_barrier节点下的所有子节点,同时注册对子节点列表变更的Watcher监听。
  3. 统计子节点的个数。
  4. 如果子节点个数不足10,那么进入等待。
  5. 接收Watcher通知后,重复2。

如何等待所有线程结束?

  所有线程在执行完毕后,都检查/queue/barrier下所有children节点数量是否为0,若不为0,则继续等待。

用什么类型的节点?

  根节点使用持久节点,子节点使用临时节点,根节点为什么要用持久节点?首先因为临时节点不能有子节点,所以根节点要用持久节点,并且在程序中要判断根节点是否存在。子节点为什么要用临时节点?临时节点随着连接的断开而消失,在程序中,虽然会删除临时节点,但可能会出现程序在节点被删除之前就crash了,如果是持久节点,节点不会被删除。


分布式系统中的应用

Kafka

Kafka中大部分组件都应用了zookeeper。

  1. Broker注册`/broker/ids/[0…N]记录了Broker服务器列表记录,这个临时节点的节点数据是ip端口之类的信息。
  2. Topic注册/broker/topcs记录了Topic的分区信息和Broker的对应关系。
  3. 生产者负载均衡,生产者需要将消息发送到对应的Broker上,生产者通过Broker和Topic注册的信息,以及Broker和Topic的对应关系和变化注册事件Watcher。监听,从而实现一种动态的负载均衡机制。
  4. 消息消费进度Offset记录消费者对指定消息分区进行消息消费的过程中,需要定时将分区消息的消费进度Offset记录到ZooKeeper上,以便消费者进行重启或者其他消费者重新阶段该消息分区的消息消费后,能够从之前的进度开始继续系消费。

dubbo

  Dubbo基于ZooKeeper实现了服务注册中心。哪一个服务由哪一个机器来提供必需让调用者知道,简单来说就是ip地址和服务名称的对应关系。ZooKeeper通过心跳机制可以检测挂掉的机器并将挂掉机器的ip和服务对应关系从列表中删除。

  至于支持高并发,简单来说就是横向扩展,在不更改代码的情况通过添加机器来提高运算能力。通过添加新的机器向ZooKeeper注册服务,服务的提供者多了能服务的客户就多了。