Zookeeper是⼀个开源的分布式协调服务,其设计⽬标是将那些复杂的且容易出错的分布式⼀致性服务封装起来,构成⼀个⾼效可靠的原语集,并以⼀些简单的接⼝提供给⽤户使⽤。zookeeper是⼀个典型的分布式数据⼀致性的解决⽅案,分布式应⽤程序可以基于它实现诸如数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能。
在ZooKeeper中,数据信息被保存在⼀个个数据节点上,这些节点被称为znode。ZNode 是
Zookeeper 中最⼩数据单位,在 ZNode 下⾯⼜可以再挂 ZNode,这样⼀层层下去就形成了⼀个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采⽤了类似⽂件系统的层级树状结构进⾏管理。⻅下图示例:
在 Zookeeper 中,每⼀个数据节点都是⼀个 ZNode,上图根⽬录下有两个节点,分别是:app1 和app2,其中 app1 下⾯⼜有三个⼦节点,所有ZNode按层次化进⾏组织,形成这么⼀颗树,ZNode的节点路径标识⽅式和Unix⽂件系统路径⾮常相似,都是由⼀系列使⽤斜杠(/)进⾏分割的路径表示,开发⼈员可以向这个节点写⼊数据,也可以在这个节点下⾯创建⼦节点。
刚刚已经了解到,Zookeeper的znode tree是由⼀系列数据节点组成的,Zookeeper 节点类型可以分为以下几类:
通常在分布式系统中,构成⼀个集群的每⼀台机器都有⾃⼰的⻆⾊,最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制⽅式获取最新数据,并提供读服务的机器为Slave机器。⽽在Zookeeper中,这些概念被颠覆了。它没有沿⽤传递的Master/Slave概念,⽽是引⼊了Leader、Follower、Observer三种⻆⾊。
Zookeeper集群中的所有机器通过Leader选举来选定⼀台被称为Leader的机器,Leader服务器为客户端提供读和写服务,除Leader外,其他机器包括Follower和Observer,Follower和Observer都能提供读服务,唯⼀的区别在于Observer不参与Leader选举过程,不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能。
Session指客户端会话,⼀个客户端连接是指客户端和服务端之间的⼀个TCP⻓连接,Zookeeper对外的服务端⼝默认为2181,客户端启动的时候,⾸先会与服务器建⽴⼀个TCP连接,从第⼀次连接建⽴开始,客户端会话的⽣命周期也开始了,通过这个连接,客户端能够⼼跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件通知。
⾸先,先了解,事务是对物理和抽象的应⽤状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,⼀般包含了⼀系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原⼦性(Atomic)、⼀致性(Consistency)、隔离性(Isolation)和持久性(Durability)。⽽在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,⼀般包括数据节点创建与删除、数据节点内容更新等操作。对于每⼀个事务请求,ZooKeeper都会为其分配⼀个全局唯⼀的事务ID,⽤ ZXID 来表示,通常是⼀个 64 位的数字。每⼀个 ZXID 对应⼀次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。
ZooKeeper 允许客户端向服务端注册⼀个 Watcher 监听,当服务端的⼀些指定事件触发了这个 Watcher,那么就会向指定客户端发送⼀个事件通知来实现分布式的通知功能。整个Watcher注册与通知过程如图所示:
Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。具体⼯作流程为:客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执⾏回调逻辑。
Zookeeper使⽤Watcher机制实现了典型的发布/订阅模型,能够让多个订阅者同时监听某⼀个主题对象,当这个主题对象⾃身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使⽤它来进⾏分布式数据的发布与订阅。另⼀⽅⾯,通过对ZooKeeper中丰富的数据节点类型进⾏交叉使⽤,配合Watcher事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应⽤场景来做下介绍。
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中⼼,顾名思义就是发布者将数据发布到ZooKeeper的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统⼀般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;⽽拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采⽤定时进⾏轮询拉取的⽅式。
ZooKeeper 采⽤的是推拉相结合的⽅式:客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据发⽣变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进⾏集中管理,那么通常情况下,应⽤在启动的时候都会主动到ZooKeeper服务端上进⾏⼀次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样⼀来,但凡配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从⽽达到实时获取最新配置信息的⽬的。
2.2 命名服务
命名服务(Name Service)也是分布式系统中⽐较常⻅的⼀类场景,是分布式系统最基本的公共服务之⼀。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(Name),其中较为常⻅的就是⼀些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使⽤命名服务,客户端应⽤能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。
ZooKeeper 提供的命名服务功能能够帮助应⽤系统通过⼀个资源引⽤的⽅式来实现对资源的定位与使⽤。另外,⼴义上命名服务的资源定位都不是真正意义的实体资源——在分布式环境中,上层应⽤仅仅需要⼀个全局唯⼀的名字,类似于数据库中的唯⼀主键。
所以接下来。我们来看看如何使⽤ZooKeeper来实现⼀套分布式全局唯⼀ID的分配机制,在过去的单库单表型系统中,通常可以使⽤数据库字段⾃带的auto_increment属性来⾃动为每条数据库记录⽣成⼀个唯⼀的ID,数据库会保证⽣成的这个ID在全局唯⼀。但是随着数据库数据规模的不断增⼤,分库分表随之出现,⽽auto_increment属性仅能针对单⼀表中的记录⾃动⽣成ID,因此在这种情况下,就⽆法再依靠数据库的auto_increment属性来唯⼀标识⼀条记录了。于是,我们必须寻求⼀种能够在分布式环境下⽣成全局唯⼀ID的⽅法。
说起全局唯⼀ ID,相信⼤家都会联想到 UUID。没错,UUID 是通⽤唯⼀识别码(Universally
Unique Identifier)的简称,是⼀种在分布式系统中⼴泛使⽤的⽤于唯⼀标识元素的标准 确实,UUID是⼀个⾮常不错的全局唯⼀ID⽣成⽅式,能够⾮常简便地保证分布式环境中的唯⼀性。UUID的优势⾃然不必多说,长度过长、语义不明等缺点也在一些系统中不适用。
在ZooKeeper中,每⼀个数据节点都能够维护⼀份⼦节点的顺序顺列,当客户端对其创建⼀个顺序⼦节点的时候 ZooKeeper 会⾃动以后缀的形式在其⼦节点上添加⼀个序号,使⽤ZooKeeper⽣成唯⼀ID这个场景中就是利⽤了ZooKeeper的这个特性。
ZooKeeper⽣成唯⼀ID的基本步骤:
1. 所有客户端都会根据⾃⼰的任务类型,在指定类型的任务下⾯通过调⽤create()接⼝来创建⼀个顺序节点,例如创建“job-”节点。
2. 节点创建完毕后,create()接⼝会返回⼀个完整的节点名,例如“job-0000000003”。
3. 客户端拿到ID返回。
在传统的基于Agent的分布式集群管理体系中,都是通过在集群中的每台机器上部署⼀个 Agent,由这个 Agent 负责主动向指定的⼀个监控中⼼系统(监控中⼼系统负责将所有数据进⾏集中处理,形成⼀系列报表,并负责实时报警,以下简称“监控中⼼”)汇报⾃⼰所在机器的状态。在集群规模适中的场景下,这确实是⼀种在⽣产实践中⼴泛使⽤的解决⽅案,能够快速有效地实现分布式环境集群监控,但是⼀旦系统的业务场景增多,集群规模变⼤之后,该解决⽅案的弊端也就显现出来了:统⼀的Agent⽆法满⾜多样的需求,也无法满足其他编程语⾔的系统。
利用zookeeper的Watcher监听机制对Zookeeper的数据节点进行监控,那么当该数据节点的内容或是其⼦节点列表发⽣变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。同时对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被
⾃动删除。利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个Watcher监听,那么但凡进⾏动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。
master选举在集群管理中很常见,我们可以选择常⻅的关系型数据库中的主键特性来实现:集群中的所有机器都向数据库中插⼊⼀条相同主键 ID 的记录,数据库会帮助我们⾃动进⾏主键冲突检查,也就是说,所有进⾏插⼊操作的客户端机器中,只有⼀台机器能够成功——那么,我们就认为向数据库中成功插⼊数据的客户端机器成为Master。但他的机制还是比较单一,同时比较消耗数据库的性能。
我们可以利用利⽤ZooKeeper的强⼀致性,能够很好保证在分布式⾼并发情况下节点的创建⼀定能够保证全局唯⼀性,即ZooKeeper将会保证客户端⽆法重复创建⼀个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同⼀个节点,那么最终⼀定只有⼀个客户端请求能够创建成功。利⽤这个特性,就能很容易地在分布式环境中进⾏Master选举了。
比如我们在ZooKeeper 上创建⼀个⽇期节点,例如“2020-11-11,客户端集群每天都会定时往ZooKeeper 上创建⼀个临时节点,例如/master_election/2020-11-11/binding。在这个过程中,只有⼀个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2020-11-11 上注册⼀个⼦节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活,⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进⾏Master选举。
从上⾯的讲解中,我们可以看到,如果仅仅只是想实现Master选举的话,那么其实只需要有⼀个能够保证数据唯⼀性的组件即可,例如关系型数据库的主键模型就是⾮常不错的选择。但是,如果希望能够快速地进⾏集群 Master 动态选举,那么就可以基于 ZooKeeper来实现。
实际分布式锁都得实现机制和Master选举比较类似。在通常的Java开发编程中,有两种常⻅的⽅式可以⽤来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然⽽,在ZooKeeper中,没有类似于这样的API可以直接使⽤,⽽是通过 ZooKeeper上的数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁。
在需要获取排他锁时,所有的客户端都会试图通过调⽤ create()接⼝,在/exclusive_lock节点下创建临时⼦节点/exclusive_lock/lock。在前⾯,我们也介绍了,ZooKeeper 会保证在所有的客户端中,最终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个⼦节点变更的Watcher监听,以便实时监听到lock节点的变更情况。
/exclusive_lock/lock 是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。 · 当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除。 正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。 ⽆论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了⼦节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。