Zookeeper场景分析及实例代码

来源:互联网 发布:日常工作提醒软件 编辑:程序博客网 时间:2024/04/29 23:43

数据发布与订阅

      发布与订阅模型,即所谓的配置中心,顾名思义就是发布者将数据发布到ZK节点上,供订阅者动态获取数据,实现配置信息的集中式管理和动态更新。例如全局的配置信息,服务式服务框架的服务地址列表等就非常适合使用。
一般的类似于发布/订阅的模式有推和拉的两种方式,分别是推Push模式和Pull模式,推模式:就是服务器主动将数据更新发送给所有订阅的客户端;而拉模式:服务端只是发送一个消息给订阅的客户端,客户端根据自己的情况,是否来获取最新的数据信息,通常采用定时轮询的方式进行拉取。
而在zookeeper中,是把这两种方式进行结合了。客户端详服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送watcher事件的通知,客户端接受到这个消息通知后,需要主动的到服务端获取最新的数据。如果将配置信息存放在Zookeeper上进行集中管理,那么通常情况下,在应用启动的时候主动到Zookeeper服务端进行进行一次配置信息的获取,同时在指定的节点上注册一个Wather事件通知,这样节点信息发生变更,服务端都会实时的通知到所有订阅的客户端,从而实现获取最新配置信息的目的。
案例模拟
 下面通过一个案例来模拟一下zookeeper的这个场景的使用。
 在平常的开发中,会遇到这样的需求,系统中需要使用一些通用的配置信息,例如机器的列表信息,运行时开发配置,数据配置信息等。这些全局配置信息通常具备下面这些特性
      1.数据量比较小
      2.数据内容在运行时会发生变化
      3.集群中各个机器共享,配置一致
对于上面中的这些配置,我们一般采取的操作是存取到本地或者内存中,无论采取哪种配置都可以实现相应的操作。使用本地配置的方式,通常系统就是在应用启动的时候读取本地配置文件properties,来进行初始化,并且在运行的过程中定期去check本地文件的内容变更。或者可以通过数据库的方式进行配置,定期的去数据库检查更新。但是一旦遇到集群规模比较大的情况的话,两种方式就不再可取。而我们还需要能够快速的做到全部配置信息的变更,同时希望变更成本足够小,因此我们需要一种更为分布式的解决方案。
   现实中实现的分布式方案有很多:
       1. 通过Redis或者Memcache来存储配置文件,通过集中式缓存系统来维护配置文件。
       2. 或者LowB点的就是通过数据库来维护,客户端通过内存来存放配置,每次配置更新时候,发出远程消息来通知各个客户端更新本地缓存。
       3. 通过Zookeeper的Wather事件机制,通知订阅的客户端。本次就是通过这种方式来实现。
        比如我们把数据库的相关的信息,供全局使用的信息来管理起来,这时候我们就可以在zookeeper上选取一个数据节点来配置存储。例如/viemall/database_properties

基于ZooKeeper的特性,借助ZooKeeper可以实现一个可靠的、简单的、修改配置能够实时生效的配置信息存储方案,整体的设计方案如图:

   

 
       整个配置信息存储方案由三部分组成:ZooKeeper服务器集群、配置管理程序、分布式应用程序。
ZooKeeper服务器集群存储配置信息,在服务器上创建一个保存数据的节点(创建节点操作);配置管理程序提供一个配置管理的UI界面或者命令行方式,用户通过配置界面修改ZooKeeper服务器节点上配置信息(设置节点数据操作);分布式应用连接到ZooKeeper集群上(创建ZooKeeper客户端操作),监听配置信息的变化(使用获取节点数据操作,并注册一个watcher)。
当配置信息发生变化时,分布式应用会更新程序中使用配置信息。

    借助 ZooKeeper我们实现的配置信息存储方案具有的优点如下:
    1.简单。修改配置整个过程变得简单很多。用户只要修改配置,无需进行其他任何操作,配置自动生效。
    2.可靠。ZooKeeper服务集群具有无单点失效的特性,使整个系统更加可靠。即使ZooKeeper 集群中的一台机器失效,也不会影响整体服务,更不会影响分布式应用配置信息的更新。
    3.实时。ZooKeeper的数据更新通知机制,可以在数据发生变化后,立即通知给分布式应用程序,具有很强的变化响应能力。

   简单的参考源码:

   http://git.oschina.net/gz-tony/zookeeper-scene-analysis

   网上开源的分布式配置平台:
    http://www.oschina.net/p/disconf?fromerr=hDYKJOtw
   其他参考信息地址:
    https://yq.aliyun.com/articles/26325
    http://blog.csdn.net/zuoanyinxiang/article/details/50937892

