ZooKeeper

ZooKeeper简介

  ZooKeeper是一个分布式服务框架,为分布式应用提供高效且可靠的分布式协调服务,诸如统一命名服务、集群管理、配置管理和分布式锁等分布式的基础服务。

  在解决一致性方面,ZooKeeper并没有直接采用Paxos算法,而是采用了ZAB(ZooKeeper Atomic Broadcast)的一致性协议。

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

 

 

 ZooKeeper是什么:

  ZooKeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。ZooKeeper可以保证如下分布式一致性特性:

  1、顺序一致性:从同一个客户端发起的事务请求,最终将会严格地按照其发起顺序被应用到ZooKeeper中去

  2、原子性:所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,要么都成功应用了某一个事务,要么都没有应用

  3、单一视图:无论客户端连接的是哪个ZooKeeper服务器,其看到的服务端数据模型都是一致的

  4、可靠性:一旦服务端成功的应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来,除非有另一个事务有对其进行了变更

  5、实时性:一旦一个事务被成功应用,ZooKeeper仅仅保证在一定的时间段内,客户端最终一定能够从服务端上读取到最新的数据状态

 

 

 ZooKeeper的设计目标:

  ZooKeeper致力于提供一个高性能、高可用,且具有严格的顺序访问(主要是写操作的严格顺序性)控制能力的分布式协调服务。

高性能使得ZooKeeper能够应用于那些对系统吞吐有明确要求的大型分布式应用中,高可用使得分布式的单点问题得到了很好的解决,而严格的顺序访问控制使得客户端能够基于ZooKeeper实现一些复杂的同步原语。下面是ZooKeeper的设计目标。

 

  目标一:简单的数据模型

  ZooKeeper使得分布式应用能够通过一个共享的、树型结构的名字空间来进行相互协调。这里所说的树型结构的名字空间,是指ZooKeeper服务器内存中的一个数据模型,其由一系列被称为ZNode的数据节点组成,总的来说,其数据模型类似于一个文件系统,而ZNode之间的层级关系,就像文件系统的目录一样。不过ZooKeeper将全量数据存储在内存中,以此来实现提高服务器吞吐、减少延迟的目的。

  

 

 

  目标二:可以构建集群

  一个ZooKeeper集群通常由一组机器组成(一般5台就可以),组成ZooKeeper集群的每台机器都在内存中维护当前的服务器状态,并且每台机器之间都互相保持着通信。值得一提的是,只要集群中存在超过一半以上的机器能够正常工作,那么整个集群就能够正常对外服务。

  

  目标三:顺序访问

  对于来自客户端的每个更新请求,ZooKeeper都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序。

 

  目标四:高性能

  由于ZooKeeper将全量数据存储在内存中,并直接服务于客户端的所有非事务请求,因此它尤其适用于以读操作为主的应用场景

 

 

 ZooKeeper基本概念:

  集群角色:通常在分布式系统中,构成一个集群中的每一台机器都有自己的角色,最典型的集群模式是Master/Slave,主备模式。在这种模式中,把能够处理所有写操作的机器称为Master机器,把所有通过异步复制方式获取最新数据,并提供读服务的机器称为Slave机器。

  而在ZooKeeper中,并没有延用传统的Master/Salve概念,而是引入了Leader、Follower和Observer三种角色。

   ZooKeeper集群中的所有机器通过一个Leader选举来选定一台被称为Leader的机器。Leader服务器为客户端提供读和写服务,除了Leader之外,Follower和Observer都能够提供读服务,唯一的区别是,Observer不参与集群选举,也不参与写操作的“过半写成功”策略。因此Observer服务器在不影响写性能的情况下提升集群的读性能。

  会话(Session):是指客户端会话,首先了解客户端连接:在ZooKeeper中,一个客户端连接是指客户端和服务器之间建立的一个TCP长连接。ZooKeeper对外提供服务的默认端口是2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向ZooKeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。

  数据节点(ZNode):在分布式系统中,通常所说的节点是组成集群的每一台机器。然而,在ZooKeeper中,节点分为两类:

    一类是组成ZooKeeper集群的机器

    另一类指数据模型中的数据单元,称之为数据节点ZNode。ZooKeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),有斜杠(/)进行分割的路径,就是一个ZNode,每一个ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息。

在ZooKeeper中,ZNode可以分为持久节点和临时节点。所谓的持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在ZooKeeper上。临时节点的生命周期则与会话周期相绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。

  版本:每一个存储数据的ZNode上,都维护一个叫做Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)和aversion(当前ZNode的ACL版本)

  Watcher:事件监听器,ZooKeeper中一个重要的特性。ZooKeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务器将事件通知到感兴趣的客户端上去,该机制是ZooKeeper实现分布式协调服务的重要特性

  ACL:ZooKeeper采用ACL(Access Control Lists)策略来进行权限控制,类似于UNIX文件系统的权限控制,包含以下5种权限

    CREATE:创建子节点的权限

    READ:获取节点数据和子节点列表的权限

    WRITE:更新节点数据的权限

    DELETE:删除子节点的权限

    ADMIN:设置节点ACL的权限

 

 ZooKeeper的ZAB协议:

  ZAB协议是专门为分布式协调服务ZooKeeper设计的一种支持崩溃恢复的原子广播协议。在ZooKeeper中主要依靠ZAB协议来实现分布式数据一致性。

