Zookeeper 介绍及应用场景分析

By Jie Yang

ZooKeeper(动物园管理员),顾名思义,是用来管理Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)的管理员,同时Apache HBase、Apache Solr等众多项目中都采用了ZooKeeper。

Zookeeper 分布式服务框架是 Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。本文将着重介绍以及分析 Zookeeper 的典型应用场景。

基本原理

系统结构

设计目标:

简单: 数据模型是简单的层次结构,所有数据在内存中进行缓存,保证zookeeper的实现是简单的; Client访问任意的Server都是相同的行为, 保证使用的简单.

重复备份:Zookeeper本身也是一个分布式系统.每一个Server节点都知道其它节点的存在,它们共同维护一个统一的数据镜像.只要超过半数的Server节点可用就能保证整个Zookeeper服务的可用性。

有序:Zookeeper会为每一个更新操作创建一个流水号,保证数据的一致性。

快:Zookeeper将所存储的数据都放到了内存中,所以对于读操作远大于写操作的系统,其效率尤为高效。当读写比例达到10:1性能最佳。

数据模

Zookeeper 会维护一个具有树形层次关系的数据结构类似

Zookeeper 这种数据结构有如下这些特点:

1.每个子目录项如 app1都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如p_1个 znode 的标识为 /app1/p_1

2.结点类型:

  • Persistent Nodes: 永久有效地节点,除非client显式的删除,否则一直存在
  • Ephemeral Nodes: 临时节点,仅在创建该节点client保持连接期间有效,一旦连接丢失,zookeeper会自动删除该节点
  • Sequence Nodes: 顺序节点,client申请创建该节点时,zk会自动在节点路径末尾添加递增序号,这种类型是实现分布式锁,分布式queue等特殊功能的关键。

 

3.znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录

4.znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据

5.znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,Zookeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了

6.znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2

7.znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的,后面在典型的应用场景中会有实例介绍.但是只支持单次监控,收到监听事件后本次监控失效,需要再次注册监控才能继续监听。

ZooKeeper 典型的应用场

命名服务(Name Service

Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。然后所有的Client节点即可get接口查询这个节点资源。

分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,实际两者没有什么本质的区别,它们都是将有层次的目录结构关联到一定资源上,区别仅仅在于Zookeeper存储的是byte[]而JDNI是对象。

配置管理(Configuration Management

配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。

像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。

 

集群管理(Group Membership

集群监控

例如需要监控集群中机器状态、能够快速对集群中机器变化做出响应。这样的场景中,往往有一个监控系统,实时监测集群机器是否存活。过去的做法通常是:监控系统通过某种手段(比如ping)定时检测每个机器、或每个机器定时向监控系统发送心跳信息。这种做法存在两个弊端:1.集群中机器有变动的时候,牵连修改的东西比较多。2.有一定的延迟。利用ZooKeeper,可以实现另一种集群机器存活性监控系统:a.客户端在节点x上注册watcher,如果x的子节点发生变化,会通知该客户端。b.创建EPHEMERAL类型的节点,一旦客户端和服务器的会话结束或过期,该节点就会消失。例如:监控系统在/cluster节点上注册一个watcher,以后每动态加机器,就往/culsterServer下创建一个EPHEMERAL类型的节点:/cluster/{hostname}。这样,监控系统就能实时知道机器的增减情况,至于后续处理就是监控系统的业务了。

Leader选举

Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。

分布式锁(Distributed Lock)

这是Zookeeper最主要的应用,当前Zookeeper是公认的分布锁的最佳解决方案。进程需要访问共享数据时, 就在”/locks”节点下创建一个ephemeral_sequence类型的子节点, 因为是sequence节点Zookeeper可以保证创建申请进程的先后顺序,

以在创建的时候, 进程通过getChildren方法获取子节点列表, 找出其中最小的节点如果它就是当前节点创建的则当前进程活动的锁。如果不是则列表中找到排名比自己前1位的节点, 称为waitPath, 然后在waitPath上注册监听, 当waitPath被删除后, 进程获得通知, 此时说明该进程获得了锁。进程删除自己创建的节点则释放锁。

因为是创建的节点是ephemeral类型,如果在申请过程中该进程实效了该进程的节点将自动被删除保证正确性。

下图中,进程Client_1,Client_2,Client_3同时申请锁在locks先后创建的lock_1,lock_2和lock_3节点,其中进程1创建的lock_1节点最小所以先获得锁,client_2监听lock_1,client_3监听lock_2,它们将先后获得锁。

共享锁

共享锁即读写锁,在单进程可以使用ReentrantReadWriteLock轻松实现,在分布式环境下通过Zookeeper对上面的互斥锁增加一定修改也很容易实现。进程在申请锁并创建节点时区分是 读还是写操作。如果是读操作则判断在它前面有没有写锁的申请,如果没有则获得读锁,如果有则监听自己前面最近的写锁节点。如果是写操作则与一般的互斥锁相同处理。即,需要等到自己是最小节点时获得锁。

下图中S1、S2、S3、S4为度操作,W2、W3为写操作,S1、S2将先获得读锁。W2监听S2,S3和S4监听W2,它们需要等等待W2释放锁后获得锁。

 

队列管

同步队列Barrier

当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,等到所有成员都离开后才算结束,所谓同进同出。

先创建一个父目录 /barrier,队列中每个成员都在其下创建自己的节点,节点类型为ephemeral_sequence,每个成员进程都监听父节点/barrier,如果发现/barrier子节点数量等于队列长度时说明所有的成员都已经进入,则可以执行业务操作,否则等待。

当成员进程执行完业务方法后,将自己的节点删除通知其它成员我已完成。各成员监听到/barrier子节点数量等于0时,说明所有成员都已完成任务,程序结束。

生产者消费者Queue

先创建一个父目录 /Queue,生产者生产时则爱都在/Queue下创建自己的节点,节点类型为persistent_sequence(不是sequence也可以,没有必要保证顺序,官网的例子是用的这个类型),消费者监听父目录 /Queue,如果子节点数量大于0,则获取其中的一个节点执行业务操作后删除该节点,如果等于0则等待。

先进先出FIFO

 

队列用 Zookeeper 实现的思路也非常简单,先创建一个父目录 /FIFO,队列成员都在/FIFO下创建自己的节点,节点类型为 ephemeral_sequence,这样就能保证所有成员加入队列时都是有编号的,进程创建自己的节点后通过 getChildren( ) 方法可以返回当前队列中的所有元素,如果发现自己是其中最小的一个则出队列并删除自身的节点,否则监听等待,这样就能保证 FIFO。

Zookeeper 最初作为 Hadoop 项目中的一个子项目,是 Hadoop 集群管理的核心模块,它主用来控制集群中的数据的一致性高。现在已广泛应用于众多著名分布式应用中。

Zookeeper总的特点是简单高效,它提供了一套很好的分布式集群管理的机制,就是它这种基于层次型的目录树的数据结构,并对树中的节点进行有效管理,从而可以设计出多种多样的分布式的数据管理模型,而不仅仅局限于上面提到的几个常用应用场景。

缺陷:

只支持单次监听,需要反复注册才能实现永久监听,这样唯一的好处就是不用解注册。也不知为何强大的Zookeeper不支持永久监听。

对写事件频繁,读写比小于4: 1的应用效率下降明显。

 

附:部分场景的样例实现

zk_example