命名服务:

   命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息。利用Zookeeper很容易创建一个全局的路径,而这个路径就可以作为一个名字,它可以指向集群中的集群,提供的服务的地址,远程对象等。简单来说使用Zookeeper做命名服务就是用路径作为名字,路径上的数据就是其名字指向的实体。
   分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点,或者可以通过使用Zookeeper的顺序节点功能可以产生唯一且有序数字主键。

  简单的参考源码:
http://git.oschina.net/gz-tony/zookeeper-scene-analysis/src/main/java/com/zookeeper/nameService
  可以参考分布式服务框架Dubbo
  阿里巴巴集团开源的分布式服务框架Dubbo中使用ZooKeeper来作为其命名服务,维护全局的服务地址列表。在Dubbo实现中:服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。服务消费者启动的时候,订阅/dubbo/{serviceName}/providers目录下的提供者URL地址, 并向/dubbo/{serviceName} /consumers目录下写入自己的URL地址。注意,所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。另外,Dubbo还有针对服务粒度的监控,方法是订阅/dubbo/{serviceName}目录下所有提供者和消费者的信息。

分布式协调/通知:

   分布式协调/通知服务是分布式 系统中不可缺少的一个环节,是将不同的分布式组件有机的结合起来的关键所在,对于一个在多台机器上部署运行的应用而言,通常需要一个协调者(CoorDinator)来控制整个系统的运行流程,例如分布式事务的处理(Curator节点操作事务处理(CuratorTransactionResult))、机器间互相协调等,同时,引入这样一个协调者,便于将分布式协调的职责从应用中分离出来,从而大大减少系统之间的耦合性,并且显著提高了系统的可扩展性。
Zookeeper中持有Watcher注册与异步通知机制,能够很好的地实现分布式环境下不同机器,甚至是不同系统之间的协调与通知,从而实现对数据的变更的实时处理,基于Zookeeper实现分布式协调/通知的功能。
通常的通知/协调机制通常方式。
        •系统调度模式:一个分布式系统与客户端系统两部分组成,控制台的职责就是需要发一些指令信息给所有的客户端,以控制他们相应的任务逻辑(机器间的限流控制、分布式的任务调度)操作人员发送通知实际是通过控制台改变某个节点的状态,然后Zookeeper将这些变化发送给注册了这个节点的Watcher的所有客户端。
       •工作进度汇报模式:在常见的任务分发系统中,通常任务被分发到不同的机器上执行后,需要实时的将自己的任务这行进度汇报给分发系统,这个时候可以通过Zookeeper来实现:
            1.通过临时节点是否存在,来确定任务机器的存活。
             2.各个任务机器实时的将自己的任务执行进度写在临时节点上去,一遍调度中心实时的获取任务执行进度,从而通过页面的直观显示给管理员查看任务的这行情况。
这个情况是每个工作进程都在某个目录下创建一个临时节点,并携带工作的进度数据。这样汇总的进程可以监控目录子节点的变化获得工作进度的实时的全局情况。

        •心跳检测机制:传统的的开发中,只是简单的通过主机之间是否ping通来判断,复杂一点则会通过与服务器建立长链接,通过TCP链接固有的心跳检测机制来实现上层机器的心跳检测。基于Zookeeper的临时节点特性,可以让不同的机器都在Zookeeper的一个指定的节点下创建临时子节点,不同的机器之间可以根据这个临时节点来判断对应的客户端机器是否存活。通常这种方式,检测系统和被检测系统之间并不直接关联起来,而是通过zk上某个节点关联,大大减少系统耦合。

        总的来说,利用Zookeeper的watcher注册和异步通知功能,通知的发送者创建一个节点,并将通知的数据写入的该节点;通知的接受者则对该节点注册watch,当节点变化时,就算作通知的到来。

Mysql数据复制总线(mysql_replicator)是一个实时数据复制框架,用于不同的Mysql数据实例之间进行异步数据复制和数据变化通知。