基于该协议,ZooKeeper集群使用一个单一的服务器接受并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到集群中其他的服务器上,因此能够很好地处理客户端大量的并发请求。

 

  ZAB协议的核心是定义了那些会改变ZooKeeper服务器数据状态的事务请求的处理方式,即:

  所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器称为leader,而余下的其他服务器则称为follower服务器。

  leader服务器负责将一个客户端事务请求转换成一个事务Proposal(提议),并为每个事务分配一个全局递增的ZXID,然后将该Proposal分发给集群中所有的follower服务器(若follower收到多个事务,则根据ZXID的先后顺序来进行排序处理)。

  之后leader服务器需要等待所有follower服务器的反馈,一旦超过半数的follower服务器进行了正确的反馈后,那么leader服务器就会再次向所有的follower服务器分发Commit消息,要求其将前一个Proposal进行提交,同时leader自身也会完成对事务的提交,每一个follower服务器在接收到Commit消息后,也会完成对事务的提交。

 

 ZAB协议介绍:

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

  1、崩溃恢复模式:当整个集群所有服务器在启动过程中,或是当leader服务器出现网络分区、崩溃退出或重启的情况时,或者leader服务器失去了与过半follower的联系,ZAB就会进入崩溃恢复模式并选举产生新的leader服务器。

         数据同步:完成新的leader选举,在正式开始工作之前(即接收客户端的事务请求之前),leader服务器会首先确认事务日志中的所有Proposal是否都

         已经被集群中过半的follower提交了。哪些事务需进行同步,哪些需要丢弃?在上一个周期中,leader服务器提交了的事务则follower都要提交。leade

         r服务器提出但没有发送给follower服务器的事务需要被丢弃。

  2、消息广播工作模式:当leader选举完成,同时集群中超半数的机器已经与leader完成了数据状态同步之后,ZAB协议就会退出恢复模式,进入消息广播工作模式。所谓的数据状态同步,就是用来保证集群中过半的机器能够和leader服务器的数据状态保持一致。

    消息广播过程:使用的是原子广播协议,类似于一个二阶段提交过程。针对客户端的事务请求,leader服务器会对其生成对应的事务Proposal,并将其发送给集群中其余的服务器,然后收集各自的选票,最后进行事务的提交。

        但与二阶段不同的是,二阶段提交的阶段若没有收到其中任何一个参与者的Ack消息,则执行回滚操作。而ZAB协议将 二阶段提交的阶段二的中断逻辑移除,即只要收到过半的follower服务器的Ack消息则执行事务的提交,而无需等待所有的follower

        服务器的Ack消息。但是同时也带了一个问题,即leader服务器发生崩溃退出而带来的数据不一致性,所以ZAB协议中添加了崩溃恢复模式来解决这个问题。

        

当一台同样遵守ZAB协议的机器启动后新加入集群中,若集群中已经有一个leader进行消息广播,那么新加入的机器就会自觉和leader进行数据同步,完成同步之后,再参与到消息广播中去工作。

ZooKeeper只允许唯一的leader服务器来进行事务请求的处理,leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他服务器接收到客户端的事务请求,那么这些非leader服务器会首先将这个事务请求转发给leader服务器。

选举前置条件:当leader服务器出现崩溃或者机器重启,或者集群中已经存在过半的服务器与该Leader服务器保持正常通信时,那么在新一轮的原子广播事务操作之前,所有的服务器会通过崩溃恢复模式来使彼此达到一个一致的状态,于是整个ZAB流程就会从消息广播工作模式进入到崩溃恢复模式来选举新的leader。一个机器要想成为新的leader,必须获得过半服务器的支持。因为每台服务器都有可能会崩溃,所以在ZAB协议运行过程中,前后会出现多个leader,并且每个服务器可能多次成为leader。进入崩溃恢复模式后,只要集群中存在过半的服务器能够彼此进行通信,那么就可以产生一个新的leader并进入消息广播工作模式。

ZXID结构:ZXID是一个64位的数字,其中低32位可以看作是一个简单的单调的计数器,针对客户端的每一个事务请求,leader服务器在产生一个新的事务proposql的时候,都会对这个计数器进行加1;而高32位存的是代表了leader周期epoch的编号,每当选举产生一个新的leader服务器,就会从这个leader服务器本地日志中取出最大事务proposql的ZXID,并从ZXID中解析出对应的epoch值,然后对其进行加1操作,之后以此编号作为新的epoch,并将低32位置0来开始生成新的ZXID。

 

ZAB与Paxos算法的联系与区别

联系:1、都是超过半数即通过

   2、在ZAB算法中,每个proposal中都包含了一个epoch代表了当前的leader周期,在Paxos算法中,同样也存在一个标识,只是名字变成了ballot

