原创

ZooKeeper的典型应用场景

ZooKeeper作为一个高可用的分布式数据管理和协调框架,基于ZAB算法实现,能够很好的保证分布式环境中的数据一致性。正因如此,使得ZooKeeper成为了解决分布式一致性问题的利器。接下来将介绍集中zookeeper在实际应用场景中的实现方式。

数据发布/订阅

ZooKeeper可用于实现数据发布订阅系统,也就是配置中心,发布者可以向ZooKeeper发送数据,订阅者可以从者ZooKeeper中获取到发布者发布的最新数据,以达到动态更新的目的。

发布/订阅系统一般有两种设计模式,分别是'推'和'拉',ZooKeeper的订阅/发布采用推拉结合的模式。服务端在节点数据变更时,会推送消息。客户端收到消息后,会去拉去最新的数据。

将配置信息存放到ZooKeeper上进行集中管理,那么在服务启动时,可以从ZooKeeper上直接获取上一次存储的配置信息,同时在ZooKeeper上注册一个Watcher进行监听,在发生数据变更时,服务端会实时的通知到客户端进行配置的更改。从而达到实时获取最新配置信息的目的。

分布式协调/通知

分布式协调/通知服务是分布式系统中不可缺少的一个缓解,是将不同的分布式组件有机结合起来的关键所在。对于一个在多台机器上部署运行的应用而言,通常需要一个协调者来控制整个系统的运行流程,例如分布式事务的处理、机器通信协调。

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

在绝大多数分布式系统中,系统机器间的通信无外乎心跳检测、工作进度汇报和系统调度。这三种类型的机器通信方式都可以使用zookeeper来实现:

  • 心跳检测,不同机器间需要检测到彼此是否在正常运行,可以使用Zookeeper实现机器间的心跳检测,基于其临时节点特性(临时节点的生存周期是客户端会话,客户端若当即后,其临时节点自然不再存在),可以让不同机器都在Zookeeper的一个指定节点下创建临时子节点,不同的机器之间可以根据这个临时子节点来判断对应的客户端机器是否存活。通过Zookeeper可以大大减少系统耦合。
  • 工作进度汇报,通常任务被分发到不同机器后,需要实时地将自己的任务执行进度汇报给分发系统,可以在Zookeeper上选择一个节点,每个任务客户端都在这个节点下面创建临时子节点,这样不仅可以判断机器是否存活,同时各个机器可以将自己的任务执行进度写到该临时节点中去,以便中心系统能够实时获取任务的执行进度。
  • 系统调度,Zookeeper能够实现如下系统调度模式:分布式系统由控制台和一些客户端系统两部分构成,控制台的职责就是需要将一些指令信息发送给所有的客户端,以控制他们进行相应的业务逻辑,后台管理人员在控制台上做一些操作,实际上就是修改Zookeeper上某些节点的数据,Zookeeper可以把数据变更以时间通知的形式发送给订阅客户端。

具体实现参考[mysql_replicator]

集群管理

Zookeeper的两大特性(节点特性和watcher机制):

  • 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据及诶单内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
  • 对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除。

