一、ZK简介
(1)什么是ZK
(2)zk体系架构
(3)数据模型、节点
Zookeeper 这种数据结构有如下这些特点:
1. 每个子目录项如 NameService 都被称作为 znode,这个 znode 是它所在的路径唯一标识,如 Server1 这 个 znode 的标识为 /NameService/Server1
2. znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL(短暂的,相对应的就是PERSISTENT持久的) 类型的目录节点不能有子节点目录
四种类型的节点:
PERSISTENT:持久化目录节点,这个目录节点存储的数据不会丢失;
PERSISTENT_SEQUENTIAL:顺序自动编号的目录节点,这种目录节点会根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名;
EPHEMERAL:临时目录节点,一旦创建这个节点的客户端与服务器端口也就是 session 超时,这种节点会被自动删除;
EPHEMERAL_SEQUENTIAL:临时自动编号节点
3. znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据(类 式Hbase)
4. znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除
5. znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 Zookeeper 的核心特性,Zookeeper 的很多功能都是基于这个特性实现的,后面在典型的应用场景中会有实例介绍
(4)Watches
二、ZK流行的应用场景(六大功能)
1、统一命名服务(Name Service)
分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。
2、配置管理(Configuration Management)
配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中
下面写个程序来演示zk配置管理的功能:
SetConfig.java:
package zookeeper;
import java.io.IOException;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
public class SetConfig {
public static String url="192.168.8.91:2181";//连接服务器,2181是端口号,服务器会监视此端口
private final static String root="/myConf";
//数据库连接URL,username,password
private final static String urlNode=root+"/url";
private final static String usernameNode=root+"/username";
private final static String passwordNode=root+"/password";
//在cli界面这样访问权限:addauth digest password /myConf
private final static String auth_type="digest";
private final static String auth_password="password";
public static void main(String[] args) throws IOException, InterruptedException, KeeperException
{
ZooKeeper zk=new ZooKeeper(url, 3000, new Watcher(){
@Override
public void process(WatchedEvent event) {
System.out.println("触发了事件:"+event.getType());
}
});
while(ZooKeeper.States.CONNECTED!=zk.getState())
{
Thread.sleep(3000);
}
//加密操作,对一个session下创建的path进行加密,通常和CREATOR_ALL_ACL一起用
zk.addAuthInfo(auth_type, auth_password.getBytes());
if(zk.exists(root, true)==null)//判断某个path是否存在,并设置是否监控这个目录节点
{
zk.create(root, "root".getBytes(), Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
}
if(zk.exists(urlNode, true)==null)
{
zk.create(urlNode, "202.101.1.1".getBytes(),Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
}
if(zk.exists(usernameNode, true)==null)
{
zk.create(usernameNode, "wangfan".getBytes(),Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
}
if(zk.exists(passwordNode,true)==null)
{
zk.create(passwordNode, "michael".getBytes(),Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);
}
zk.close();
}
}
package zookeeper;
import java.io.IOException;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
public class Client implements Watcher{
public static String url="192.168.8.91:2181";//连接服务器,2181是端口号,服务器会监视此端口
private final static String root="/myConf";
//数据库连接URL,username,password
private String urlNode=root+"/url";
private String usernameNode=root+"/username";
private String passwordNode=root+"/password";
private final static String auth_type="digest";
private final static String auth_password="password";
private String urlString;
private String username;
private String password;
ZooKeeper zk=null;
public String getUrlString() {
return urlString;
}
public void setUrlString(String urlString) {
this.urlString = urlString;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void initValue() throws KeeperException, InterruptedException
{
urlString=new String(zk.getData(urlNode, false, null));//url变化不会触发事件,getData()方法是获取这个path对应的目录节点存储的数据,同时还可以设置是否监控这个目录节点数据的状态
username=new String(zk.getData(usernameNode, true, null));
password=new String(zk.getData(passwordNode, true, null));
}
public ZooKeeper getZK() throws IOException, InterruptedException
{
zk=new ZooKeeper(url,3000,this);
zk.addAuthInfo(auth_type, auth_password.getBytes());
while(ZooKeeper.States.CONNECTED!=zk.getState())
{
Thread.sleep(3000);
}
return zk;
}
@Override
public void process(WatchedEvent event) {
// TODO 自动生成的方法存根
if(event.getType()==Watcher.Event.EventType.None)
{
System.out.println("连接服务器成功!");
}
else if(event.getType()==Watcher.Event.EventType.NodeCreated)
{
System.out.println("节点创建成功!");
}
else if(event.getType()==Watcher.Event.EventType.NodeChildrenChanged)
{
System.out.println("子节点更新成功!");
//读取新的配置
try {
initValue();
} catch (KeeperException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
else if(event.getType()==Watcher.Event.EventType.NodeDataChanged)
{
System.out.println("节点更新成功!");
//读取新的配置
try {
initValue();
} catch (KeeperException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
} catch (InterruptedException e) {
// TODO 自动生成的 catch 块
e.printStackTrace();
}
}
else if(event.getType()==Watcher.Event.EventType.NodeDeleted)
{
System.out.println("节点删除成功!");
}
}
public static void main(String[] args) throws IOException, InterruptedException, KeeperException
{
Client zkTest=new Client();
ZooKeeper zk=zkTest.getZK();
zkTest.initValue();
int i=0;
while(true)
{
System.out.println(zkTest.getUrlString());
System.out.println(zkTest.getUsername());
System.out.println(zkTest.getPassword());
System.out.println("-----------------------------------------");
Thread.sleep(10000);
i++;
if(i==10)
break;
}
zk.close();
}
}
运行:
(1)首先运行setConfig.java,创建节点
(2)运行Client.java
打开cli,命令如下,改变username的值:
同时发现客户端也跟着变化:
当改变url的值得时候,发现并没有变化,因为getData的时候并没有监控这个目录节点数据的状态
3、集群管理
Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election(Leader选举)。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL(短暂有次序) 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。
4、分布式锁
5、队列管理
Zookeeper 可以处理两种类型的队列:
1. 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
2. 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 /synchronizing 目录的所有目录节点,也就是member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start
FIFO 队列用 Zookeeper 实现思路如下:
实现的思路也非常简单,就是在特定的目录下创建 PERSISTENT_SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。