Zookeeper集群管理:

  随着分布式系统规模的扩大,集群中的机器规模也随之变大,因此,如何更好的进行集群系统管理显得越来越重要了。所谓的集群管理,包括集群监控和集群控制两大块,前者侧重与对集群运行状态的收集,后者是对集群进行操作与控制。
  场景:
      1)希望知道当前集群中究竟有多少的机器在工作。
      2)对集群中每台机器的运行状态进行数据收集
      3)对集群中机器进行上下线操作。
 在传统的基于Agent的分布式集群管理体系中,都是通过在集群中的每台机器上部署一个Agent,由这个Agent负责主动向指定的一个监控中心系统汇报自己所在机器的状态。在集群规模适中的场景下,这确实是一种在生产实践中广泛使用的解决方案,能够快速有效的实现分布式环境集群的监控,但是一旦系统业务场景增多,集群规模变大之后,该解决方案的弊端就会显现出来:
        1)大规模的升级困难,以客户端形式存在的Agent,每一个客户端都需要升级部署。
       2)统一的Agent无法满足多要的需求,对于不同场景的业务可能监控的需求都会不一样,所以不适合一个统一的Agent来提供。
        3)编程语言多样性,各种系统层出不穷,如果使用传统的Agent,那么需要提供各种需要的Agent客户端,另一方面似的“监控中心”对异构系统的数据进行整合上面临巨大的挑战。

  Zookeeper具有一下两大特性。
        1)客户端如果对Zookeeper的一个数据节点注册Wather监听,那么当数据节点的内容或者是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
       2)对Zookeeper创建的临时节点,一旦客户端与服务端之间会话失效,那么该临时节点也就被自动清除。
  如:监控系统在/clusterServer节点上注册一个Watcher监听,那么但凡进行动态添加机器操作,就会在/clusterServer节点下创建一个临时节点:/clusterServer/[HostName].这样一来。监控系统就能实时的检测到机器的变动情况,至于后续的处理就是监控系统的业务了。


Master选举:

  在分布式环境中,相同的业务应用分布在不同的机器上,有些业务逻辑(例如一些耗时的计算,网络I/O处理),往往只需要让整个集群中的某一台机器进行执行,其余机器可以共享这个结果,这样可以大大减少重复劳动,提高性能,于是这个master选举便是这种场景下的碰到的主要问题。
利用ZooKeeper的强一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建 /currentMaster 节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。
另外,这种场景演化一下,就是动态Master选举。这就要用到EPHEMERAL_SEQUENTIAL类型节点的特性了。
上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,就是允许所有请求都能够创建成功,但是得有个创建顺序,于是所有的请求最终在ZK上创建结果的一种可能情况是这样:
 /currentMaster/{sessionId}-1 ,
 /currentMaster/{sessionId}-2,
 /currentMaster/{sessionId}-3 ….. 
  每次选取序列号最小的那个机器作为Master,
  如果这个机器挂了,由于他创建的节点会马上小时,那么之后最小的那个机器就是Master了。
       1. 在搜索系统中,如果集群中每个机器都生成一份全量索引,不仅耗时,而且不能保证彼此之间索引数据一致。因此让集群中的Master来进行全量索引的生成,然后同步到集群中其它机器。另外,Master选举的容灾措施是,可以随时进行手动指定master,就是说应用在zk在无法获取master信息时,可以通过比如http方式,向一个地方获取master。
      2. 在Hbase中,也是使用ZooKeeper来实现动态HMaster的选举。在Hbase实现中,会在ZK上存储一些ROOT表的地址和HMaster的地址,HRegionServer也会把自己以临时节点(Ephemeral)的方式注册到Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的存活状态,同时,一旦HMaster出现问题,会重新选举出一个HMaster来运行,从而避免了HMaster的单点问题


分布式锁

分布式锁,这个主要得益于ZooKeeper为我们保证了数据的强一致性。锁服务可以分为两类,一个排他锁、一个是共享锁的概念;
  1.排他锁:
   保证当前有且只有一个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到;
通常的做法是把zk上的一个znode看作是一把锁,通过create znode的方式来实现。所有客户端都去创建/distribute_lock节点下创建临时子节点/distribute_lock/lock,同时没有创建成功的客户端就可以在/distribute_lock节点注册一个子节点变更的Watcher监听,,最终成功创建的那个客户端也即拥有了这把锁。
   分布式锁(InterProcessMutex)
   
  InterProcessMutex 类详解步骤:获取锁的过程步骤:
