zookeeper详解

1.Zookeeper是什么

官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
上面的解释有点抽象,简单来说Zookeeper维护一个类似文件系统的数据结构+监听通知机制。
1.1 文件系统

Zookeeper维护一个类似文件系统的数据结构:

每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

有四种类型的znode:

  • PERSISTENT-持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

1.2  监听通知机制(Watcher机制)

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。

1.3 ZooKeeper 的 Watcher 机制

总的来说可以分为三个过程:客户端注册 Watcher、服务器处理 Watcher 和客户端回调 Watcher,其内部各组件之间的关系如图: 

ZooKeeper 的 Watcher 具有以下几个特性。

一次性
无论是服务端还是客户端,一旦一个 Watcher 被触发,ZooKeeper 都会将其从相应的存储中移除。因此,在 Watcher 的使用上,需要反复注册。这样的设计有效地减轻了服务端的压力。

客户端串行执行
客户端 Watcher 回调的过程是一个串行同步的过程,这为我们保证了顺序,同时,需要注意的一点是,一定不能因为一个 Watcher 的处理逻辑影响了整个客户端的 Watcher 回调,所以,我觉得客户端 Watcher 的实现类要另开一个线程进行处理业务逻辑,以便给其他的 Watcher 调用让出时间。

轻量
WatcherEvent 是 ZooKeeper 整个 Watcher 通知机制的最小通知单元,这个数据结构中只包含三部分内容:通知状态、事件类型和节点路径。也就是说,Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。例如针对 NodeDataChanged 事件,ZooKeeper 的Watcher 只会通知客户端指定数据节点的数据内容发生了变更,而对于原始数据以及变更后的新数据都无法从这个事件中直接获取到,而是需要客户端主动重新去获取数据——这也是 ZooKeeper 的 Watcher 机制的一个非常重要的特性。

1.4 ZooKeeper 提供了权限控制机制(基于ACL(访问控制列表))

zk提供了基于ACL(访问控制列表)权限控制机制,zk提供了四种身份认证:

world:默认方式,相当于全世界都能访问
auth:代表已经认证通过的用户(cli中可以通过addauth digest user:pwd 来添加当前上下文中的授权用户)
digest:即用户名:密码这种方式认证,这也是业务系统中最常用的
ip:使用Ip地址认证。 

这样也方便基于现用的用户系统进行集成。 zkClient是基于zooKeeper客户端的二次封装,使用起来更简单方便。 添加授权信息接口:

 

public void addAuthInfo(final String scheme, final byte[] auth) 

 

需要在完成zooKeeper连接创建后,给该连接会话添加上相关的权限信息,之后凡是通过该会合对zk的服务器端任何操作都会带上该权限。

创建节点时需要显式的调用有ACL参数的接口,否则创建的就是world的权限节点,任何人都可以访问:

public void createEphemeral(final String path, final Object data, final List<ACL> acl) 
public String createEphemeralSequential(final String path, final Object data, final List<ACL> acl)
public void createPersistent(String path, Object data, List<ACL> acl)
public void setAcl(final String path, final List<ACL> acl)
public String createPersistentSequential(String path, Object data, List<ACL> acl) 

对于删除节点操作,其作用范围是其子节点,即对节点N1添加权限后,可以删除,但对于N1的子节点来说,就必须使用相对应的权限信息才能删除。

// 创建会话
final ZkClient zkClient = new ZkClient("192.168.1.220:2181", 5000, 5000);
// 构造权限信息
List<ACL> acl = new ArrayList<ACL>();
acl.add(new ACL(ZooDefs.Perms.ALL, new Id("digest", DigestAuthenticationProvider.generateDigest("admin:admin123"))));//需要Digest后数据
// 创建节点
zkClient.create("/path1", "helloworld!", acl, CreateMode.EPHEMERAL);

// 未添加授权信息,读取失败
try {
    System.out.println(zkClient.readData("/path1"));
    zkClient.delete("/path1");
} catch (Exception e) {
    System.out.println("权限不够");
}
// 添加授权信息
zkClient.addAuthInfo("digest", "admin:admin123".getBytes());
// 读取节点
System.out.println(zkClient.readData("/path1"));
// 删除节点,成功
zkClient.delete("/path1");
System.out.println(zkClient.exists("/path1"));

 

 

2. Zookeeper能做什么

zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。

假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。

 

3.Zookeeper客户端框架zkClient的使用

ZkClient将ZK原生API中的异步处理进行了同步化。

   下面是对于节点的增删改查,注意znode不能重复创建,不然会报错。 KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /wbs

