Zookeeper应用场景实战(一)

本文探讨了Zookeeper的原生Java客户端API和第三方库ApacheCurator的使用,强调了原生API的不足,如一次性Watcher、缺乏重连机制和复杂异常处理。Curator提供了更完善的API和解决方案,适用于日常开发。还介绍了Zookeeper在分布式命名服务中的应用,如API目录、节点命名和ID生成。
摘要由CSDN通过智能技术生成

1. Zookeeper Java客户端

ZooKeeper应用的开发主要通过Java客户端API去连接和操作ZooKeeper集群。可供选择的Java客户端API有:

  • ZooKeeper官方的Java客户端API。
  • 第三方的Java客户端API,比如Curator。

ZooKeeper官方的客户端API提供了基本的操作,例如,创建会话、创建节点、读取节点、更新数据、删除节点和检查节点是否存在等。不过,对于实际开发来说,ZooKeeper官方API有一些不足之处,具体如下:

  • ZooKeeper的Watcher监听是一次性的,每次触发之后都需要重新进行注册。
  • 会话超时之后没有实现重连机制。
  • 异常处理烦琐,ZooKeeper提供了很多异常,对于开发人员来说可能根本不知道应该如何处理这些抛出的异常。
  • 仅提供了简单的byte[]数组类型的接口,没有提供Java POJO级别的序列化数据处理接口。
  • 创建节点时如果抛出异常,需要自行检查节点是否存在。
  • 无法实现级联删除。

总之,ZooKeeper官方API功能比较简单,在实际开发过程中比较笨重,一般不推荐使用。

1.1 Zookeeper 原生Java客户端使用

引入zookeeper客户端依赖

<!-- zookeeper client -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
</dependency>

注意:尽量保持与服务端版本一致,不然会有很多兼容性的问题

ZooKeeper原生客户端主要使用org.apache.zookeeper.ZooKeeper这个类来使用ZooKeeper服务。

ZooKeeper常用构造器
 ZooKeeper (connectString, sessionTimeout, watcher)
  • connectString:使用逗号分隔的列表,每个ZooKeeper节点是一个host:port对,host 是机器名或者IP地址, port是ZooKeeper节点对客户端提供服务的端口号,客户端会任意选取connectString中的一个节点建立连接。
  • sessionTimeout : 会话超时时间(Zookeeper客户端和服务端之间是长连接,通过心跳保持长连接,如果超过sessionTimeout设置的时间,相互之间没有任何心跳、通讯,那这个长连接就认为是断掉了)。
  • watcher:用于接收到来自ZooKeeper集群的事件。
使用 zookeeper原生 API,连接zookeeper集群
public class ZookeeperClientFactory {

    private static final int SESSION_TIMEOUT = 5000;

    public static ZooKeeper getZKClient(String connetString) throws Exception {
        //会话连接是异步的,这里用的CountDownLatch进行阻塞,待建立连接后释放
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        ZooKeeper zooKeeper = new ZooKeeper(connetString, SESSION_TIMEOUT, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                if (event.getType() == Event.EventType.None
                        && event.getState() == Watcher.Event.KeeperState.SyncConnected) {
                    //如果收到了服务端的响应事件,则代表连接成功
                    countDownLatch.countDown();
                    System.out.println("连接建立");
                } else {
                    System.out.println("连接失败");
                    countDownLatch.countDown();
                }
            }
        });

        System.out.println("等待ZK建立连接...");
        //阻塞,等待建立连接
        countDownLatch.await();

        return zooKeeper;
    }

}

同步创建节点