ZAB协议和Paxos算法的本质区别在于,两者的涉及目标不太一样。ZAB协议主要用于构建一个高可用的分布式数据主备系统,例如ZooKeeper,而Paxos则用于构建一个分布式一致性状态集系统。

 

 

 

ZooKeeper的技术内幕 

一、系统模型

  1、数据模型:ZooKeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),有斜杠(/)进行分割的路径,就是一个ZNode,每一个ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息。

  事务ID:在数据库中,事务是指一组原子性的SQL语句。而在ZK中事务是指能够改变ZK服务器状态的操作,也成为事务操作或者事务更新操作,一般包括数据节点创建与删除、数据节点内容更新和客户端会话创建与失效等操作。对于每一个事务请求,ZK都会为其分配一个全局唯一的事务ID,称为ZXID,每一个ZXID对应一次更新操作,从这些ZXID中可以间接地识别出ZK处理这些更新操作请求的全局顺序。

 

  2、节点特性

在ZK中,每个节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型相关。

节点类型:分为持久节点(PERSISTENT)、临时节点(EPHEMERAL)和顺序节点。

  持久节点:该数据节点一旦被创建后,就会一直存在与ZK服务器上,直到有删除操作来主动清除这个节点

  临时节点:临时节点的生命周期和客户端会话绑定在一起,如果客户端会话失效(而非TCP连接断开),那么这个节点就会被自动清理掉,临时节点不能有子节点,只能作为叶子节点

  顺序节点:标识节点创建的顺序,按照节点创建的先后顺序分配一个递增的序号

所有可能的节点组合:持久节点,持久顺序节点;临时节点,临时顺序节点

 

   3、节点的状态信息-版本,保证分布式数据原子性操作

节点的三种类型的版本信息:

  version:当前数据节点数据内容的版本号

  cversion:当前数据子节点的版本号

  aversion:当前数据节点(ACL)变更版本号

 如数据节点/zk-book刚被创建完成,其version值为0,对其节点本身内容没更新一次(即使更新前后值相同),version值加1;

在并发环境中,需要通过一些机制来保证这些数据在某个操作过程中不会被外界修改,一般是通过加锁的方式来实现。另外还有使用版本号控制的乐观锁。

  悲观锁:假定不同事务之间的处理一定会出现干扰,从而需要一个事务在处理过程中都对数据进行加锁处理,适合数据更新竞争十分激烈的场景。

  乐观锁:假定多个事务在处理过程中不会彼此影响,因此在事务处理的绝大部分时间里不需要进行加锁处理,每个事务在提交更新请求之前,首先都会检查在事务读取后处理期间,是否有其他事务对数据进行了更改,如果有其他事务进行了修改的话,则当前正在提交的事务就需要回滚,否则就提交。适合并发竞争不大,事务冲突少的场景。

ZooKeeper使用数据节点的version来实现乐观锁,对每一个更新节点内容的请求,ZK会从setDataRequest请求中获取到当前请求的版本version,同时从数据记录nodeRecord中获取到当前服务器上该节点数据的最新版本currentVersion,然后进行比对,如果不匹配则抛出BadVersionException异常。

 

  Watcher机制——数据变更的通知

ZK允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定的客户端发送一个事件通知来实现分布式的通知功能。

ZK除了提供对Znode节点的处理能力,还提供了对节点的变更进行监听通知的能力。

 监听机制的步骤如下:

  任何客户端session都可以对自己感兴趣的Znode注册监听,当监听的Znode被修改,则注册监听的客户端就会收到这个Znode的变更事件通知,这个通知十分简单,只会告知客户端发生了事件,而不会说明事件的具体内容。变更后的新数据需要客户端主动去获取。

 常见的事件通知有:

  session建立成功事件

  节点添加

  节点删除

  节点变更

  子节点列表变化

需要注意的是,一次监听事件,只会被触发一次(一次性),如果想要继续监听到Znode的第二次变更,则需要重新注册监听。

 

  Leader选举流程(默认奇数台服务器)

   先看一下服务器的几种状态:

    LOOKING:寻找leader状态,因此会进入leader选举流程

    FOLLOWING:跟随者状态,表明当前服务器的角色是Follower

    LEADING:领导者状态,表明当前服务器的角色是Leader

    OBSERVING:观察者状态,表明当前服务器的角色是Observer

 