public class ZkClientBase {
    /** zookeeper地址  集群配置 */
    static final String CONNECT_ADDR = "47.102.102.112:2180,47.102.122.102:2182,47.102.102.132:2183";
    /** session超时时间 */
    static final int SESSION_OUTTIME = 5000;//ms
    public static void main(String[] args) throws Exception {
        ZkClient zkc = new ZkClient(new ZkConnection(CONNECT_ADDR), 5000);
        //1. 创建临时节点
        zkc.createEphemeral("/wbs");
        //2.创建持久节点支持递归创建,并不支持递归赋值, true 代表支持递归创建
        zkc.createPersistent("/小王/zz", true);
        Thread.sleep(10000);
        zkc.delete("/wbs");
        //递归删除/super
        zkc.deleteRecursive("/小王");
        //2. 设置path和data 并且读取子节点和每个节点的内容
        zkc.createPersistent("/super", "1234");
        zkc.createPersistent("/super/c1", "c1内容");
        zkc.createPersistent("/super/c2", "c2内容");
        List<String> list = zkc.getChildren("/super");
        for(String p : list){
            System.out.println(p);
            String rp = "/super/" + p;
            String data = zkc.readData(rp);
            System.out.println("节点为:" + rp + ",内容为: " + data);
        }
        //3. 更新和判断节点是否存在
        zkc.writeData("/super/c1", "新内容");
        //读取某个节点的信息
        Object o = zkc.readData("/super/c1");
    }
}

ZKClient Watch的使用
我们发现,上述ZKClient里面并没有类似的watcher、watch参数,这也就是说我们开发人员无需关心反复注册watcher的问题,zkclient给我们提供了一套监听方式,我们可以使用监听节点的方式进行操作,剔除了繁琐的反复watcher操作、简化了代码的复杂程度

subscribeChildChanges方法 订阅子节点变化
参数1:path路径
参数2:实现了IZKChildListener接口的类(如:实例化IZKClientListener类) 只需要重写其handleChildChanges(String parentPath, List currentChild)方法。其中

parentPath 为监听节点全路径
currentChilds为新的子节点列表
IZKChildListener事件说明针对于下面的事件触发:
新增,删除 节点(包括当前的节点和子节点的变化)
注意: 不监听节点内容的变化

public class ZkClientWatcher1 {
    /** zookeeper地址 */
    static final String CONNECT_ADDR = "47.102.102.112:2180,47.102.122.102:2182,47.102.102.132:2183";
    /** session超时时间 */
    static final int SESSION_OUTTIME = 5000;//ms
    public static void main(String[] args) throws Exception {
        ZkClient zkc = new ZkClient(new ZkConnection(CONNECT_ADDR), 5000);
        
        //添加监听对当前节点和其子节点变化。
        zkc.subscribeChildChanges("/wbs", new IZkChildListener() {
            @Override
            public void handleChildChange(String parentPath, List<String> currentChilds) throws Exception {
                System.out.println("parentPath: " + parentPath);
                System.out.println("currentChilds: " + currentChilds);
            }
        });
        Thread.sleep(Integer.MAX_VALUE);
    }
}

subscribeDataChanges 订阅内容变化(监听节点内容的变化)
和前面的subscribeChildChanges类似

参数1 路径
参数2 IZkDataListener对象,重写handleDataDeleted(String path) 方法,可以得到删除节点的path 重写handleDataChange(String path, Object data)可以得到变更的节点和变更的内容
举例

public class ZkClientWatcher2 {
    /** zookeeper地址 */
    static final String CONNECT_ADDR = "47.102.102.102:2180,47.102.102.102:2182,47.102.102.102:2183";
    /** session超时时间 */
    static final int SESSION_OUTTIME = 5000;//ms
    public static void main(String[] args) throws Exception {
        ZkClient zkc = new ZkClient(new ZkConnection(CONNECT_ADDR), 5000);
        zkc.createPersistent("/super", true);
        //添加监听对当前节点和其子节点变化(新增和删除)。
        zkc.subscribeDataChanges("/super", new IZkDataListener() {
            @Override
            public void handleDataDeleted(String path) throws Exception {
                System.out.println("删除的节点为:" + path);
            }
            //添加监听对当前节点和其子节点变化(修改)。
            @Override
            public void handleDataChange(String path, Object data) throws Exception {
                System.out.println("变更的节点为:" + path + ", 变更内容为:" + data);
            }
        });
        Thread.sleep(3000);
        zkc.writeData("/super", "456", -1);
        Thread.sleep(2000);
        zkc.deleteRecursive("/super");
        Thread.sleep(Integer.MAX_VALUE);
    }
}

细心的人会发现上面有很多的Thread.sleep(3000) 是应为 添加监听对象的时候,会新开个线程。

posted @ 2019-03-26 17:08  好记性不如烂笔头=>  阅读(843)  评论(0编辑  收藏  举报