ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop的重要组件,CDH版本中更是使用它进行Namenode的协调控制。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、名字服务、分布式同步、组服务等。ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。node
(1) 角色
Zookeeper中的角色主要有如下三类,以下表所示:网络
系统模型如图所示:数据结构
(2)重要概念
ZNode
前文已介绍了ZNode, ZNode根据其自己的特性,能够分为下面两类:
Regular ZNode: 常规型ZNode, 用户须要显式的建立、删除
Ephemeral ZNode: 临时型ZNode, 用户建立它以后,能够显式的删除,也能够在建立它的Session结束后,由ZooKeeper Server自动删除
ZNode还有一个Sequential的特性,若是建立的时候指定的话,该ZNode的名字后面会自动Append一个不断增长的SequenceNo。
Session
Client与ZooKeeper之间的通讯,须要建立一个Session,这个Session会有一个超时时间。由于ZooKeeper集群会把Client的Session信息持久化,因此在Session没超时以前,Client与ZooKeeper Server的链接能够在各个ZooKeeper Server之间透明地移动。
在实际的应用中,若是Client与Server之间的通讯足够频繁,Session的维护就不须要其它额外的消息了。不然,ZooKeeper Client会每t/3 ms发一次心跳给Server,若是Client 2t/3 ms没收到来自Server的心跳回应,就会换到一个新的ZooKeeper Server上。这里t是用户配置的Session的超时时间。
Watcher
ZooKeeper支持一种Watch操做,Client能够在某个ZNode上设置一个Watcher,来Watch该ZNode上的变化。若是该ZNode上有相应的变化,就会触发这个Watcher,把相应的事件通知给设置Watcher的Client。须要注意的是,ZooKeeper中的Watcher是一次性的,即触发一次就会被取消,若是想继续Watch的话,须要客户端从新设置Watcher。这个跟epoll里的oneshot模式有点相似。并发
(3)特性分布式
顺序性,client的updates请求都会根据它发出的顺序被顺序的处理;
原子性, 一个update操做要么成功要么失败,没有其余可能的结果;
一致的镜像,client不论链接到哪一个server,展现给它都是同一个视图;
可靠性,一旦一个update被应用就被持久化了,除非另外一个update请求更新了当前值
实时性,对于每一个client它的系统视图都是最新的oop
ZooKeeper Client Library提供了丰富直观的API供用户程序使用,下面是一些经常使用的API:
create(path, data, flags): 建立一个ZNode, path是其路径,data是要存储在该ZNode上的数据,flags经常使用的有: PERSISTEN, PERSISTENT_SEQUENTAIL, EPHEMERAL, EPHEMERAL_SEQUENTAIL
delete(path, version): 删除一个ZNode,能够经过version删除指定的版本, 若是version是-1的话,表示删除全部的版本
exists(path, watch): 判断指定ZNode是否存在,并设置是否Watch这个ZNode。这里若是要设置Watcher的话,Watcher是在建立ZooKeeper实例时指定的,若是要设置特定的Watcher的话,能够调用另外一个重载版本的exists(path, watcher)。如下几个带watch参数的API也都相似
getData(path, watch): 读取指定ZNode上的数据,并设置是否watch这个ZNode
setData(path, watch): 更新指定ZNode的数据,并设置是否Watch这个ZNode
getChildren(path, watch): 获取指定ZNode的全部子ZNode的名字,并设置是否Watch这个ZNode
sync(path): 把全部在sync以前的更新操做都进行同步,达到每一个请求都在半数以上的ZooKeeper Server上生效。path参数目前没有用
setAcl(path, acl): 设置指定ZNode的Acl信息
getAcl(path): 获取指定ZNode的Acl信息性能
读、写(更新)模式spa
在ZooKeeper集群中,读能够从任意一个ZooKeeper Server读,这一点是保证ZooKeeper比较好的读性能的关键;写的请求会先Forwarder到Leader,而后由Leader来经过ZooKeeper中的原子广播协议,将请求广播给全部的Follower,Leader收到一半以上的写成功的Ack后,就认为该写成功了,就会将该写进行持久化,并告诉客户端写成功了。操作系统
WAL和Snapshot线程
和大多数分布式系统同样,ZooKeeper也有WAL(Write-Ahead-Log),对于每个更新操做,ZooKeeper都会先写WAL, 而后再对内存中的数据作更新,而后向Client通知更新结果。另外,ZooKeeper还会按期将内存中的目录树进行Snapshot,落地到磁盘上,这个跟HDFS中的FSImage是比较相似的。这么作的主要目的,一固然是数据的持久化,二是加快重启以后的恢复速度,若是所有经过Replay WAL的形式恢复的话,会比较慢。
FIFO
对于每个ZooKeeper客户端而言,全部的操做都是遵循FIFO顺序的,这一特性是由下面两个基本特性来保证的:一是ZooKeeper Client与Server之间的网络通讯是基于TCP,TCP保证了Client/Server之间传输包的顺序;二是ZooKeeper Server执行客户端请求也是严格按照FIFO顺序的。
Linearizability
在ZooKeeper中,全部的更新操做都有严格的偏序关系,更新操做都是串行执行的,这一点是保证ZooKeeper功能正确性的关键。
zookeeper中的数据是按照“树”结构进行存储的。并且znode节点还分为4中不一样的类型。
(1)、znode
根据本小结第一部分的描述,很显然zookeeper集群自身维护了一套数据结构。这个存储结构是一个树形结构,其上的每个节点,咱们称之为“znode”。
每个znode默认可以存储1MB的数据(对于记录状态性质的数据来讲,够了)
可使用zkCli命令,登陆到zookeeper上,并经过ls、create、delete、sync等命令操做这些znode节点
znode除了名称、数据之外,还有一套属性:zxid。这套zid与时间戳对应,记录zid不一样的状态(后续咱们将用到)
那么每一个znode结构又是什么样的呢?以下图所示:
此外,znode还有操做权限。若是咱们把以上几类属性细化,又能够获得如下属性的细节:
(2)、znode中的存在类型
咱们知道了zookeeper内部维护了一套数据结构:由znode构成的集合,znode的集合又是一个树形结构。每个znode又有不少属性进行描述。而且znode的存在性还分为四类,以下如所示:
znode是由客户端建立的,它和建立它的客户端的内在联系,决定了它的存在性:
PERSISTENT-持久化节点:建立这个节点的客户端在与zookeeper服务的链接断开后,这个节点也不会被删除(除非您使用API强制删除)。
PERSISTENT_SEQUENTIAL-持久化顺序编号节点:当客户端请求建立这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录惟一的编号(这个编号只会一直增加)。当客户端与zookeeper服务的链接断开后,这个节点也不会被删除。
EPHEMERAL-临时目录节点:建立这个节点的客户端在与zookeeper服务的链接断开后,这个节点(还有涉及到的子节点)就会被删除。
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点:当客户端请求建立这个节点A后,zookeeper会根据parent-znode的zxid状态,为这个A节点编写一个全目录惟一的编号(这个编号只会一直增加)。当建立这个节点的客户端与zookeeper服务的链接断开后,这个节点被删除。
另外,不管是EPHEMERAL仍是EPHEMERAL_SEQUENTIAL节点类型,在zookeeper的client异常终止后,节点也会被删除。
1. 名字服务(NameService)
分布式应用中,一般须要一套完备的命令机制,既能产生惟一的标识,又方便人识别和记忆。 咱们知道,每一个ZNode均可以由其路径惟一标识,路径自己也比较简洁直观,另外ZNode上还能够存储少许数据,这些都是实现统一的NameService的基础。下面以在HDFS中实现NameService为例,来讲明实现NameService的基本布骤:
目标:经过简单的名字来访问指定的HDFS机群
定义命名规则:这里要作到简洁易记忆。下面是一种可选的方案: [serviceScheme://][zkCluster]-[clusterName],好比hdfs://lgprc-example/表示基于lgprc ZooKeeper集群的用来作example的HDFS集群
配置DNS映射: 将zkCluster的标识lgprc经过DNS解析到对应的ZooKeeper集群的地址
建立ZNode: 在对应的ZooKeeper上建立/NameService/hdfs/lgprc-example结点,将HDFS的配置文件存储于该结点下
用户程序要访问hdfs://lgprc-example/的HDFS集群,首先经过DNS找到lgprc的ZooKeeper机群的地址,而后在ZooKeeper的/NameService/hdfs/lgprc-example结点中读取到HDFS的配置,进而根据获得的配置,获得HDFS的实际访问入口
2. 配置管理(Configuration Management)
在分布式系统中,常会遇到这样的场景: 某个Job的不少个实例在运行,它们在运行时大多数配置项是相同的,若是想要统一改某个配置,一个个实例去改,是比较低效,也是比较容易出错的方式。经过ZooKeeper能够很好的解决这样的问题,下面的基本的步骤:
将公共的配置内容放到ZooKeeper中某个ZNode上,好比/service/common-conf
全部的实例在启动时都会传入ZooKeeper集群的入口地址,而且在运行过程当中Watch /service/common-conf这个ZNode
若是集群管理员修改了了common-conf,全部的实例都会被通知到,根据收到的通知更新本身的配置,并继续Watch /service/common-conf
3. 组员管理(Group Membership)
在典型的Master-Slave结构的分布式系统中,Master须要做为“总管”来管理全部的Slave, 当有Slave加入,或者有Slave宕机,Master都须要感知到这个事情,而后做出对应的调整,以便不影响整个集群对外提供服务。以HBase为例,HMaster管理了全部的RegionServer,当有新的RegionServer加入的时候,HMaster须要分配一些Region到该RegionServer上去,让其提供服务;当有RegionServer宕机时,HMaster须要将该RegionServer以前服务的Region都从新分配到当前正在提供服务的其它RegionServer上,以便不影响客户端的正常访问。下面是这种场景下使用ZooKeeper的基本步骤:
Master在ZooKeeper上建立/service/slaves结点,并设置对该结点的Watcher
每一个Slave在启动成功后,建立惟一标识本身的临时性(Ephemeral)结点/service/slaves/${slave_id},并将本身地址(ip/port)等相关信息写入该结点
Master收到有新子结点加入的通知后,作相应的处理
若是有Slave宕机,因为它所对应的结点是临时性结点,在它的Session超时后,ZooKeeper会自动删除该结点
Master收到有子结点消失的通知,作相应的处理
4. 简单互斥锁(Simple Lock)
咱们知识,在传统的应用程序中,线程、进程的同步,均可以经过操做系统提供的机制来完成。可是在分布式系统中,多个进程之间的同步,操做系统层面就无能为力了。这时候就须要像ZooKeeper这样的分布式的协调(Coordination)服务来协助完成同步,下面是用ZooKeeper实现简单的互斥锁的步骤,这个能够和线程间同步的mutex作类比来理解:
多个进程尝试去在指定的目录下去建立一个临时性(Ephemeral)结点 /locks/my_lock
ZooKeeper能保证,只会有一个进程成功建立该结点,建立结点成功的进程就是抢到锁的进程,假设该进程为A
其它进程都对/locks/my_lock进行Watch
当A进程再也不须要锁,能够显式删除/locks/my_lock释放锁;或者是A进程宕机后Session超时,ZooKeeper系统自动删除/locks/my_lock结点释放锁。此时,其它进程就会收到ZooKeeper的通知,并尝试去建立/locks/my_lock抢锁,如此循环反复
5. 互斥锁(Simple Lock without Herd Effect)
上一节的例子中有一个问题,每次抢锁都会有大量的进程去竞争,会形成羊群效应(Herd Effect),为了解决这个问题,咱们能够经过下面的步骤来改进上述过程:
每一个进程都在ZooKeeper上建立一个临时的顺序结点(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的为当前的持锁者(${seq}是ZooKeeper生成的Sequenctial Number)
其它进程都对只watch比它次小的进程对应的结点,好比2 watch 1, 3 watch 2, 以此类推
当前持锁者释放锁后,比它次大的进程就会收到ZooKeeper的通知,它成为新的持锁者,如此循环反复
这里须要补充一点,一般在分布式系统中用ZooKeeper来作Leader Election(选主)就是经过上面的机制来实现的,这里的持锁者就是当前的“主”。
6. 读写锁(Read/Write Lock)
咱们知道,读写锁跟互斥锁相比不一样的地方是,它分红了读和写两种模式,多个读能够并发执行,但写和读、写都互斥,不能同时执行行。利用ZooKeeper,在上面的基础上,稍作修改也能够实现传统的读写锁的语义,下面是基本的步骤:
每一个进程都在ZooKeeper上建立一个临时的顺序结点(Ephemeral Sequential) /locks/lock_${seq}
${seq}最小的一个或多个结点为当前的持锁者,多个是由于多个读能够并发
须要写锁的进程,Watch比它次小的进程对应的结点
须要读锁的进程,Watch比它小的最后一个写进程对应的结点
当前结点释放锁后,全部Watch该结点的进程都会被通知到,他们成为新的持锁者,如此循环反复
7. 屏障(Barrier)
在分布式系统中,屏障是这样一种语义: 客户端须要等待多个进程完成各自的任务,而后才能继续往前进行下一步。下用是用ZooKeeper来实现屏障的基本步骤:
Client在ZooKeeper上建立屏障结点/barrier/my_barrier,并启动执行各个任务的进程
Client经过exist()来Watch /barrier/my_barrier结点
每一个任务进程在完成任务后,去检查是否达到指定的条件,若是没达到就啥也不作,若是达到了就把/barrier/my_barrier结点删除
Client收到/barrier/my_barrier被删除的通知,屏障消失,继续下一步任务
8. 双屏障(Double Barrier)
双屏障是这样一种语义: 它能够用来同步一个任务的开始和结束,当有足够多的进程进入屏障后,才开始执行任务;当全部的进程都执行完各自的任务后,屏障才撤销。下面是用ZooKeeper来实现双屏障的基本步骤:
进入屏障: Client Watch /barrier/ready结点, 经过判断该结点是否存在来决定是否启动任务 每一个任务进程进入屏障时建立一个临时结点/barrier/process/${process_id},而后检查进入屏障的结点数是否达到指定的值,若是达到了指定的值,就建立一个/barrier/ready结点,不然继续等待 Client收到/barrier/ready建立的通知,就启动任务执行过程 离开屏障: Client Watch /barrier/process,若是其没有子结点,就能够认为任务执行结束,能够离开屏障 每一个任务进程执行任务结束后,都须要删除本身对应的结点/barrier/process/${process_id}