1、服务器启动时期的Leader选举(ZAB协议的崩溃恢复模式)

  每个服务器启动后进入LOOKING状态,开始选举一个新的leader或者寻找已经存在的leader,如果leader已经存在,其他服务器会通知这个新启动的服务器,哪个是leader,然后新的服务器会与leader建立连接,进行数据同步,以确保自己的状态和leader一致。

  如果集群中所有的服务器都出于LOOKING状态,那么这些服务器之间就会进行通信来选举一个leader,选举后的leader服务器进入LEADING状态,其他服务器进入FOLLOWING状态。

  名词解释,SID:服务器编号,编号越大投票时权重越大  ;ZXID:最近执行的事务ID,值越大表示数据越新

  1、每个服务器发出一个投票

    每台服务器首先都会将投票投给自己,投票中包括服务器的标识mySID和最近执行的事务ID,ZXID,即(mySID,ZXID)

  2、接收来自各个服务器的投票

    每个服务器都会接收来自其他所有服务器的投票,首先检查接收到的投票的有效性

  3、处理投票

    在接收到来自其他服务器的投票后,针对每一个投票。服务器都需要将别人的投票(vSID,vZXID)和自己的投票(mySID,ZXID)一一进行比较,比较的规则如下:

    优先检查ZXID,ZXID相同的话再比较SID;如vZXID>ZXID或者vZXID=ZXID&&vSID>mySID,则修改自己的投票为(vSID,vZXID),否则不需要修改投票,全部比较完毕后,再一次将投票信息发送出去。

  4、统计投票

    经过这两次投票后,集群中的每台机器都会再次收到其他机器的投票,然后开始统计投票。如果一台机器收到了超过半数的相同的投票,那么这个投票对应的SID,即为leader。

  5、改变服务器状态

    一旦确定了leader,每个服务器就会更新自己的状态:如果是Follower,那么就变更为FOLLOWING,如果是leader,就变更为LEADING

 

2、服务器运行期间的leader选举

  一旦leader所在的机器挂了,那么整个集群将暂时无法对外服务,而是进入新一轮的leader选举。服务器运行期间的leader选举和启动时期基本过程是一致的。

  有一点不同的是当leader挂掉后,余下的所有非Observer服务器会将自己的服务器状态变更为LOOKING,然后进入leader选举流程。

 总结:通常哪台服务器上的数据越新,那么越有可能称为leader,原因很简单,数据越新,那么它的ZXID也就越大,也就越能保证数据的恢复。当然,如果集群中同时有几个服务器具有相同的ZXID,那么SID较大的那台服务器会称为leader。

 

ZooKeeper使用

 客户端脚本:

  进入ZooKeeper的目录之后,直接执行命令连接本地zk服务器: $ sh zkCli.sh 

  连接远程zk:$ sh zkCli.sh -server ip:port

1、创建节点

  create [-s/-e] path data acl    -s或-e表示顺序或临时节点,都不添加表示创建的是持久节点。acl用来权限控制的参数

  命令:create /zk-book 123  表示在zk的根节点下创建了一个叫做 /zk-book的持久节点,节点的数据内容为123。

2、读取命令

  ls:列出zk指定节点下的所有子节点,只能查看第一层的直接子节点,命令:ls path [watch]

  get:获取zk指定节点的数据内容和属性信息,命令:get path [watch]

3、更新命令

  set:更新指定节点的数据内容,命令:set path data [version]

4、删除节点

  delete:删除zk上的指定节点,命令:delete path [version],需要注意的是,无法删除一个包含子节点的节点。

 

ZooKeeper的应用场景

  ZooKeepper是一个典型的发布订阅模式的分布式数据管理与协调框架。基于对ZAB算法的实现,该框架可以很好地保证分布式环境中数据的一致性。也正是基于这样的特性,使得ZK成为了解决分布式一致性问题的利器。开发人员可以使用它来进行分布式数据的发布与订阅。另一方面,通过对zk中丰富的数据节点类型进行交叉使用,配合watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能,如数据发布订阅,负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等。

1、数据发布订阅:

  即所谓的配置中心,如:苏宁的SCM统一配置系统,即发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。

发布订阅系统一般有两种模式,分别是推(Push)和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式,则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时轮询拉取的方式。 ZooKeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端收到这个消息通知后,需要主动到服务端获取最新的数据。

  如果将配置信息存放到ZooKeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到ZooKeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务器都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。

 实际项目中适用的场景:系统中需要一些通用的适配信息,例如机器列表信息,运行时开关配置、数据库配置信息等,一般都有这几个特点:

 1、数据量通常比较小

 2、数据内容在运行时会发生动态变化

 3、集群中各机器共享,配置一致

对于这类配置信息,一般的做法通常可以将其存储在ZooKeeper本地配置文件或者内存变量中,以存储在本地文件中为例,看如何使用ZooKeeper来实现配置管理:

  1、配置存储:首先在ZooKeeper上选取一个数据节点用于配置的存储,例如:/app1/xxx.conf,然后将配置信息写入到该节点上中去。

  2、配置获取:集群中每台机器在启动初始化阶段,首先会从上面提到的ZooKeeper配置节点上(xxx.conf)读取配置信息;同时,客户端还需要在该配置节点上注册一个

    数据变更的Watcher监听,一旦节点数据变更,所有订阅的客户端都能够获取到数据变更通知。

  3、配置变更:在系统运行过程中,可能会需要修改配置的情况,如开关状态修改,这个时候就需要进行配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点

    的内容进行更新,ZooKeeper的Watcher机制就会帮助我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据

    的获取。

 