利用ZooKeeper有两个特性,就可以实时另一种集群机器存活性监控系统。可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册一个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建一个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。
下面通过分布式日志收集系统的典型应用来学习Zookeeper如何实现集群管理。
  分布式日志收集系统的核心工作就是收集分布在不同机器上的系统日志,在典型的日志系统架构设计中,整个日志系统会把所有需要收集的日志机器分为多个组别,每个组别对应一个收集器,这个收集器其实就是一个后台机器,用于收集日志,对于大规模的分布式日志收集系统场景,通常需要解决两个问题:
  · 变化的日志源机器
  · 变化的收集器机器
  无论是日志源机器还是收集器机器的变更,最终都可以归结为如何快速、合理、动态地为每个收集器分配对应的日志源机器。

  • 注册收集器机器,在Zookeeper上创建一个节点作为收集器的根节点,例如/logs/collector的收集器节点,每个收集器机器启动时都会在收集器节点下创建自己的节点,如/logs/collector/[Hostname]
  • 任务分发,所有收集器机器都创建完对应节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点,如/logs/collector/host1(持久节点)上去。这样,收集器机器就能够根据自己对应的收集器节点上获取日志源机器列表,进而开始进行日志收集工作。
  • 状态汇报,完成任务分发后,机器随时会宕机,所以需要有一个收集器的状态汇报机制,每个收集器机器上创建完节点后,还需要再对应子节点上创建一个状态子节点,如/logs/collector/host/status(临时节点),每个收集器机器都需要定期向该结点写入自己的状态信息,这可看做是心跳检测机制,通常收集器机器都会写入日志收集状态信息,日志系统通过判断状态子节点最后的更新时间来确定收集器机器是否存活。
  • 动态分配,若收集器机器宕机,则需要动态进行收集任务的分配,收集系统运行过程中关注/logs/collector节点下所有子节点的变更,一旦有机器停止汇报或有新机器加入,就开始进行任务的重新分配,此时通常由两种做法:
     - 全局动态分配,当收集器机器宕机或有新的机器加入,系统根据新的收集器机器列表,立即对所有的日志源机器重新进行一次分组,然后将其分配给剩下的收集器机器。
     - 局部动态分配,每个收集器机器在汇报自己日志收集状态的同时,也会把自己的负载汇报上去,如果一个机器宕机了,那么日志系统就会把之前分配给这个机器的任务重新分配到那些负载较低的机器,同样,如果有新机器加入,会从那些负载高的机器上转移一部分任务给新机器。  

    Master选举

在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权,如在读写分离的应用场景中,客户端的写请求往往是由Master来处理,或者其常常处理一些复杂的逻辑并将处理结果同步给其他系统单元。利用Zookeeper的一致性,能够很好地保证在分布式高并发情况下节点的创建一定能够保证全局唯一性,即Zookeeper将会保证客户端无法重复创建一个已经存在的数据节点(由其分布式数据的一致性保证)。

上文中提到,所有客户端创建请求,最终只有一个能够创建成功。在这里稍微变化下,就是允许所有请求都能够创建成功,但是得有个创建顺序,于是所有的请求最终在ZK上创建结果的一种可能情况是这样:

  1. /currentMaster/{sessionId}-1
  2. /currentMaster/{sessionId}-2
  3. /currentMaster/{sessionId}-3

每次选取序列号最小的那个机器作为Master,如果这个机器挂了,由于他创建的节点会马上小时,那么之后最小的那个机器就是Master了。
其在实际中应用有:

  • 在搜索系统中,如果集群中每个机器都生成一份全量索引,不仅耗时,而且不能保证彼此之间索引数据一致。因此让集群中的Master来进行全量索引的生成,然后同步到集群中其它机器。另外,Master选举的容灾措施是,可以随时进行手动指定master,就是说应用在zk在无法获取master信息时,可以通过比如http方式,向一个地方获取master。
  • 在Hbase中,也是使用ZooKeeper来实现动态HMaster的选举。在Hbase实现中,会在ZK上存储一些ROOT表的地址和 HMaster的地址,HRegionServer也会把自己以临时节点(Ephemeral)的方式注册到Zookeeper中,使得HMaster可以随时感知到各个HRegionServer的存活状态,同时,一旦HMaster出现问题,会重新选举出一个HMaster来运行,从而避免了 HMaster的单点问题。

其实仅仅要实现Master选举,用一个能够保证数据唯一性的组件即可,比如关系型数据库主键的方式就可以完美解决,不过如果希望实现在Master挂掉后快速进行选举,那么还是需要使用zookeeper的。

分布式锁

分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。

  • 获取锁,在需要获取排它锁时,所有客户端通过调用接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。Zookeeper可以保证只有一个客户端能够创建成功,没有成功的客户端需要注册/exclusive_lock节点监听。
  • 释放锁,当获取锁的客户端宕机或者正常完成业务逻辑都会导致临时节点的删除,此时,所有在/exclusive_lock节点上注册监听的客户端都会收到通知,可以重新发起分布式锁获取。