@Test
public void testCreate() throws KeeperException, InterruptedException {
    ZooKeeper zooKeeper = getZooKeeper();
    String path = zooKeeper.create("/test", "test".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}

修改节点数据

@Test
public void testSetData() throws KeeperException, InterruptedException {
      ZooKeeper zooKeeper = getZooKeeper();

      Stat stat = new Stat();
      byte[] data = zooKeeper.getData("/test", false, stat);

      int version = stat.getVersion();
      zooKeeper.setData("/test", "test01".getBytes(), version);
      // -1: 无条件更新
      //zooKeeper.setData(first_node, "third".getBytes(), -1);
}

异步创建节点:

@Test
public void createAsycTest() throws InterruptedException {
    getZooKeeper().create(first_node, "test001".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,
             CreateMode.PERSISTENT,(rc, path, ctx, name) -> {},"context");
    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}

Zookeeper主要方法

  • create(path, data, acl,createMode): 创建一个给定路径的 znode,并在 znode 保存 data[]的 数据,createMode指定 znode 的类型。
  • delete(path, version):如果给定 path 上的 znode 的版本和给定的 version 匹配, 删除 znode。
  • exists(path, watch):判断给定 path 上的 znode 是否存在,并在 znode 设置一个 watch。
  • getData(path, watch,stat):返回给定 path 上的 znode 数据,并在 znode 设置一个 watch。
  • setData(path, data, version):如果给定 path 上的 znode 的版本和给定的 version 匹配,设置 znode 数据。
  • getChildren(path, watch):返回给定 path 上的 znode 的孩子 znode 名字,并在 znode 设置一个 watch。
  • sync(path):把客户端 session 连接节点和 leader 节点进行同步。

方法特点:

  • 所有获取 znode 数据的 API 都可以设置一个 watch 用来监控 znode 的变化。
  • 所有更新 znode 数据的 API 都有两个版本: 无条件更新版本和条件更新版本。如果 version 为 -1,更新为无条件更新。否则只有给定的 version 和 znode 当前的 version 一样,才会进行更新,这样的更新是条件更新。
  • 所有的方法都有同步和异步两个版本。同步版本的方法发送请求给 ZooKeeper 并等待服务器的响应。异步版本把请求放入客户端的请求队列,然后马上返回。异步版本通过 callback 来接受来自服务端的响应。

1.2 Curator开源客户端使用

Curator是Netflix公司开源的一套ZooKeeper客户端框架,和ZkClient一样它解决了非常底层的细节开发工作,包括连接、重连、反复注册Watcher的问题以及NodeExistsException异常等。

Curator是Apache基金会的顶级项目之一,Curator具有更加完善的文档,另外还提供了一套易用性和可读性更强的Fluent风格的客户端API框架。

Curator还为ZooKeeper客户端框架提供了一些比较普遍的、开箱即用的、分布式开发用的解决方案,例如Recipe、共享锁服务、Master选举机制和分布式计算器等,帮助开发者避免了“重复造轮子”的无效开发工作。

Guava is to Java that Curator to ZooKeeper

在实际的开发场景中,使用Curator客户端就足以应付日常的ZooKeeper集群操作的需求。

官网:Apache Curator

引入依赖,Curator 包含了几个包:

  • curator-framework是对ZooKeeper的底层API的一些封装。
  • curator-client提供了一些客户端的操作,例如重试策略等。
  • curator-recipes封装了一些高级特性,如:Cache事件监听、选举、分布式锁、分布式计数器、分布式Barrier等。
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.0</version>
</dependency>

<!--curator-->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.1.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
        </exclusion>
    </exclusions>
</dependency>

创建一个客户端实例

在使用curator-framework包操作ZooKeeper前,首先要创建一个客户端实例。这是一个CuratorFramework类型的对象,有两种方法:

  • 使用工厂类CuratorFrameworkFactory的静态newClient()方法;
private final static String CLUSTER_CONNECT_STR = "192.168.65.11:2181,192.168.65.12:2181,192.168.65.13:2181";

//重试策略
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//创建客户端实例
CuratorFramework zkClient = CuratorFrameworkFactory.newClient(CLUSTER_CONNECT_STR, retryPolicy);
//启动客户端
zkClient.start();
  • 使用工厂类CuratorFrameworkFactory的静态builder构造者方法。
private final static String CLUSTER_CONNECT_STR = "192.168.65.11:2181,192.168.65.12:2181,192.168.65.13:2181";

//随着重试次数增加重试时间间隔变大,指数倍增长baseSleepTimeMs * Math.max(1, random.nextInt(1 << (retryCount + 1)))
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString(CLUSTER_CONNECT_STR)
                .sessionTimeoutMs(5000)  // 会话超时时间
                .connectionTimeoutMs(5000) // 连接超时时间
                .retryPolicy(retryPolicy)
                .namespace("base") // 包含隔离名称
                .build();
client.start();
  • connectionString:服务器地址列表,在指定服务器地址列表的时候可以是一个地址,也可以是多个地址。如果是多个地址,那么每个服务器地址列表用逗号分隔, 如 host1:port1,host2:port2,host3;port3 。
  • retryPolicy:重试策略,当客户端异常退出或者与服务端失去连接的时候,可以通过设置客户端重新连接 ZooKeeper 服务端。而 Curator 提供了 一次重试、多次重试等不同种类的实现方式。在 Curator 内部,可以通过判断服务器返回的 keeperException 的状态代码来判断是否进行重试处理,如果返回的是 OK 表示一切操作都没有问题,而 SYSTEMERROR 表示系统或服务端错误。

策略名称

描述

ExponentialBackoffRetry

重试一组次数,重试之间的睡眠时间增加

RetryNTimes

重试最大次数

RetryOneTime

只重试一次

RetryUntilElapsed

在给定的时间结束之前重试

  • 超时时间:Curator 客户端创建过程中,有两个超时时间的设置。一个是 sessionTimeoutMs 会话超时时间,用来设置该条会话在 ZooKeeper 服务端的失效时间。另一个是 connectionTimeoutMs 客户端创建会话的超时时间,用来限制客户端发起一个会话连接到接收 ZooKeeper 服务端应答的时间。sessionTimeoutMs 作用在服务端,而 connectionTimeoutMs 作用在客户端。

创建节点

描述一个节点要包括节点的类型,即临时节点还是持久节点、节点的数据信息、节点是否是有序节点等属性和性质。创建节点的方式如下面的代码所示

@Test
public void testCreate() throws Exception {

  CuratorFramework curatorFramework = getCuratorFramework();
  String forPath = curatorFramework
          .create()
          // protection 模式,防止由于异常原因,导致僵尸节点
          .withProtection()
          .withMode(CreateMode.PERSISTENT).forPath("/curator-node", "test".getBytes());

}

在 Curator 中,可以使用 create 函数创建数据节点,并通过 withMode 函数指定节点类型(持久化节点,临时节点,顺序节点,临时顺序节点,持久化顺序节点等),默认是持久化节点,之后调用 forPath 函数来指定节点的路径和数据信息。

一次性创建带层级结构的节点

// 递归创建子节点
@Test
public void testCreateWithParent() throws Exception {
    CuratorFramework curatorFramework = getCuratorFramework();

    String pathWithParent = "/parent-node/sub-node-1";
    String path = curatorFramework.create().creatingParentsIfNeeded().forPath(pathWithParent);
 }

获取数据

 @Test
 public void testGetData() throws Exception {
    CuratorFramework curatorFramework = getCuratorFramework();
    byte[] bytes = curatorFramework.getData().forPath("/curator-node");
 }

更新节点

我们通过客户端实例的 setData() 方法更新 ZooKeeper 服务上的数据节点,在setData 方法的后边,通过 forPath 函数来指定更新的数据节点路径以及要更新的数据。

@Test
public void testSetData() throws Exception {
   CuratorFramework curatorFramework = getCuratorFramework();
   curatorFramework.setData().forPath("/curator-node", "testChanged!".getBytes());
   byte[] bytes = curatorFramework.getData().forPath("/curator-node");

}

删除节点

@Test
public void testDelete() throws Exception {
   CuratorFramework curatorFramework = getCuratorFramework();
   String pathWithParent = "/parent-node";
        curatorFramework.delete().guaranteed().deletingChildrenIfNeeded().forPath(pathWithParent);
}

guaranteed:该函数的功能如字面意思一样,主要起到一个保障删除成功的作用,其底层工作方式是:只要该客户端的会话有效,就会在后台持续发起删除请求,直到该数据节点在 ZooKeeper 服务端被删除。

deletingChildrenIfNeeded:指定了该函数后,系统在删除该数据节点的时候会以递归的方式直接删除其子节点,以及子节点的子节点。

异步接口

Curator 引入了BackgroundCallback 接口,用来处理服务器端返回来的信息,这个处理过程是在异步线程中调用,默认在 EventThread中调用,也可以自定义线程池。

public interface BackgroundCallback
{
    /**
     * Called when the async background operation completes
     *
     * @param client the client
     * @param event operation result details
     * @throws Exception errors
     */
    public void processResult(CuratorFramework client, CuratorEvent event) throws Exception;
}

如上接口,主要参数为 client 客户端和服务端事件event,inBackground 异步处理默认在EventThread中执行

@Test
public void testBackground() throws Exception {
    CuratorFramework curatorFramework = getCuratorFramework();
//  curatorFramework.getData().inBackground(new BackgroundCallback() {
//  @Override
//  public void processResult(CuratorFramework client, CuratorEvent event) throws Exception 
    {
    }
//    });
    curatorFramework.getData().inBackground((item1, item2) -> {
         log.info("event: {}", item2);
     }).forPath("/test");

    TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}

指定线程池

 @Test
 public void testThreadPool() throws Exception {

    CuratorFramework curatorFramework = getCuratorFramework();
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    String ZK_NODE="/myNode";
    curatorFramework.getData().inBackground((client, event) -> {
            log.info("event: {}", event);
        },executorService).forPath(ZK_NODE);

 }

Curator 监听器

/**
 * Receives notifications about errors and background events
 */
public interface CuratorListener
{
    /**
     * Called when a background task has completed or a watch has triggered
     *
     * @param client client
     * @param event the event
     * @throws Exception any errors
     */
    public void         eventReceived(CuratorFramework client, CuratorEvent event) throws Exception;
}

针对 background 通知和错误通知。使用此监听器之后,调用inBackground 方法会异步获得监听

Curator Caches:

Curator 引入了 Cache 来实现对 Zookeeper服务端事件监听,Cache 事件监听可以理解为一个本地缓存视图与远程 Zookeeper 视图的对比过程,Cache 提供了反复注册的功能。Cache 分为两类注册类型:节点监听和子节点监听。

node cache:NodeCache 对某一个节点进行监听

public NodeCache(CuratorFramework client,String path);
// 创建节点缓存,用于监听指定节点的变化
NodeCache nodeCache = new NodeCache(zkClient, path);
// 启动NodeCache并立即从服务端获取最新数据
nodeCache.start(true);

可以通过注册监听器来实现,对当前节点数据变化的处理

public void addListener(NodeCacheListener listener);
// 注册节点变化监听器
 nodeCache.getListenable().addListener(new NodeCacheListener() {
    @Override
    public void nodeChanged() throws Exception {
        byte[] newData = nodeCache.getCurrentData().getData();
        System.out.println("Node data changed: " + new String(newData));
        }
 });

2. Zookeeper在分布式命名服务中的应用

命名服务是为系统中的资源提供标识能力。ZooKeeper的命名服务主要是利用ZooKeeper节点的树形分层结构和子节点的顺序维护能力,来为分布式系统中的资源命名。

哪些应用场景需要用到分布式命名服务呢?典型的有:

  • 分布式API目录
  • 分布式节点命名
  • 分布式ID生成器

2.1 分布式API目录

为分布式系统中各种API接口服务的名称、链接地址,提供类似JNDI(Java命名和目录接口)中的文件系统的功能。借助于ZooKeeper的树形分层结构就能提供分布式的API调用功能。

著名的Dubbo分布式框架就是应用了ZooKeeper的分布式的JNDI功能。在Dubbo中,使用ZooKeeper维护的全局服务接口API的地址列表。大致的思路为:

  • 服务提供者(Service Provider)在启动的时候,向ZooKeeper上的指定节点/dubbo/${serviceName}/providers写入自己的API地址,这个操作就相当于服务的公开。
  • 服务消费者(Consumer)启动的时候,订阅节点/dubbo/{serviceName}/providers下的服务提供者的URL地址,获得所有服务提供者的API。

2.2 分布式节点的命名

一个分布式系统通常会由很多的节点组成,节点的数量不是固定的,而是不断动态变化的。比如说,当业务不断膨胀和流量洪峰到来时,大量的节点可能会动态加入到集群中。而一旦流量洪峰过去了,就需要下线大量的节点。再比如说,由于机器或者网络的原因,一些节点会主动离开集群。

如何为大量的动态节点命名呢?一种简单的办法是可以通过配置文件,手动为每一个节点命名。但是,如果节点数据量太大,或者说变动频繁,手动命名则是不现实的,这就需要用到分布式节点的命名服务。

可用于生成集群节点的编号的方案:

(1)使用数据库的自增ID特性,用数据表存储机器的MAC地址或者IP来维护。

(2)使用ZooKeeper持久顺序节点的顺序特性来维护节点的NodeId编号。

在第2种方案中,集群节点命名服务的基本流程是:

  • 启动节点服务,连接ZooKeeper,检查命名服务根节点是否存在,如果不存在,就创建系统的根节点。
  • 在根节点下创建一个临时顺序ZNode节点,取回ZNode的编号把它作为分布式系统中节点的NODEID。
  • 如果临时节点太多,可以根据需要删除临时顺序ZNode节点。

2.3 分布式的ID生成器

在分布式系统中,分布式ID生成器的使用场景非常之多:

  • 大量的数据记录,需要分布式ID。
  • 大量的系统消息,需要分布式ID。
  • 大量的请求日志,如restful的操作记录,需要唯一标识,以便进行后续的用户行为分析和调用链路分析。
  • 分布式节点的命名服务,往往也需要分布式ID。
  • 。。。

传统的数据库自增主键已经不能满足需求。在分布式系统环境中,迫切需要一种全新的唯一ID系统,这种系统需要满足以下需求:

(1)全局唯一:不能出现重复ID。

(2)高可用:ID生成系统是基础系统,被许多关键系统调用,一旦宕机,就会造成严重影响。

有哪些分布式的ID生成器方案呢?大致如下:

  1. Java的UUID。
  2. 分布式缓存Redis生成ID:利用Redis的原子操作INCR和INCRBY,生成全局唯一的ID。
  3. Twitter的SnowFlake算法。
  4. ZooKeeper生成ID:利用ZooKeeper的顺序节点,生成全局唯一的ID。
  5. MongoDb的ObjectId:MongoDB是一个分布式的非结构化NoSQL数据库,每插入一条记录会自动生成全局唯一的一个“_id”字段值,它是一个12字节的字符串,可以作为分布式系统中全局唯一的ID。

基于Zookeeper实现分布式ID生成器

在ZooKeeper节点的四种类型中,其中有以下两种类型具备自动编号的能力

  • PERSISTENT_SEQUENTIAL持久化顺序节点。
  • EPHEMERAL_SEQUENTIAL临时顺序节点。

ZooKeeper的每一个节点都会为它的第一级子节点维护一份顺序编号,会记录每个子节点创建的先后顺序,这个顺序编号是分布式同步的,也是全局唯一的。

可以通过创建ZooKeeper的临时顺序节点的方法,生成全局唯一的ID

Zookeeper的写操作是具有线性一致性的,所有的写请求都会转到leader节点,而且必须是数据写成功commit之后,才会执行下一个写操作。每个操作具备原子性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值