2、负载均衡

  负载均衡(load balance)是一种常见的计算机网络技术,用来对计算机集群、网络连接、CPU、磁盘驱动器或其他资源进行分配负载,以达到优化资源使用、最大化吞吐率

、最小化响应时间和避免过载的目的。由于分布式系统具有对等性(副本和节点对等),为了保证系统的高可用性,通常采用副本的方式来对数据和服务进行部署。而对于消费者

而言,则需要在这些对等的服务提供方中选择一个来执行相关的业务逻辑,其中典型的就是DNS服务。

  1、一种动态的DNS服务:DNS是域名系统(Domain Name Sysytem)的缩写,DNS系统可以看作是一个超大规模的分布式映射表,用于将域名和IP地址进行一一映射,进而

方便人们通过域名来访问互联网站点。但是域名有限,通常需要给一个集群配置一个域名解析。通常使用修改本地的hosts文件来实现域名和IP地址的绑定,但是在机器规模相当庞

大的情况下,如果需要临时变更域名,那就要到每个机器上进行逐个变更,需要消耗大量时间而且无法保证实时性。 使用ZooKeeper可以实现动态的DNS方案,Dynamic DNS,简

称DDNS。

域名配置:和配置管理一样,我们首先需要再ZooKeeper上创建一个节点来进行域名配置,如/DDNS/app1/server.conf,

域名解析:使用DDNS方案,域名的解析过程都是由每一个应用自己负责的,通常应用都会首先从域名节点中获取一份IP地址和端口的配置,进行自行解析。同时每个应用还会在域

名节点上注册一个数据变更Watcher监听,以便及时收到域名变更的通知。

域名变更:在DDNS中,我们需要对指定的域名节点进行更新操作,ZooKeeper就会向订阅的客户端发送这个事件通知,应用在收到通知后,就会再次进行域名配置的获取。

 

3、命名服务

  命名服务是分布式系统最基本的公共服务之一。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址和远程对象等——这些都可以统称为名字(Name)。

其中最常见的是一些分布式应用服务框架(RPC)中的服务地址列表,通过使用命名服务,客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。

ZooKeeper提供的命名服务主要是根据指定名字来获取资源或服务的地址,提供者等信息,利用其ZNode的特点和Watcher机制,将其作为动态注册和获取信息的配置中心,统一管理服务名称和其对应的服务器列表信息,使我们能够实时的感受到后端服务器的状态(上线,下线,宕机等)

因此,上层服务仅仅需要通过名字就可以实现对资源的定位,因此这个名字需要保证全局且唯一,类似于UUID。下面介绍ZooKeeper如何生成全局唯一的ID。

  调用ZooKeeper节点创建的API可以创建一个顺序节点,并且这个API会返回这个节点的完成名字。就是利用这个特性来生成全局唯一的ID

  1、所有的客户端都会根据自己的任务类型,在指定的任务类型下通过调用create()接口来创建一个顺序节点,如job-

  2、节点创建完毕后,create接口会返回一个完整的节点名,如job-00001

  3、客户端拿到这个返回值后,拼接上type类型,例如type2-job-00001,这个值就可以作为全局的唯一ID了。

 

服务注册原理:

  在ZK中进行服务注册,实际上就是在ZK中创建了一个znode节点,该节点存储了该服务的IP、端口、调用方式(协议、序列化方式)等。该节点承担着最重要的职责,它由服务提供者(发布服务时)创建,以供消费者获取节点中的信息,从而定位到服务提供者网络拓扑围着以及得知如何调用。过程如下:

  1、服务提供者启动时,会将其服务名称,ip地址注册配置中心(创建的是临时节点)

  2、服务消费者在第一次调用服务时,会通过注册中心找到相应的服务的IP地址列表,并缓存到本地,以供后续使用。当消费者调用服务时,

    不会再去请求注册中心,而是直接通过负载均衡算法从IP列表中取一个服务提供者的服务器调用服务

  3、当服务提供者的某台服务器宕机或下线时,相应的ip会从服务提供者IP列表中移除(临时节点会自动从ZK上移除),同时,注册中心会

    将新的服务IP列表发送给服务消费者,重新缓存在消费者本地

  4、当某个服务的所有服务器都下线了,那么这个服务也就下线了

  5、同样,当服务提供者的某台机器上线时,注册中心会将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机

  6、服务提供方可以根据服务消费者的数量来作为服务下线的依据。

 

4、分布式协调/通知

  分布式协调/通知服务时分布式系统中不可缺少的一个环节,是将不同的分布式组件有机结合起来的关键所在。对于一个在多台机器上部署运行的应用而言,通常需要需要一个协调者来控制整个系统的运行流程,例如分布式事务、机器间的互相协调等。同时,引入这样一个协调者,便于将分布式协调的职责从应用中分离出来,从而大大减少系统之间的耦合性,而且能够显著提高系统的可扩展性。

  ZooKeeper中特有的Watcher注册和异步通知机制,能够很好地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据变更的实时处理。通常的做法是不同的客户端都对ZK上同一个数据节点进行Watcher注册,监听数据节点的变化(包括数据节点本身及其子节点),如果数据节点发生变化,那么所有的订阅者都能接收到相应的Watcher通知,并做出相应的处理。