1.acquire方法,根据当前线程获取锁对象,判断当前的线程是否已经获取锁,此处则代表可重入;
2.获取锁方法,String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());

3.当获取到锁时,则把锁数据放入内存对象

<span style="font-family:Arial;font-size:18px;">private final ConcurrentMap<Thread, LockData>   threadData = Maps.newConcurrentMap();  /**     * Acquire the mutex - blocking until it's available. Note: the same thread     * can call acquire re-entrantly. Each call to acquire must be balanced by a call     * to {@link #release()}     *     * @throws Exception ZK errors, connection interruptions     */    @Override    public void acquire() throws Exception    {        if ( !internalLock(-1, null) )        {            throw new IOException("Lost connection while trying to acquire lock: " + basePath);        }    }private boolean internalLock(long time, TimeUnit unit) throws Exception    {        /*           Note on concurrency: a given lockData instance           can be only acted on by a single thread so locking isn't necessary        */        Thread          currentThread = Thread.currentThread();        LockData        lockData = threadData.get(currentThread);        if ( lockData != null )        {            // re-entering            lockData.lockCount.incrementAndGet();            return true;        }        String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());        if ( lockPath != null )        {            LockData        newLockData = new LockData(currentThread, lockPath);            threadData.put(currentThread, newLockData);            return true;        }        return false;    }String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception    {        final long      startMillis = System.currentTimeMillis();        final Long      millisToWait = (unit != null) ? unit.toMillis(time) : null;        final byte[]    localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;        int             retryCount = 0;        String          ourPath = null;        boolean         hasTheLock = false;        boolean         isDone = false;        while ( !isDone )        {            isDone = true;            try            {                if ( localLockNodeBytes != null )                {                    ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, localLockNodeBytes);                }                else                {                    ourPath = client.create().creatingParentsIfNeeded().withProtection().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);                }                hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);            }            catch ( KeeperException.NoNodeException e )            {                // gets thrown by StandardLockInternalsDriver when it can't find the lock node                // this can happen when the session expires, etc. So, if the retry allows, just try it all again                if ( client.getZookeeperClient().getRetryPolicy().allowRetry(retryCount++, System.currentTimeMillis() - startMillis, RetryLoop.getDefaultRetrySleeper()) )                {                    isDone = false;                }                else                {                    throw e;                }            }        }        if ( hasTheLock )        {            return ourPath;        }        return null;    }    private boolean internalLockLoop(long startMillis, Long millisToWait, String ourPath) throws Exception    {        boolean     haveTheLock = false;        boolean     doDelete = false;        try        {            if ( revocable.get() != null )            {                client.getData().usingWatcher(revocableWatcher).forPath(ourPath);            }            while ( (client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock )            {                         //获取父节点下所有线程的子节点                List<String>        children = getSortedChildren();                         //获取当前线程的节点名称                String              sequenceNodeName = ourPath.substring(basePath.length() + 1); // +1 to include the slash                           //计算当前节点是否获取到锁                PredicateResults    predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);                if ( predicateResults.getsTheLock() )                {                    haveTheLock = true;                }                else                {                    //没有索取到锁,则让线程等待,并且watcher当前节点,当节点有变化的之后,则notifyAll当前等待的线程,让它再次进入来争抢锁                    String  previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();                    synchronized(this)                    {                        try                         {                            // use getData() instead of exists() to avoid leaving unneeded watchers which is a type of resource leak                            client.getData().usingWatcher(watcher).forPath(previousSequencePath);                            if ( millisToWait != null )                            {                                millisToWait -= (System.currentTimeMillis() - startMillis);                                startMillis = System.currentTimeMillis();                                if ( millisToWait <= 0 )                                {                                    doDelete = true;    // timed out - delete our node                                    break;                                }                                wait(millisToWait);                            }                            else                            {                                wait();                            }                        }                        catch ( KeeperException.NoNodeException e )                         {                            // it has been deleted (i.e. lock released). Try to acquire again                        }                    }                }            }        }        catch ( Exception e )        {            doDelete = true;            throw e;        }        finally        {            if ( doDelete )            {                deleteOurPath(ourPath);            }        }        return haveTheLock;    }</span>


 共享锁:

    共享锁又称读锁,同样是一种基本的锁类型。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务只能等待这个数据对象加共享锁,一直到该数据对象上的所有共享锁都被释放。

实现的思路:
  1)每启动一个进程, 则在zookeeper中创建一个带序列的临时节点
  2)客户端调用getChildren()接口来获取所有已经创建的子节点。
  3)如果无法获取共享锁,那么就调用exist()来对比比自己小的节点,注册Wather。具体:
     读请求:向比自己序号小的最后一个写请求节点注册Wather监听。
     写请求:向比自己序号小娥最后一个节点注册Wather。
  4)等待Wather通知,继续进入步骤2:

