zk 基本命令
客户端命令:
连接指定的zookeeper服务器: sh zkCli.sh -server ip:port
创建节点: create [-s] [-e] path data acl 其中-s -e分别指定节点特性,顺序或临时节点。默认情况下(不添加-s, -e),创建的是持久化节点。
create /zk-book 123 就是在zookeeper的根节点下创建了一个叫做/zk-book的节点,并且节点的数据内容是"123",。
读取
ls path [watch] ls可以列出zookeeper指定节点下的所有子节点,
get path [watch] 可以获取zk指定节点的数据内容和属性信息
set path data [version] 可以更新指定节点的数据内容
java客户端API使用
客户端可以通过创建一个Zookeeper实例来连接Zookeeper服务器,Zookeeper客户端与服务端会话的建立是一个异步的过程,构造方法会在处理完客户端初始化工作后立即返回,此时并没有真正建立好一个可用的会话,在会话的生命周期处于CONNECTING状态,当该会话真正创建完毕后,Zookeeper服务端会向会话对应的客户端发起一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话。构造方法内部实现了与zookeeper服务器之间的tcp连接,负责维护客户端会话的生命周期:
// Java API -> 创建连接 -> 创建一个最基本的ZooKeeper对象实例
public class ZooKeeper_Constructor_Usage_Simple implements Watcher {
/*一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。*/
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception{
ZooKeeper zookeeper = new ZooKeeper("server-2:2181",
5000, //
new ZooKeeper_Constructor_Usage_Simple());
System.out.println(zookeeper.getState());
try {
connectedSemaphore.await();
} catch (InterruptedException e) {}
System.out.println("ZooKeeper session established.");
}
/**
* 该类实现 Watcher 接口,重写了 process 方法,该方法负责处理来自 Zookeeper
* 服务端的 Watcher 通知,在收到服务端发来的 SyncConnected 事件之后,解除
* 主程序在 CountDownLatch 上的等待阻塞。至此,客户端会话创建为完毕。
*/
@Override
public void process(WatchedEvent event) {
System.out.println("Receive watched event:" + event);
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
}
}
输出:
Receive watched event:WatchedEvent state:SyncConnected type:None path:null
CONNECTED
ZooKeeper session established.
Disconnected from the target VM, address: '127.0.0.1:52618', transport: 'socket'
还可以复用sessionId和sessionPasswd来创建一个Zookeeper对象实例:
// Java API -> 创建连接 -> 创建一个最基本的ZooKeeper对象实例,复用sessionId和session passwd
public class ZooKeeper_Constructor_Usage_With_SID_PASSWD implements Watcher {
/*一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。*/
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception{
//创建会话
ZooKeeper zookeeper = new ZooKeeper("server-2:2181",
50000, //
new ZooKeeper_Constructor_Usage_With_SID_PASSWD());
connectedSemaphore.await();
long sessionId = zookeeper.getSessionId();
byte[] passwd = zookeeper.getSessionPasswd();
//使用错误的 sessionId 和 SessionPasswd 来创建 Zookeeper 客户端的实例
zookeeper = new ZooKeeper("server-2:2181",
50000, //
new ZooKeeper_Constructor_Usage_With_SID_PASSWD(),//
1l,//
"test".getBytes());
//使用正确的 sessionId 和 SessionPasswd 来创建 Zookeeper 客户端的实例
zookeeper = new ZooKeeper("server-2:2181",
50000, //
new ZooKeeper_Constructor_Usage_With_SID_PASSWD(),//
sessionId,//
passwd);
Thread.sleep( Integer.MAX_VALUE );
}
/**
* 该类实现 Watcher 接口,重写了 process 方法,该方法负责处理来自 Zookeeper
* 服务端的 Watcher 通知,在收到服务端发来的 SyncConnected 事件之后,解除
* 主程序在 CountDownLatch 上的等待阻塞。至此,客户端会话创建为完毕。
*/
@Override
public void process(WatchedEvent event) {
System.out.println("Receive watched event:" + event);
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
}
}
output:
Receive watched event:WatchedEvent state:SyncConnected type:None path:null
Receive watched event:WatchedEvent state:Expired type:None path:null
Receive watched event:WatchedEvent state:SyncConnected type:None path:null
第一次使用了错误的sessionId和sessionPasswd来创建Zookeeper客户端实例,结果收到服务端Expired事件通知。第二次使用了正确的sessionId和sessionPasswd来创建客户端实例,结果连接成功。
创建节点:
节点的创建分为同步和异步两种API:
//ZooKeeper API创建节点,使用同步(sync)接口。
public class ZooKeeper_Create_API_Sync_Usage implements Watcher {
/*一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待。
调用 countDown() 的线程打开入口前,所有调用 await 的线程都一直在入口处等待。*/
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception{
//创建会话
ZooKeeper zookeeper = new ZooKeeper("server-2:2181",
5000, //
new ZooKeeper_Create_API_Sync_Usage());
connectedSemaphore.await();
//创建临时节点-EPHEMERAL
String path1 = zookeeper.create("/zk-test-ephemeral-",
"".getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
System.out.println("Success create znode: " + path1);
//临时顺序节点-EPHEMERAL_SEQUENTIAL
String path2 = zookeeper.create("/zk-test-ephemeral-",
"".getBytes(),
Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("Success create znode: " + path2);
}
/**
* 该类实现 Watcher 接口,重写了 process 方法,该方法负责处理来自 Zookeeper
* 服务端的 Watcher 通知,在收到服务端发来的 SyncConnected 事件之后,解除
* 主程序在 CountDownLatch 上的等待阻塞。至此,客户端会话创建为完毕。
*
* WatchedEvent三个基本属性:通知状态keepState、事件类型eventType、节点路径path
*
* 服务端在生成WatcheredEvent事件之后,会调用getWrapper方法将自己包装成一个可序列化的WatcherEvent事件,
* 以便通过网络传输到客户端。客户端在接受到服务端的这个事件对象后,首先会将WatcherEvent事件还原成一个WatchedEvent
* 事件,并传递给process方法处理,回调方法process根据入参就能够解析出完整的服务端事件了。
*/
@Override
public void process(WatchedEvent event) {
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
}
}
// ZooKeeper API创建节点,使用异步(async)接口。
public class ZooKeeper_Create_API_ASync_Usage implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
public static void main(String[] args) throws Exception{
ZooKeeper zookeeper = new ZooKeeper("server-2:2181",
5000, //
new ZooKeeper_Create_API_ASync_Usage());
connectedSemaphore.await();
//临时节点-EPHEMERAL
zookeeper.create("/zk-test-ephemeral-", "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL,
new IStringCallback(), "I am context.");
//临时节点-EPHEMERAL
zookeeper.create("/zk-test-ephemeral-", "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL,
new IStringCallback(), "I am context.");
//临时顺序节点-EPHEMERAL_SEQUENTIAL
zookeeper.create("/zk-test-ephemeral-", "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL,
new IStringCallback(), "I am context.");
Thread.sleep( Integer.MAX_VALUE );
}
@Override
public void process(WatchedEvent event) {
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
}
}
class IStringCallback implements AsyncCallback.StringCallback{
public void processResult(int rc, String path, Object ctx, String name) {
System.out.println("Create path result: [" + rc + ", " + path + ", "
+ ctx + ", real path name: " + name);
}
}
output:
Create path result: [0, /zk-test-ephemeral-, I am context., real path name: /zk-test-ephemeral-
Create path result: [-110, /zk-test-ephemeral-, I am context., real path name: null
Create path result: [0, /zk-test-ephemeral-, I am context., real path name: /zk-test-ephemeral-0000000030
在同步的创建接口中,我们需要关注接口抛出的异常,但是在异步接口中,接口本身是不会抛出异常的,所有的异常都会在回调函数中通过Result code来体现。
获取节点信息
// ZooKeeper API 获取子节点列表,使用同步(sync)接口。
public class ZooKeeper_GetChildren_API_Sync_Usage implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk = null;
public static void main(String[] args) throws Exception{
//父节点路径
String path = "/zk-book";
//创建Zookeeper会话周期
zk = new ZooKeeper("server-2:2181",
5000, //
new ZooKeeper_GetChildren_API_Sync_Usage());
connectedSemaphore.await();
//创建父节点
zk.create(path, "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//创建子节点
zk.create(path+"/c1", "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
//获取父节点下所有子节点,同时在接口调用时注册一个 Watcher,一旦此时有子节点被创建,
//Zookeeper服务端就会想客户端发出一个“子节点变更”的事件通知,于是,客户端在收到这个
//时间通知之后就可以再次调用getChildren方法来获取新的子节点列表。
List<String> childrenList = zk.getChildren(path, true);
System.out.println(childrenList);
//再次创建子节点
zk.create(path+"/c2", "".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
Thread.sleep( Integer.MAX_VALUE );
}
@Override
public void process(WatchedEvent event) {
if (KeeperState.SyncConnected == event.getState()) {
if (EventType.None == event.getType() && null == event.getPath()) {
connectedSemaphore.countDown();
} else if (event.getType() == EventType.NodeChildrenChanged) {
try {
System.out.println("ReGet Child:"+zk.getChildren(event.getPath(),true));
} catch (Exception e) {}
}
}
}
}
zk使用场景
数据发布/订阅
数据发布/订阅(publish/subscribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到zookeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推push模式和拉pull模式,在推模式中,服务端主动将数据更新发送给所有订阅的客户端; 而拉模式则是由客户端主动发送请求来获取最新数据,通常客户端都采用定时进行拉取的方式。zookeeper采用的是推拉相结合的方式,客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到Zookeeper上进行集中管理,通常情况下,应用在启动的时候都会主动到Zookeeper服务端上进行一次配置信息的获取,同事,在指定节点上注册Watcher监听,这样只要配置信息发生变更,服务端都能实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
命名服务
命名服务是指通过指定的名字来获取资源或者服务的地址,提供者的信息,比如阿里的dubbo就使用zk来作为其命名服务,维护全局的服务地址列表,服务提供者在启动的时候,向ZK上的指定节点/dubbo/${serviceName}/providers目录下写入自己的URL地址,这个操作就完成了服务的发布。服务消费者启动的时候,订阅/dubbo/{serviceName}/providers目录下的提供者URL地址, 并向/dubbo/{serviceName} /consumers目录下写入自己的URL地址。所有向ZK上注册的地址都是临时节点,这样就能够保证服务提供者和消费者能够自动感应资源的变化。 通过使用命名服务,客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息
java语言中的JNDI便是一种典型的命名服务,开发人员常常使用应用服务器自带的JNDI实现来完成数据源的配置和管理,使用JNDI后,开发人员可以完全不需要关心与数据库相关的任何信息,包括数据库类型,JDBC驱动类型,用户名和密码等。 Zk提供的命名服务功能与JNDI技术类似,能够帮助应用系统通过一个资源引用的方式实现对资源的定位和使用。在分布式环境中,上层应用常常需要一个全局唯一的名字,如何使用zk来实现一套分布式全局唯一ID的呢?
全局唯一ID,我们会想到uuid,uuid确实是通用唯一识别码,非常简便地保证分布式环境中的唯一性,一个标准的uuid是一个包含32位字符和4个短线的字符串。虽然uuid很简便,但uuid存在一些不足的地方,比如生成的字符串长度过长,而且含义不明,根据生成的字符串开发人员基本看不出任何其表达的含义,其实我们可以通过调用ZK节点创建的API接口创建一个顺序节点,并且在API返回值中返回这个节点的完整名字,这个名字可以包含用户自定义的前缀,这样就可以生成全局唯一的id并且含义清楚。
master选举
在集群的所有机器中选举出一台机器作为master,针对这个需求,我们可以选择常见的关系型数据库中的主键特性来实现:集群中的所有机器都向数据库中插入一条相同主键ID的记录,数据库会帮助我们自动进行主键冲突检查,在所有进行插入操作的客户端机器中,只有一台机器能够成功,那么我们就认为向数据库中成功插入数据的客户端机器成为master。咋一看,这个方案确实可行,依靠关系型数据库中的主键特性确实能够很好地保证在集群中选举出唯一的一个master,但是如果选举出来的master挂了,该怎么处理呢?
显然,关系型数据库无法通知我们这个事件,而zk可以做到这一点,利用zk的强一致性,能够很好地保证在分布式高并发情况下节点的创建的全局唯一性,也即zk将会保证客户端无法重复创建一个已经存在的数据节点,如果同时有多个客户端请求创建同一个节点,那么最终一定只有一个客户端请求能够创建成功,利用这个特性,就能很容易在分布式环境进行master选举了。 每台机器向zk上创建一个临时节点,只有一台机器能够创建成功,成功创建节点的机器就成为master,其他没有创建成功的机器在创建的节点上注册watcher,用于监控当前master机器是否存活,一旦发现当前master挂了,其余客户端将会重新进行master选举。
分布式锁
排它锁
在java中,synchronized机制和ReentrantLock用来表示锁,在zk中,没有类似于这样的API可以直接使用,而是通过在zk上的数据节点来表示一个锁,比如/exclusive_lock/lock节点就可以被定义为一个锁,在需要获取排它锁时,所有的客户端试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock,最终只有一个客户端能够创建成功,创建成功的客户端就获取了锁,没有获取到锁的客户端就在/exclusive_lock节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。
释放锁
- /exclusive_lock/lock是一个临时节点,当获取锁的客户端机器发生宕机,zk上的这个临时节点就会被删除,
- 正常执行完业务逻辑后,客户端就会自动将自己创建的临时节点删除。
共享锁
- 创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的Watcher监听
- 确定自己的节点序号在所有子节点中的顺序
- 对于读请求,如果没有比自己序号小的子节点,或是所有比自己序号小的节点都是读请求,那么表明自己已经成功获取到了共享锁,开始执行读取逻辑
- 对于写请求,如果自己不是序号最小的节点,就需要进入等待
- 接收到watcher通知后,重复步骤1
释放锁的逻辑和排他锁是一致的。
当一个客户端移除自己的共享锁后,大量的Watcher通知和”子节点列表获取“两个操作重复运行,并且绝大多数的运行结果都是判断出自己并非是序号最小的节点,从而继续等待下一次通知,客户端无端地接收到过多与自己无关的事件通知显然不太合理,如果在集群规模比较大的情况,不仅会对zk服务器造成巨大的性能影响和网络冲击,如果同一时间多个节点对应的客户端完成事务,zk服务器就会在短时间内向其余客户端发送大量的事件通知,这就造成了羊群效应。造成这一局面的原因在于没找准客户端真正的关注点,其实每个客户端是判断自己是否是所有子节点中序号最小的,每个节点只需要关注比自己序号小的那个相关节点的变更情况就可以了,而不需要关注全局的子列表变更情况。
zk的应用
zk在yarn中的应用
YARN主要由ResourceManager、NodeManager、ApplicationMaster(AM)和Container四部分组成,RM作为全局的资源管理器,负责整个系统的资源管理和分配,但RM存在单点问题,对此YARN设计了一套Active/Standby模式的ResourceManager HA架构:
RM是基于zk实现的ActiveStandbyElector组件来确定RM的状态是Active还是standby,具体做法:
创建锁节点, 在zk上会有一个类似/yarn-leader-election/yarn-rm-cluster的锁节点,所有的rm在启动的时候,都会去竞争写一个lock子节点,该节点的类型是临时节点,zk能够保证我们最终只有一个RM能够创建成功,创建成功的RM就切换为Active状态,没有成功的RM就是Standby状态。
注册wather监听 : 所有standby状态的RM都会向锁路径注册一个节点变更的Watcher监听,利用临时节点的特性,能够快速感知到Active状态的RM运行状态
主备切换: 当Active状态的RM出现重启或者挂掉的异常情况时,其在zk上创建的lock节点也会随之被删除,此时其余各个standby状态的RM都会接收到来自zk服务端的watcher事件通知,然后重复步骤1执行
此外RM内部的一些状态信息也是存储在zk上。
zk在kafka上的应用
Kafka是一个吞吐量极高的分布式消息系统,其整体设计是典型的发布与订阅模式系统,在kafka集群中,没有中心节点的概念,集群中的所有服务器是对等的,可以在不做任何配置变更的情况下实现服务器的添加和删除。虽然broker是分布式部署并且相互之间是独立运行的(kafka的一些概念这里笔者不做介绍),但还是需要有一个注册系统能够将整个集群的broker服务器管理起来,kafka使用了zk,在zk上会有一个专门用来进行broker服务器列表记录的节点,路径为/brokers/ids, 每个broker服务器在启动时,都会到zk上进行注册,即到broker节点下创建自己的节点,/brokers/ids/[0-N], 不同的broker必须使用不同的broker id进行注册,创建完broker节点(同样是临时节点)后,每个broker就会将IP地址和端口号等信息写入到该节点去。
在kafka中会将一个topic的消息分成多个分区并将其分到多个broker上,这些分区信息和broker的对应关系也是由zk维护的,由专门的节点来记录,其节点路径为/brokers/topics, kafka中的每一个Topic都会以/brokers/topics/[topic]的形式记录在这个节点下, broker服务器在启动后,会在对应的topic节点下注册自己的borker id,并写入针对该topic自己负责的分区总数,/brokers/topics/logins/3 ->2 这个节点表明broker id为3 的一个broker服务器,对于login这个topic消息提供了2个分区进行消息存储。
生产者均衡
kafka是分布式部署的broker服务器,会对同一个topic的消息进行分区并将其分布到不同的broker服务器上,因此,生产者需要将消息合理地发送到这些分布式的broker上,如何进行负载均衡呢?在每一个broker启动时,会首先完成broker注册过程,诸如”有哪些可订阅的topic的元数据信息“, 生产者就能够通过这个节点变化来动态感知到broker服务器列表的变更,kafka的生产者会对zk上broker的增减、topic的增减、broker和topic关联关系的变化等事件注册watcher监听,通过zk的watcher通知,生产者可以动态获取broker和topic的变化情况。
消费者均衡
- 每个消费者服务器在启动的时候,都会到zk的指定节点下创建一个属于自己的消费者节点,例如 /consumers/[group_id]/ids/[consumer_id],完成节点创建后,消费者就会将自己订阅的Topic信息写入该节点,(这个节点也是一个临时节点,第一单消费者服务器出现故障或者下线就会被删掉)
- 每个消费者都需要关注所属消费者组中消费者服务器的变化情况,即对/consumers/[group_id]/ids节点注册子节点变化的Watcher监听,一旦发现消费者新增或减少,就会触发消费者的负载均衡。
- 对broker服务器的变化注册监听: 消费者需要对/broker/ids[0-N]中的节点进行监听的注册,如果发现broker服务器列表发生变化,就根据具体情况来决定是否需要进行消费者的负载均衡。
- 进行消费者负载均衡: 为了能够让同一个topic下不同分区的消息尽量均衡地被多个消费者消费而进行的一个消费者与消息分区分配的过程。通常,对于一个消费者分组,如果组内的消费者服务器发生变更或broker服务器发生变更,会触发消费者负载均衡。
这里对kafka的具体负载均衡算法不做具体介绍,有兴趣的可以查阅相关资料。
zk在dubbo中的应用
我们以服务”com.foo.BarService“这个服务来做具体介绍:
服务提供者: 服务提供者在初始化启动的时候,会首先在zookeeper的/dubbo/com.foo.BarService/providers节点下创建一个子节点,并写入自己的URL地址,这就代表了”com.foo.BarService“这个服务的一个提供者。
服务消费者
服务消费者会在启动的时候,读取并订阅zk上/dubbo/com.foo.BarService/providers节点下的所有子节点,并解析出所有提供者的url地址作为该服务地址列表,然后开始发起正常调用。同时,服务消费者还会在zk的/dubbo/com.foo.BarService/consumers节点下创建一个临时节点,并写入自己的url地址,这就代表了com.foo.BarService这个服务的一个消费者。
监控中心
监控中心是dubbo中服务治理体系的重要一环,它需要知道一个服务的所有提供者和订阅者,以及变化情况,监控中心在启动的时候,会通过zk的/dubbo/com.foo.BarService节点来获取所有提供者和消费者的URL地址,并注册Watcher来监听其子节点变化。