一种通用的分布式系统机器间的通信方式:在绝大部分的分布式系统中,系统机器间的通信无外乎心跳检测、工作进度汇报和系统调度三种类型,那么基于ZooKeeper如何实现呢?

心跳检测:机器间的心跳机制是指在分布式环境中,不同机器之间需要检测到彼此是否在正常运行。常见的是通过TCP连接固有的心跳检测机制来实现上层机器的心跳检测。基于ZK的心跳检

  测方式是,利用ZK的临时节点的特性,可以让不同的机器都在zk的一个指定节点下创建临时子节点,不同的机器之间可以根据这个临时节点来判断对应的客户端机器是否存活。通过这种

  方式,系统之间不需要直接关联,大大减少了耦合

工作进度汇报:在一个常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时地将自己的任务执行进度汇报给分发系统。这个时候就可以通过zk来实现。在zk上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样便可以实现两个功能:

  1、通过判断临时节点是否存在来确定任务机器是否存活

  2、各个任务机器会实时地将自己的任务执行进度写到这个临时节点上去,以便中心系统能够实时地获取到任务的执行进度。

系统调度:使用zk,能够实现另一种系统调度模式:一个分布式系统由控制中心和一些客户端系统两部分组成,控制中心的职责就是需要将一些指令信息发送给所有的客户端,以控制它们进行相应的业务逻辑。后台管理人员在控制中心上做的一些操作,实际上就是修改zk上某些节点的数据,而zk进一步把这些数据变更以事件通知的形式发送给对应的订阅客户端。

使用zk来实现分布式系统机器间的通信,不仅能省去大量底层网络通信和协议设计上重复的工作,更为重要的一点是大大降低了系统间的耦合,而且能够方便地实现异构系统之间的灵活通信。

 

5、集群管理

  随着分布式系统规模的日益扩大,集群中机器规模也随之变大。所谓集群管理,包括集群监控与集群控制两大块,前者侧重对集群运行时状态的收集,后者则是对集群进行操作和控制,如我们需要知道:

  1、当前集群中有究竟有多少台机器在工作

  2、对集群中每台机器的运行时状态进行数据收集

  3、对集群中机器进行上下线工作

由于ZooKeeper具有两大特性:

  1、客户端如果对ZooKeeper的一个数据节点注册Watcher监听,那么当这个数据节点的内容或者其子节点发生变更时,ZooKeeper服务器就会向订阅的客户端发送变更通知

  2、对在ZooKeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么该临时节点也就会自动删除

利用ZooKeeper的这两大特性,就可以实现另一种集群机器存活性监控系统。例如,监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,

就会在/clusterServcers节点下创建一个临时节点:/clusterServers/[Hostname]。这样一来,监控系统就能够实时监测到机器的变动情况。分布式日志收集和在线云主机管理就是

ZooKeeper实现集群管理的两个实例。

 

6、Master选举

7、分布式锁

  分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之间的干扰,以保证一致性,在这种情况下,就需要使用分布式锁了。下面介绍ZooKeeper如何实现排他锁和共享锁两类分布式锁。

  1、排他锁:(X锁),又称为写锁或独占锁,是一种基本的锁类型,如果事务T1对数据O1加上了排他锁,那么在这个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,知道T1释放了锁。

定义锁:在Java中,有Synchronized机制和ReentrantLock来定义锁,在ZooKeeper中没有类似于这样的API可以使用,而是通过ZooKeeper上的数据节点来表示一个锁,例如/exclusive_lock/lock就可以被定义为一个锁,不同的机器通过抢占创建临时节点的方式获取锁,会话失效则释放锁。

获取锁:在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在/exclusice_lock节点下创建临时子节点    /exclusive_lock/lock。由于ZooKeeper会保证所有的客户端中只有一个客户端能创建成功,那么,只要客户端创建成功,就可以认为获取到了锁。同时,没有获得锁的客户端需要在 /exclusive_lock节点下注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。   

释放锁:由于 客户端创建的/exclusive_lock/lock节点是一个临时节点,因此下面两种情况都有可能释放锁:

  (1)当前获取锁的客户端机器发生宕机,那么ZooKeeper上的这个临时节点就会被移除

  (2)正常执行完业务逻辑后,客户端就会主动将自己创建的临时节点删除

如果lock节点被删除,那么ZooKeeper就会通知所有在 /exclusive_lock节点上注册了子节点变更的Watcher监听的客户端,这些客户端在接收到通知后,重新发起发布式锁的获取。

  

  2、共享锁:(S锁),又称为读锁,如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,而不能修改这条数据,直到该数据对象上的所有共享锁都被释放。

定义锁:和排他锁一样,同样是通过ZooKeeper上的数据节点来表示一个锁,是一个类似于/share_lock/[hostname]-请求类型-请求序号  的临时顺序节点,如/share_lock/192.168.0.1-R-00001,那么这个节点就代表了一个共享锁。