共享锁又称为读锁,若事务T1对数据对象O1加上共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。(控制时序)

  • 获取锁,在需要获取共享锁时,所有客户端都会到/shared_lock下面创建一个临时顺序节点,如果是读请求,那么就创建例如/shared_lock/host1-R-00000001的节点,如果是写请求,那么就创建例如/shared_lock/host2-W-00000002的节点。

  • 判断读写顺序,不同事务可以同时对一个数据对象进行读写操作,而更新操作必须在当前没有任何事务进行读写情况下进行,通过Zookeeper来确定分布式读写顺序,大致分为四步。

    1. 创建完节点后,获取/shared_lock节点下所有子节点,并对该节点变更注册监听。
    2. 确定自己的节点序号在所有子节点中的顺序。
    3. 对于读请求:若没有比自己序号小的子节点或所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若自己不是序号最小的子节点,那么需要等待。
    4. 接收到Watcher通知后,重复步骤1。
  • 释放锁,其释放锁的流程与独占锁一致。
    上述共享锁的实现方案,可以满足一般分布式集群竞争锁的需求,但是如果机器规模扩大会出现一些问题,下面着重分析判断读写顺序的步骤3。
      针对如上图所示的情况进行分析
      1. host1首先进行读操作,完成后将节点/shared_lock/host1-R-00000001删除。
      2. 余下4台机器均收到这个节点移除的通知,然后重新从/shared_lock节点上获取一份新的子节点列表。
      3. 每台机器判断自己的读写顺序,其中host2检测到自己序号最小,于是进行写操作,余下的机器则继续等待。
      4. 继续...

可以看到,host1客户端在移除自己的共享锁后,Zookeeper发送了子节点更变Watcher通知给所有机器,然而除了给host2产生影响外,对其他机器没有任何作用。大量的Watcher通知和子节点列表获取两个操作会重复运行,这样会造成系能鞥影响和网络开销,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或事务中断引起节点小时,Zookeeper服务器就会在短时间内向其他所有客户端发送大量的事件通知,这就是所谓的羊群效应。

 可以有如下改动来避免羊群效应。
  1. 客户端调用create接口常见类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
  2. 客户端调用getChildren接口获取所有已经创建的子节点列表(不注册任何Watcher)。
  3. 如果无法获取共享锁,就调用exist接口来对比自己小的节点注册Watcher。对于读请求:向比自己序号小,最后一个写请求节点注册Watcher监听。对于写请求:向比自己序号小的最后一个节点注册Watcher监听。
  4. 等待Watcher通知,继续进入步骤2。

  此方案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号比自己小的那个节点是否存在即可。不过改动后的共享锁较为麻烦,开发人员可以根据自己的业务场景和集群规模选择合适的分布式锁的实现方式。

分布式队列

分布式队列可以简单分为先入先出队列模型和等待队列元素聚集后统一安排处理执行的Barrier模型。

  • FIFO先入先出,先进入队列的请求操作先完成后,才会开始处理后面的请求。FIFO队列就类似于全写的共享模型,所有客户端都会到/queue_fifo这个节点下创建一个临时节点,如/queue_fifo/host1-00000001。
    创建完节点后,按照如下步骤执行。
      1. 通过调用getChildren接口来获取/queue_fifo节点的所有子节点,即获取队列中所有的元素。
      2. 确定自己的节点序号在所有子节点中的顺序。
      3. 如果自己的序号不是最小,那么需要等待,同时向比自己序号小的最后一个节点注册Watcher监听。
      4. 接收到Watcher通知后,重复步骤1。
  • Barrier分布式屏障,最终的合并计算需要基于很多并行计算的子结果来进行,开始时,/queue_barrier节点已经默认存在,并且将结点数据内容赋值为数字n来代表Barrier值,之后,所有客户端都会到/queue_barrier节点下创建一个临时节点,例如/queue_barrier/host1。
    创建完节点后,按照如下步骤执行。
      1. 通过调用getData接口获取/queue_barrier节点的数据内容,如10。
      2. 通过调用getChildren接口获取/queue_barrier节点下的所有子节点,同时注册对子节点变更的Watcher监听。
      3. 统计子节点的个数。
      4. 如果子节点个数还不足10个,那么需要等待。
      5. 接受到Wacher通知后,重复步骤3

上边我们介绍了Zookeeper的典型的应用场景。zookeeper已经被广泛应用于越来越多的大型分布式系统中了,其中包括:Dubbo的注册中心,HDFS的namenode和YARN框架的ResourceManager的HA(用zookeeper解决单点问题实现HA),HBase,Kafka等大数据和分布式系统框架中。我们可以学习这些内容时,注意一下Zookeeper的具体的应用实现。

参考:

《从Paxos到ZooKeeper》

sev7e0
Write by sev7e0
end
本文目录