目录
一,简介
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
ZooKeeper本质上是一个分布式的小文件存储系统。提供基于类似文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理,从而来维护和监控存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。比如:统一命名服务、分布式配置管理、分布式消息队列、分布式锁、分布式协调等功能。
ZooKeeper是一个集中的服务,用于维护配置信息、命名、提供分布式同步、提供组服务等。
特性:
- 全局数据一致性:集群中每个server保存一份相同的数据副本,客户端连接哪个server都展示一致的数据;
- 可靠性:如果消息被其中一台服务器接收,则将被所有的服务器接收;
- 顺序性:包括全局有序和偏序:
- 全局有序:如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都在消息b前被发布;
- 偏序:如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面;
- 数据更新原子性:一个更新数据要么成功(半数以上的节点成功即视为成功),要么失败,不存在中间状态;
- 实时性:zookeeper保证客户端在一个时间间隔范围内获得服务器的更新信息。
1,工作机制
Zookeeper是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据(在zookeeper中),然后接受观察者(客户端)的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应反应。
执行步骤:
- 服务启动时在zookeeper中注册信息(创建临时节点);
- zookeeper客户端获取当前在zookeeper中注册的在线服务列表,并且注册监听;
- 当服务的状态发生变化,Zookeeper将通知注册的客户端;
- 客户端执行相应的反应。
2,数据结构
Zookeeper数据结构与Unix文件系统类似,整体上是树形结构,每个节点称做一个znode,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据;
如果在创建znode时Flag设置为EPHEMERAL(临时节点),那么当创建这个znode的节点(服务)和Zookeeper失去连接后,这个znode将不再存在在Zookeeper里;
Zookeeper使用Watcher察觉事件信息。当客户端接收到事件信息,比如连接超时、节点数据改变、子节点改变,可以调用相应的行为来处理数据。
znode特点:
- 具有文件和目录两种特点:每个节点既像文件一样维护数据、元信息、时间戳等信息,又像可以作为一个目录的路径;
- 具有原子性:
- 存储数据大小有限制:每个znode的数据大小最多1M;
- 每个节点由3哥部分组成:
- stat:状态信息,保存该znode的版本、权限的信息;
- data:数据信息,保存该znode的数据;
- children:子节点信息,保存该znode下的子节点。
3,Zookeeper集群角色
(1)Leader
领导者,集群中各个服务器的调度者;对于create、setData、delete等有写操作的请求,不论哪个服务(Leader和Follower)收到该请求,都要统一转发给Leader处理,由Leader决定编号和执行操作。
(2)Follower
随从,
- 处理客户端非事务请求;
- 收到写请求时,转发请求给Leader;
- 当Leader出现问题时参与选举投票。
(3)Observer
观察者角色,可选,常用于访问量比较大的集群,并不影响集群事务处理能力,提搞集群的非事务处理能力,
观察和同步zookeeper集群的最新状态变化;
- 处理非事务请求;
- 将事务请求转发给Leader;
- 不参与投票,只提供非事务服务处理能力。
二,集群安装
安装前需要确保JDK环境正常。
通常使用奇数台服务器:为了保证Leader的选举(跟它使用的选举算法Paxos算法有关)
1,下载
Apache ZooKeeper官网地址https://zookeeper.apache.org/
2,解压
tar -zxvf apache-zookeeper-3.8.0-bin.tar.gz
3,修改配置
(1)将/usr/local/apache-zookeeper-3.8.0-bin/conf中zoo_sample.cfg 修改为 zoo.cfg;
zoo_sample.cfg:是zookeeper提供的样例配置文件,实际的配置文件要用zoo.cfg。
(2)编辑配置文件zoo.cfg:
①修改dataDir路径
dataDir=/usr/local/apache-zookeeper-3.8.0-bin/zkData
dataDir:指定的路径是保存Zookeeper中数据的目录。
②添加集群信息配置
server.1=192.168.2.133:2888:3888
server.2=192.168.2.134:2888:3888
server.3=192.168.2.135:2888:3888
server.A=B:C:D格式的配置含义:
A:是一个数字,表示这是第几号服务器;对应dataDir目录下myid文件里面的值;
B:服务器的地址;
C:端口号,服务器Follower与集群中的Leader服务器交换信息的端口;
D:端口号,当集群中的Leader宕机时,重新选举Leader时,服务器通过这个端口在选举时相互通信。
(3)新建zkData文件夹
在/usr/local/apache-zookeeper-3.8.0-bin/路径下新建zkData文件夹
这个路径下的文件夹名对应zoo.cfg配置文件中dataDir指定的路径名。
(4)在zkData目录(配置文件zoo.cfg的dataDir指定的路径)下创建myid文件,并输入server编号
文件myid:里面仅有一个数值,这个数是这台服务器的zookeeper的身份标号,在Zookeeper启动时读取此文件的这个数字,与zoo.cfg里面配置的集群信息进行比较,进而判断是哪个服务器。
4,启动zookeeper
bin/zkServer.sh start
5,查看状态
bin/zkServer.sh status
6,启动客户端
#连接本地的zookeeper服务
bin/zkCli.sh
#指定远程zookeeper服务地址,连接远程zookeeper服务
bin/zkCli.sh -server 192.168.2.134
7,退出客户端
quit
8,停止zookeeper
bin/zkServer.sh stop
如果要想使用 Observer 模式:
- 需要在对应节点的配置文件添加配置:peerType=observer
- 必须在配置文件指定哪些节点被指定为 Observer,如:server.1:localhost:2888:3888:observer
三,配置文件详解
- tickTime=200:每个心跳的毫秒数,通信心跳时间,Zookeeper客户端与服务器心跳时间;
- initLimit=10:初始同步阶段可以占用的心跳数,Follower和Leader初始连接时最多的连接次数,尝试initLimit次数之后便不在连接,认为连接不上;
- syncLimit=5:在发送请求和获得确认之间可以通过的心跳次数,Leader和Follower之间通信时间如果超过syncLimit*tickTime,则Leader认为Follower宕机,并将该Follower从服务器列表删除;
- dataDir=路径:数据所在的目录,保存zookeeper中的数据;
- clientPort=2181:客户端将要连接的端,默认2181;
四,命令行客户端使用
1,命令行客户端
(1)启动客户端
bin/zkCli.sh -server 192.168.2.134:2181
(2)查看客户端所有操作命令
help
2,查看znode节点信息
(1)查看指定znode中包含的子znode
(2)查看指定znode的详细信息
节点属性:
- 第一行:当前znode中包含的子znode;
- cZxid:创建节点的事务zxid,
- 每次修改zookeeper状态都会产生一个zookeeper事务ID;
- 事务ID是zookeeper中所有修改的总的次序,每次修改都有唯一的zxid;
- 如果zxid1小于zxid2,则zxid1在zxid2之前发生;
- ctime:当前znode被创建的时间;
- mZxid:当前znode最后更新的事务zxid;
- mtime:当前znode最后被修改的时间;
- pZxid:当前znode最后被更新的子节点的zxid;
- cversion:子节点版本号,当前znode子节点变化号,用于标记znode子节点被修改的次数;
- dataversion:数据版本号,当前znode数据变化号,每次对该节点做set操作,都会增加1,以保证数据更新时的先后顺序问题;
- aclVersion:当前znode访问控制列表的变化号;
- ephemeralOwner:该znode是否是临时节点,如果是则这个值是该znode拥有者的session id;如果值为0则不是临时节点;
- dataLength:该znode的数据长度;
- numChildren:该znode的子节点数量。
对于zookeeper来说,每次发生变化都会产生一个唯一的事务id——zxid(zookeeper transaction id);通过zxid,可以确定更新操作的先后顺序。
(3)获得节点的值
(4)查看节点状态
3,创建znode节点
(1)节点类型介绍
- 永久节点:客户端和服务器断开连接后,创建的节点不删除
- 无序号:zookeeper没有给给节点名称进行顺序编号,一个名字只能存在一个;
- 顺序编号:zookeeper给该节点名称顺序编号;创建节点时zookeeper会自动给名称后附件一个值作为顺序号;顺序号是一个单调递增的值,由父节点维护;
- 临时节点:客户端与服务器断开连接后,该节点被服务器自动删除;不能创建子节点:
- 无序号:同上;
- 顺序编号:同上。
这四种类型的 Znode 节点,分别对应的属性(API中):
- PERSISTENT:永久节点
- EPHEMERAL:临时节点
- PERSISTENT_SEQUENTIAL:永久节点、序列化
- EPHEMERAL_SEQUENTIAL:临时节点、序列化
(2)创建永久化不带顺序编号的节点
(3)创建永久带顺序编号接电脑
(4) 创建临时不带顺序编号节点
(5)创建临时带顺序编号节点
(6)退出当前客户端并重新登录客户端
可以看到临时节点都已经不存在了!
4,修改znode节点的值
5,删除znode节点
(1)删除节点
只能删除没有子节点的节点。
(2)递归删除节点
递归的把当前节点和子节点全部删除。
6,监听节点
在zookeeper中,通过Watcher机制实现分布式的通知功能(多个订阅者同时监听一个主题,当该主题发生变化时,会通知所有订阅者使他们能够做出相应的处理);
zookeeper客户端向zookeeper服务端注册一个Watcher监听,当服务端的一些事件触发了这个Watcher,服务端就会向注册了的客户端发送事件通知;
触发事件种类:节点创建、节点删除、节点改变、子节点改变;(我将其分为值变化和路径变化)
即:客户端注册监听znode,当该节点发生变化时(包括:数据改变、节点删除、子znode节点增加删除),Zookeeper快速通知监听的客户端。
监听器原理:
- 在主线程中创建Zookeeper客户端,其中包括两个线程:
- Listener,负责监听数据或路径变化
- connect,负责网络连接通信,
- zk客户端通过connect线程将注册的监听时间发送给Zookeeper;
- 在zookeeper服务端的注册监听器列表中,将注册的监听事件添加到列表中;
- 当zookeeper服务端监听到有数据或路径变化,就会将这个消息发送给Listener线程;
- Listerner线程调用process()方法,对时间做响应。
总之:
①客户端向服务端注册Watcher
②服务端事件发生触发Watcher
③客户端回调Watcher得到触发事件情况,并调用process方法处理
(1)监听节点的值变化
收到数据变化的监听:
注意:一次设置监听只能监听一次事件,再次修改值就不会在收到监听事件。
Watcher机制:
- 一次性触发:一个watcher event只会触发一次客户端的监听;后续在发生该事件不会再次触发,除非再一次注了;
- 时间封装:使用WatherEvent对象封装服务端事件并传递;WatherEvent包含每个事件的三个基本属性:
- keeperState:通知状态
- EventType:事件类型
- path:节点路径
- event异步发送:watcher通知事件从服务器端发送到客户端是异步进行的;
(2)监听子节点路径变化
注意:一次设置监听只能监听一次事件,再次修改值就不会在收到监听事件。
7,查看历史命令
redo:重新执行指定命令编号的历史命令,命令编号是history查看出来的前面的编号。
五,原生 Zookeeper Java API
1,引入zookeeper依赖
<!--zookeeper依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.8.0</version>
</dependency>
2,创建zookeeper客户端
public void init() throws IOException {
//连接zookeeper服务器
//Watcher 接口表示一个标准的事件处理器,其定义了事件通知相关的逻辑,
//包含 KeeperState 和 EventType 两个枚举类,分别代表了通知状态和事件类型,
//同时定义了事件的回调方法:process(WatchedEvent event)。
zkClient = new ZooKeeper(ipAddrs, sessionTimeout, new Watcher() {
@Override
//process 方法是 Watcher 接口中的一个回调方法,当 ZooKeeper 向客户端发
//送一个 Watcher 事件通知时,客户端就会对相应的 process 方法进行回调,从而
//实现对事件的处理。
public void process(WatchedEvent watchedEvent) {
//收到时间通知后要执行的响应
//再次启动监听(因为设置一次监听只能监听一次)
}
});
}
3,简单的操作
//创建子节点
public void create() throws Exception {
String nodeCreated = zkClient.create(
"/zoo", //要创建的节点路径
"dongwuyuan".getBytes(), //节点数据
ZooDefs.Ids.OPEN_ACL_UNSAFE,//节点权限
CreateMode.PERSISTENT//节点类型,
);
}
//获取子节点并监听节点变化
public void getChildren() throws KeeperException, InterruptedException {
List<String> children = zkClient.getChildren("/",true);
for(String child:children){
System.out.println(child);
}
}
//判断znode是否存在
public void exist() throws KeeperException, InterruptedException {
Stat stat = zkClient.exists("zoo",false);
System.out.println(stat == null ? "not exist" : "exist");
}
可以看到ZooKeeper()类的方法与zookeeper客户端命令行命令时对应的:
4,重点类和方法说明
(1)org.apache.zookeeper.ZooKeeper
Zookeeper时Java中客户端的主类,负责建立与zookeeper集群的会话,并提供与zookeeper命令行客户端命令你的对应的方法。
(2)org.apache.zookeeper.Watcher
Watcher接口是一个标准的事件处理器,它定义了事件通知相关的逻辑,包含了KeeperState(通知状态)和EventType(事件类型)两个枚举类,同时定义了时间的回调方法process(WatchedEvent event)。
(3)process()
process()方法是Watcher接口中的一个回调方法,当zookeeper向客户端发送给一个Watcher事件通知时,客户端就会调用相应的process()方法,从而实现对事件的处理。
六,应用场景
1,统一命名服务
在分布式系统中,命名服务使客户端应用能够通过指定名字获取资源或其他服务的地址和信息;可以使集群中的集群,通过名字指定服务地址。
此外,多个服务起同一个服务名时,在访问时只需要使用服务名而不用指定IP。
2,统一配置管理
将数据发布到zookeeper节点上,而订阅者动态获取数据,实现配置信息的集中管理和动态更新。
①将服务的配置信息从代码中的配置文件中提取出来放在zookeeper中配置,在修改配置时,不但不用重新部署服务,而且可以快速同步到各个节点上;
②一个集群中,多个节点的配置信息时一致的,不用重复配置;
可将配置管理交付Zookeeper实现:
将配置信息写入ZooKeeper上的一个znode节点上;
各个客户端服务器(对zookeeper来说是zk的客户端)监听这个znode;
一旦znode中的数据被修改,zookeeper将通知各个客户端服务器。
实现思想:订阅者应用在启动时主动从zookeeper中获取一次配置,同时在该节点注册一个Watcher,当该节点的信息发生变化时,zookeeper会实时通知订阅者的客户端,从而获取更新配置。
3,统一集群管理
在分布式环境中,将一个集群中各个服务的信息放入zookeeper中,可以实时掌握每个节点的状态;
可将集群管理交付Zookeeper实现:
将节点信息写入Zookeeper的一个znode;
将一个集群中的各个节点对应的znode放在同一个路径下;
监听这些znode可获取它的实时状态变化。
4,服务器动态上下线
在分布式系统中,客户端能实时感知到服务器的上下线;
执行流程:
- 服务端启动时,将自己的信息注册到zookeeper中(即在zookeeper中创建你你一个临时节点);
//连接zookeeper zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent event) { } }); //该服务在zookeeper上注册自己(创建一个临时顺序编号节点) String create = zk.create(parentNode + "/server", hostname.getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
- 客户端注册监听,并且获取当前在线服务器列表;
//连接zookeeper,注册监听 zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent event) { // 再次启动监听 try { getServerList(); } catch (Exception e) { e.printStackTrace(); } } }); //获取当前在线服务器列表 public void getServerList() throws Exception { // 1, 获取服务器子节点信息,并且对父节点进行监听;监听子节点路径的变化 List<String> children = zk.getChildren(parentNode, true); // 2,存储服务器信息列表 ArrayList<String> servers = new ArrayList<>(); // 3,遍历所有节点,获取节点中的主机名称信息 for (String child : children) { byte[] data = zk.getData(parentNode + "/" + child, false, null); servers.add(new String(data)); } }
- 当服务器节点上下线时,zookeeper通知客户端该事件;
- 客户端调用process()方法,重新获取服务器列表,并注册监听。
在这里,服务器和客户端是针对监听和被监听的应用来说的,对于zookeeper来说,这些服务器和客户端都是zookeeper的客户端;其实把服务端称作被监听的服务,把客户端成为监听的服务更合适。
思路:客户端刚开始从zookeeper中获取在线服务器列表,每当客户端收到zookeeper的事件通知,就调用process()方法,重新获取在线服务器列表。
5,软负载均衡
在zookeeper中记录每台服务器的访问数,让访问量最少的服务器去处理最新的客户端的请求。
6,分布式锁
zookeeper具有很好的保持数据一致性,可用于实现分布式锁。
在分布式系统中,通过对临界资源加锁保证多个进程能够有序的访问该临界资源;这个锁叫分布式锁。
执行流程:
- 客户端连接zookeeper,注册监听;
// 获取连接 zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() { @Override public void process(WatchedEvent watchedEvent) { //如果连接上zk,可以释放 if (watchedEvent.getState() == Event.KeeperState.SyncConnected){ connectLatch.countDown(); } // 如果前一个临时节点被删除,可以唤醒当前节点去获得锁 if (watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){ waitLatch.countDown(); } } });
- 判断永久根节点是否存在,如果不能存在创建;
// 判断根节点/locks是否存在 Stat stat = zk.exists("/locks", false); if (stat == null) { // 创建一下根节点 zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); }
- 加锁:
- zookeeper中,创建对应的临时顺序节点;
- 判断创建的节点是否是最小序号节点,如果是获取锁,如果不是,监听它序号前一个节点;
// 创建对应的临时带序号节点 currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点 List<String> children = zk.getChildren("/locks", false); // 如果children 只有一个值,那就直接获取锁; 如果有多个节点,需要判断,谁最小 if (children.size() == 1) { return; } else { Collections.sort(children); // 获取节点名称 seq-00000000 String thisNode = currentMode.substring("/locks/".length()); // 通过seq-00000000获取该节点在children集合的位置 int index = children.indexOf(thisNode); // 判断 if (index == -1) { System.out.println("数据异常"); } else if (index == 0) { // 就一个节点,可以获取锁了 return; } else { //如果节点前有节点,监听它前一个节点变化 waitPath = "/locks/" + children.get(index - 1); zk.getData(waitPath,true,new Stat()); //卡在这,等待前一个节点被删除的事件出现,唤醒这个等待 waitLatch.await(); return; } }
- 获取到锁处理完业务后,delete当前节点释放锁,然后下面的节点将收到通知。
zk.delete(this.currentMode,-1);
java.util.concurrent.CountDownLatch类:
- CountDownLatch类是一个同步计数器,构造时传入int参数,该参数就是计数器的初始值;
- 每调用一次countDown()方法,计数器减1;
- 当计数减至0时触发特定的事件;
- 计数器大于0 时,await()方法会阻塞程序继续执行。
实现思路:每个客户端创建一个临时顺序节点,只有顺序号最小的节点能获取到锁;通过后一个节点监听前一个节点的路径,当前一个客户端释放锁之后删除对应的节点之后,后一个节点监听到这个事件便获取锁。锁:zookeeper中的临时顺序节点;获取到锁:成为顺序号最小的顺序节点;释放锁:删除这个客户端对应的顺序节点。
锁分为保持独占和控制时序:
- 保持独占:所有试图获取这个锁的客户端,最终只有一个可以成功的
- 使用zookeeper实现思路:把zookeeper的一个znode看作一把锁,通过create znode的方式实现,所有的客户端都去创建这个znode,创建成功的客户端相当于拥有了锁;
- 控制时序:所有试图获取这个锁的客户端,最终都会被安排执行并排序;
- 使用zookeeper实现思路:提前创建一个永久节点,所有客户端在该永久节点创建临时有序节点,通过zookeeper自动生成的顺序序号保证子节点创建的时效性。
七,Curator框架
Curator使zookeeper的java客户端库,封装了java API,简化了zookeeper客户端的使用,解决原生 Zookeeper Java API不方便编程的问题。
本来在学习kafka,其中用到了zookeeper,所以就想着先看一下zookeeper对其进行简单的了解一下,并未打算深处原理的研究和代码编写方面的学习,所以关于① Curator的使用②客户端向服务端写数据流程③选举机制④权限认证⑤监控命令和配置信息⑥图形化工具 这些更深入、更底层的东西在后面补充研究吧。