获取锁:在需要获取共享锁时,所有的客户端都会到/share_lock这个节点下创建一个临时顺序节点,如果当前是读请求,那么就创建例如 /share_lock/192.168.0.1-R-00001的节点,如果是写请求,就会创建如/share_lock/192.168.0.1-W-00001  的节点

判断读写顺序:  根据共享锁的定义,不同的事务都可以对同一个数据对象进行读取操作,而更新操作必须在当前没有任务事务进行读写操作的情况下进行。基于这个原则,来看一下如何通过    ZK的节点来确定分布式读写顺序,大致分为4个步骤:

  (1):创建完节点后,获取/share_lock节点下的所有子节点列表,并对该节点注册子节点变更的Watcher监听。

  (2):确定自己的节点序号在所有子节点列表中的顺序

  (3):对于读请求:如果没有比 自己序号小的子节点(第一个创建的子节点),或是所有的比自己序号小的节点都是读请求,那么表名自己已经成功获取到了共享锁,同时开始执行读

        取逻辑;如果比自己序号小的子节点中有写请求,那么就需要进入等待

       对于写请求:如果自己不是序号最小的子节点,那么就需要进入等待

  (4):接收到Watcher通知后,重复步骤(1)

  但是有个问题,  如果集群规模比较大,大量的机器在/share_lock节点下创建子节点,并注册Watcher监听,每一次Watcher监听都判断自己在子节点列表中的顺序,如果是写操作则判断有没有比自己序号小的节点,读操作也要判断比自己小的节点的操作类型,如果没有轮到自己操作,则继续等待下一次通知。如果集群规模比较的情况下,同一时间点有多个节点对应的客户端完成事务或是中断使节点删除,则zk就会在短时间内向其余客户端发送大量的事件通知,这就是所谓的羊群效应。

  改进后的分布式锁实现:找准客户端真正的关注点,客户端每次都是判断自己是否是所有子节点中序号最小的,因此不用关注/share_lock节点下的所有子节点的变更,只需要关注比自己节点序号小的子节点的变更情况就可以了。

  

释放锁:和排他锁一致。 

                                                                                                                                                                                                                                                                         

ZooKeeper在Kafka中的应用

  Kafka是一个 吞吐量极高的分布式消息中间件,在kafka集群中没有中心主节点的概念,集群中所有的服务器都是对等的。因此,可以在不做任何配置更改的情况下实现服务器的添加与删除。同样,消息的生产者和消费者也能做到随意的重启和机器上下线。

  Broker注册

Kafka中的Broker是分布式部署并且相互之间是独立运行的,所以需要一个注册系统能够将整个集群中的Broker服务器都管理起来。在Kafka的设计中,选择了使用ZooKeeper来进行所有Broker的管理。

  在ZooKeeper上会有一个专门用来进行Broker服务器列表记录的节点,如称为Broker节点,其节点路径为/brokers/ids。

  1、每个Broker服务器在启动时,都会到ZooKeeper上进行注册,即到Broker节点下创建一个属于自己的子节点,如其节点路径为/broker/ids/[0...N]。

  2、ZooKeeper使用一个全局唯一的数字来指代每一个Broker服务器,称为BrokerID,创建完Broker节点后,每个Broker就会将自己的IP地址和端口等信息写入到该节点中去

  3、Broker节点创建的是一个临时节点,一旦这个Broker宕机或者下线后,那么这个Broker对应的节点也就被删除了,因此可以通过ZooKeeper上Broker节点的变化情况来动态表示Broker

    服务器的可用性

  Topic注册

在Kafka上会将同一个Topic的消息分成多个分区并将其分布到多个Broker上,这些分区信息以及与Broker的对应关系都是由ZooKeeper维护的,由专门的节点来记录,如若其节点路径为/brokers/topics。称之为Topic节点。

  1、Kafka中的每一个topic,都会以/brokers/topics/[topicName]的形式记录在这个节点下,例如/brokers/topics/login和 /brokers/topics/search分别表示topic login和topic search。

  2、Broker服务器在启动后,会到对应的节点下注册自己的BrokerID,并写入针对该Topic的分区总数,例如/brokers/topics/login3-->2表示ID为3的Broker为login这个topic提供了2个

    分区进行消息存储

  3、分区数节点也是一个临时节点

  生产者负载均衡

Kafka是分布式部署Broker服务器的,会对同一个Topic的消息进行分区并将其分布到不同的Broker服务器上;因此,生产者需要将消息合理地发送到这些分布式的Broker上。

在Kafka中,客户端使用了基于ZooKeeper的负载均衡策略来解决生产者的负载均衡问题。每当一个Broker启动时,会首先完成Broker注册过程,并注册一些诸如”有哪些可订阅的Topic“

的元数据信息。生产者就能通过这个节点动态地感知到Broker服务器列表的变更。在实际上,Kafka生产者会对ZK上”Broker的新增或减少“,”Topic的新增或减少“,”Broker和Topic关联关系“等事件注册Watcher监听,这样就可以实现一种动态的负载均衡机制了。在这种模式下,还能允许开发人员控制生产者根据一定的规则来进行数据分区。

  消费者负载均衡