分布式队列

    队列有很多种产品,大都是消息系统所实现的,像ActiveMQ,JBossMQ,RabbitMQ,IBM-MQ等。分步式队列产品并不太多,像Beanstalkd。
我们主要介绍基于Zookeeper实现的分布式队列。分布式队列,简单地分为两大类,一种是常规的FIFO队列,另外一种则是要等到队列元素聚集之后才统一执行的Barrier模式:
FIFO:先进先出
先进先出(FIFO)队列,是消息队列最基本的一种实现形式,先发出的先消费。
实现的思路也非常简单,在/queue-fifo的目录下创建 SEQUENTIAL 类型的子目录 /x(i),这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证FIFO。

    

   

 
 图标解释
1.app1,app2,app3是3个独立的业务系统
2.zk1,zk2,zk3是ZooKeeper集群的3个连接点
3./queue-fifo,是znode的队列,按顺序存储数据
4./queue-fifo/x1,是znode队列中,1号排对者,由app1提交
5./queue-fifo/x2,是znode队列中,2号排对者,由app2提交
6.app3是消费者,通过zk3连接到znode队列中,找到/queue-fifo中顺序最少的节点消费,删除消费后的节点(红色线表示)
注:
•1). app1可以通过zk2提交,app2也可通过zk3提交
•2). app1可以提交3次请求,生成x1,x2,x3多个节点
•3). app1可以作为消费者,消费队列数据
执行顺序:
2.通过调用getChildren()接口来获取/queue-fifo节点下所有的子节点,即获取队列的所有元素。
3.确定自己的节点在所有子节点的index
4.如果自己不是序号最小的子节点,则需要进入等待,同时向比自己小的需要的最后一个节点注册Wather监听
5.接受到Wather通知后,重复步骤2;

Barrier:分布式屏障:

   Barrier原意是指障碍物、屏障,而在分布式系统中,特指系统之间的一个协助条件,规定了一个队列的元素必须都聚集后才能统一进行安排,否则一直等待,在一些分布式并行计算的应用场景上:最终合并计算需要基于很多的并行计算的子结果来进行。在JDK中自带了CyclicBarrier实现。
在分布式环境中通过Curator中提供的DistributedBarrier来实现分布式Barrier的:
实现的思路:
  1、通过默认节点/queue_barrier初始化一个数字N 代表循环值。同时注册对子节点列表变更的Wather监听事件。
  2、通过getData()接口获取/queue_barrier几点的数据内容:5;
   通过调用getChildren()接口获取/queue_barrier节点下的所有子节点,即获取队列的所有元素,
  3、统计子节点的个数。
  4、如果子节点的个数不足10个,那么需要等待。
  5、接受到Wather通知后重复步骤2。
  
   图标解释
1.app1,app2,app3,app4是4个独立的业务系统
2.zk1,zk2,zk3是ZooKeeper集群的3个连接点
3./queue,是znode的队列,假设队列长度为3
4./queue/x1,是znode队列中,1号排对者,由app1提交,同步请求,app1挂载等待
5./queue/x2,是znode队列中,2号排对者,由app2提交,同步请求,app2挂起等待
6./queue/x3,是znode队列中,3号排对者,由app3提交,同步请求,app3挂起等待
7./queue/start,当znode队列中满了,触发创建开始节点
8.当/qeueu/start被创建后,app4被启动,所有zk的连接通知同步程序(红色线),队列已完成,所有程序结束
注:
•1). 创建/queue/x1,/queue/x2,/queue/x3没有前后顺序,提交后程序就同步挂起。
•2). app1可以通过zk2提交,app2也可通过zk3提交
•3). app1可以提交3次请求,生成x1,x2,x3使用队列充满

•4). /queue/start被创建后,zk1会监听到这个事件,再告诉app1,队列已完成!


源码分享:http://git.oschina.net/gz-tony/zookeeper-scene-analysis


0 0
原创粉丝点击