Kafka中的消费者同样需要进行负载均衡来实现多个 消费者合理地从对应的Broker上接收消息。Kafka存在消费组的概念,每个消费组内都包含了若干个消费者,每一条消息只会发送给分组中的一个消费者,不同的消费者分组消费自己特定Topic下的消息,互不干扰,也不需要互相协调。因此消费者的负载均衡也就是同一个消费组内部的消息消费策略(消费者消费分区的对应关系策略)。

消费分区与消费者的关系:

  对于每一个而消费者分组,Kafka都会分配一个全局唯一的GroupID,同一个消费者分组内部的所有消息者都共享该ID。同时kafka也会为每个消费者分配一个ConsumerID。在kafka的设计中规定了每个消息分区有且只能有一个消费组内的一个消费者进行消息的消费。因此,需要在ZooKeeper上记录下消息分区与消费者之间的对应关系。每个消费者一旦确定了对一个消费分区的消费权利,那么需要将其ConsumerID写入到对应的消息分区的临时节点上,例如/consumers/[group_id]/owners/[topic]/[broker_id-partition_id],其中broker_id-partition_id就是一个消费分区的标识,节点内容就是消费该分区上消息的消费者的ConsumerID。

消息消费进度Offset记录

  在消费者对指定消息分区进行消息消费的过程中,需要定时地将分区消息的消费进度,即Offset记录到ZooKeeper上去,以便在该消费者进行重启或是其他消费者重新接管该消息分区的消息消费后,能够从之前的进度开始继续往后进行消息的消费。Offset在ZooKeeper上的记录又一个专门的节点负责。其节点路径为/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id],其节点内容就是Offset值。

  消费者注册

  下面是消费者在初始化启动时加入消费者分组的过程:

  1、注册到消费者分组

每个消费者服务器在启动的时候,都会到ZK的指定节点下创建一个属于自己的消费者节点,例如/consumers/[group_id]/ids/[consumer_id]。完成节点创建后,消费者就会将自己订阅的Topic信息写入该节点。注意,该节点也是一个临时节点,也就是说,一旦消费者服务器出现故障或者下线后,其对应的消费者节点就会被删除掉

  2、对消费者分组中消费者的变化注册监听

每个消费者都需要关注所属消费者分组中消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听。一旦发现消费者新增或减少,就会触发消费者的负载均衡。

  3、对Broker服务器的变化注册监听

消费者需要对/broker/ids/[0...N]中的节点进行监听的注册,如果发现Broker服务器列表发生变化,那么就根据具体情况来决定是否需要进行消费者的负载均衡。

  4、进行消费者负载均衡

所谓消费者负载均衡,就是为了能够让同一个Topic下不同分区的消息尽量地被多个消费者消费而进行的一个消费者与消息分区分配的过程。通常,对于一个消费者分组,如果组内的消费者服务器发生变更或者Broker服务器发生变更,会触发消费者负载均衡。

 

RPC服务框架-Dubbo

Dubbo注册中心在ZooKeeper上的服务的节点设计为:

第一层:/dubbo  根节点

第二层:/dubbo/com.foo.xxxService  服务节点,代表了dubbo的一个服务

第三层:  /dubbo/com.foo.xxxService/providers  服务提供者的父节点,其子节点代表了当前服务的提供者

      /dubbo/com.foo.xxxService/consumers  服务消费者的父节点,其子节点代表了当前服务的真正消费者

第四层:分别是服务提供者和服务消费者的ip

 

工作流程:

  1、服务提供者:服务提供者在初始化启动的时候,会首先在ZK的/dubbo/com.foo.xxxService/providers节点下创建一个子节点,并写入自己的URL地址

  2、服务消费者:服务消费者会在启动的时候,读取并订阅ZK上/dubbo/com.foo.xxxService/providers节点下的所有子节点,并解析出所有提供者的URL地址来作为该服务地址列表,然后发起正常调用同时,服务消费者还会在/dubbo/com.foo.xxxService/consumers节点下创建一个临时节点,并写入自己的URL地址,代表其实这个服务一个消费者

  3、注册中心:监控中心是Dubbo中服务治理体系的重要一部分,其需要知道所有服务提供者和消费者,及其变化情况。因此监控中心在启动的时候,会通过Z的/dubbo/com.foo.xxxService节点来获取所有提供者和消费者的URL地址,并注册Watcher来监听其子节点变化。

  需要注意的是,所有的提供者在ZK上注册的节点都是临时节点,利用的是临时节点的生命周期与会话相关的特性,因此一旦提供者所在的机器出现故障导致该提供者无法对外提供服务时,该临时节点就会自动从ZK上删除,这样服务的消费者和监控中心都能感知服务提供者的变化。

 

 

END.

 

posted @ 2019-05-27 17:58  杨岂  阅读(503)  评论(0编辑  收藏  举报