ZooKeeper : Curator框架namespace原理分析

在上一篇博客中只是简单提及到了namespace,并没有详细介绍namespace,本篇博客,博主给大家详细介绍Curator框架中的namespace

博主使用的Curator框架版本是5.2.0ZooKeeper版本是3.6.3

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>

在这里插入图片描述
5.2.0版本的Curator使用3.6.3版本的ZooKeeper
在这里插入图片描述

namespace节点类型

Curator框架中的命名空间对应到ZooKeeper中就是一个节点,而这个命名空间节点的节点类型是什么呢?
在这里插入图片描述
首先不可能是临时节点,因为临时节点不能创建子节点,这种情况肯定不能满足业务需求。
在这里插入图片描述
使用TTL Znode需要配置extendedTypesEnabled=true,不然创建TTL Znode时会收到Unimplemented的报错,在重要概念这篇博客中进行了介绍。所以命名空间节点肯定不会是临时节点。

也不可能是持久节点,因为当客户端关闭一段时间后,该命名空间节点就被移除了,这显然不是持久节点。那就只剩下TTL节点和容器节点这两种类型。其实也不可能是TTL节点,因为ZooKeeper服务端并不能创建TTL节点(没有添加extendedTypesEnabled=true这个配置),所以命名空间这个节点的默认类型是容器节点。

由于Curator框架是基于ZooKeeperJava客户端原生API来实现更高级、更易用的API,所以在创建命名空间这个节点时,还是会调用ZooKeeper类的create方法(由ZooKeeperJava客户端提供),因此通过Debug就可以知道命名空间节点的类型了。先在ZooKeeper类的create方法打上Debug标记,如下图所示:
在这里插入图片描述
测试代码:

package com.kaven.zookeeper;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.retry.ExponentialBackoffRetry;

/**
 * @Author: ITKaven
 * @Date: 2021/11/20 10:30
 * @Blog: https://kaven.blog.csdn.net
 * @Leetcode: https://leetcode-cn.com/u/kavenit
 * @Notes:
 */

public class Application{

    private static final String SERVER_PROXY = "192.168.1.184:9000";
    private static final int TIMEOUT = 40000;

    public static void main(String[] args) throws Exception {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework curator = CuratorFrameworkFactory.builder()
                .connectString(SERVER_PROXY)
                .namespace("curator")
                .retryPolicy(retryPolicy)
                .connectionTimeoutMs(TIMEOUT)
                .sessionTimeoutMs(TIMEOUT)
                .build();

        curator.start();
        if (curator.getState().equals(CuratorFrameworkState.STARTED)) {
            System.out.println("连接成功!");
            curator.checkExists()
                    .forPath("/");

        }
        Thread.sleep(10000000);
    }
}

这里先不管Curator框架相关API的使用,checkExists方法表示会检查节点是否存在,如果存在就返回该节点的状态信息。如下图所示,命名空间节点默认是容器节点类型。
在这里插入图片描述
Curator框架对创建节点的API进行了增强,当需要创建的节点的Parents不存在时,会先创建它的Parents

测试代码:

package com.kaven.zookeeper;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.imps.CuratorFrameworkState;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;

/**
 * @Author: ITKaven
 * @Date: 2021/11/20 10:30
 * @Blog: https://kaven.blog.csdn.net
 * @Leetcode: https://leetcode-cn.com/u/kavenit
 * @Notes:
 */

public class Application{

    private static final String SERVER_PROXY = "192.168.1.184:9000";
    private static final int TIMEOUT = 40000;

    public static void main(String[] args) throws Exception {
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework curator = CuratorFrameworkFactory.builder()
                .connectString(SERVER_PROXY)
                .namespace("curator")
                .retryPolicy(retryPolicy)
                .connectionTimeoutMs(TIMEOUT)
                .sessionTimeoutMs(TIMEOUT)
                .build();

        curator.start();
        if (curator.getState().equals(CuratorFrameworkState.STARTED)) {
            System.out.println("连接成功!");
            curator.create()
                    .creatingParentsIfNeeded()
                    .withMode(CreateMode.EPHEMERAL)
                    .forPath("/kaven/docker", "data".getBytes());
        }
        Thread.sleep(10000000);
    }
}

creatingParentsIfNeeded方法表示当需要创建的节点的Parents不存在时,会先创建它的Parents(必须先创建父节点,才能创建子节点),并且以持久节点类型创建这些Parents。如下图所示,命名空间节点默认还是容器节点类型。
在这里插入图片描述
/curator/kaven节点是持久节点类型,这是调用creatingParentsIfNeeded方法的结果。
在这里插入图片描述
/curator/kaven/docker是临时节点类型,这是通过withMode(CreateMode.EPHEMERAL)直接指定的。
在这里插入图片描述
因此,可以知道命名空间节点默认是容器节点类型。

如果想将命名空间节点设置成/curator/namespace这种形式,即更深层的节点,可以如下所示进行定义(以此类推,不需要加/前缀):

namespace("curator/namespace")

如果加/前缀会报错:
在这里插入图片描述

并且这些节点都将以容器节点类型被创建(都不存在的情况下)。
在这里插入图片描述
在这里插入图片描述
如果只是部分节点存在,不会覆盖存在的节点,只会创建不存在的节点,还是以容器节点类型进行创建。
在这里插入图片描述

在这里插入图片描述
这些只是通过Debug得到的结论,可能存在偶然情况,接下来博主通过分析Curator框架的源码来验证上述的结论。

源码分析

问题

  • 命名空间节点什么时候被创建的?
  • 命名空间节点如何创建的?

带着这两个问题博主来分析一下Curator框架的相关源码。命名空间节点什么时候被创建的?其实是在Curator框架第一次对ZooKeeper服务端进行操作的时候,Curator框架每次操作都会指定一个路径(需要知道操作哪个节点),通过forPath方法来指定,而这个路径是相对于命名空间而言,因此命名空间节点必须提前被创建。在每个操作的实现类中的forPath方法都会调用CuratorFrameworkImpl类中的fixForNamespace方法。

如下图所示(以CreateBuilderImpl类为例):在这里插入图片描述
调用CuratorFrameworkImpl类中的fixForNamespace方法:在这里插入图片描述
这些操作最终都会调用CuratorFrameworkImpl类中的fixForNamespace方法:

    String fixForNamespace(String path, boolean isSequential) {
        return this.namespace.fixForNamespace(path, isSequential);
    }

CuratorFrameworkImpl类中的fixForNamespace方法会调用NamespaceImpl类中的fixForNamespace方法:

    String fixForNamespace(String path, boolean isSequential) {
        if (this.ensurePathNeeded.get()) {
            try {
                final CuratorZookeeperClient zookeeperClient = this.client.getZookeeperClient();
                RetryLoop.callWithRetry(zookeeperClient, new Callable<Object>() {
                    public Object call() throws Exception {
                        ZKPaths.mkdirs(zookeeperClient.getZooKeeper(), ZKPaths.makePath("/", NamespaceImpl.this.namespace), true, NamespaceImpl.this.client.getAclProvider(), true);
                        return null;
                    }
                });
                this.ensurePathNeeded.set(false);
            } catch (Exception var4) {
                ThreadUtils.checkInterrupted(var4);
                this.client.logError("Ensure path threw exception", var4);
            }
        }

        return ZKPaths.fixForNamespace(this.namespace, path, isSequential);
    }

ensurePathNeeded属性是AtomicBoolean类型(保证操作的原子性),表示命名空间这个节点是否需要被创建,只要namespace != null,该属性的值就为true(如果为null,即没有指定namespace或者直接指定为null,即namespace(null),这样命名空间就是ZooKeeper中的根节点/,前缀/拼接null,还是/),即需要被创建。

    NamespaceImpl(CuratorFrameworkImpl client, String namespace)
    {
        if ( namespace != null )
        {
            try
            {
                PathUtils.validatePath("/" + namespace);
            }
            catch ( IllegalArgumentException e )
            {
                throw new IllegalArgumentException("Invalid namespace: " + namespace + ", " + e.getMessage());
            }
        }

        this.client = client;
        this.namespace = namespace;
        ensurePathNeeded = new AtomicBoolean(namespace != null);
    }

下面这一行是关键:

 ZKPaths.mkdirs(zookeeperClient.getZooKeeper(), ZKPaths.makePath("/", NamespaceImpl.this.namespace), true, NamespaceImpl.this.client.getAclProvider(), true);

调用了ZKPaths类中的mkdirs方法,并且最后一个参数的值为true,而最后一个参数是asContainers,很显然命名空间节点默认是容器节点。

    public static void mkdirs(ZooKeeper zookeeper, String path, boolean makeLastNode, InternalACLProvider aclProvider, boolean asContainers) throws InterruptedException, KeeperException {
        PathUtils.validatePath(path);
        int pos = 1;

        do {
            pos = path.indexOf(47, pos + 1);
            if (pos == -1) {
                if (!makeLastNode) {
                    break;
                }

                pos = path.length();
            }

            String subPath = path.substring(0, pos);
            if (zookeeper.exists(subPath, false) == null) {
                try {
                    List<ACL> acl = null;
                    if (aclProvider != null) {
                        acl = aclProvider.getAclForPath(subPath);
                        if (acl == null) {
                            acl = aclProvider.getDefaultAcl();
                        }
                    }

                    if (acl == null) {
                        acl = Ids.OPEN_ACL_UNSAFE;
                    }

                    zookeeper.create(subPath, new byte[0], (List)acl, getCreateMode(asContainers));
                } catch (NodeExistsException var8) {
                }
            }
        } while(pos < path.length());
    }

这里的path参数已经将命名空间加了/前缀,通过调用makePath方法实现。

    public static String makePath(String parent, String child) {
        int maxPathLength = nullableStringLength(parent) + nullableStringLength(child) + 2;
        StringBuilder path = new StringBuilder(maxPathLength);
        // 给定一个父节点和一个子节点,将它们加入给定的path
        joinPath(path, parent, child);
        return path.toString();
    }

简化ZKPaths类中的mkdirs方法如下:

        int pos = 1;
        do {
            pos = path.indexOf(47, pos + 1);
            if (pos == -1) {
                if (!makeLastNode) {
                    break;
                }
                pos = path.length();
            }
            String subPath = path.substring(0, pos);
            if (zookeeper.exists(subPath, false) == null) {
                try {
                    zookeeper.create(subPath, new byte[0], (List)acl, getCreateMode(asContainers));
                } catch (NodeExistsException var8) {
                }
            }
        } while(pos < path.length());

47是字符/int值,pos代表当前已经创建好的节点的后一个位置(默认值为1,代表/已经被创建好了,因为这是ZooKeeper内置的根节点,而它的后一个位置就是1),因此path.indexOf(47, pos + 1)就是查询当前已经创建好的节点的后一个位置后面出现字符/的第一个位置,subPath变量就是当前需要创建的节点的路径, 通过path.substring(0, pos)得到,然后检查该节点是否存在(zookeeper.exists(subPath, false)false表示不在该节点上留下Watcher),如果不存在,即返回null,就创建该节点,还是通过ZooKeeperJava客户端原生API来进行创建的,如果节点存在不会覆盖该节点;而节点类型通过getCreateMode方法获得,而这里的asContainers参数默认为true,也再一次说明命名空间节点默认是容器节点;makeLastNode参数表示是否创建最后一个节点,默认是true,因为最后一个节点的结尾没有/字符,因此path.indexOf(47, pos + 1)的结果是-1,如果makeLastNodetruepos = path.length()),subPath的值就和path一样,所以会创建最后一个节点,而makeLastNodefalse,就会通过break跳出do-while循环;该方法以do-while循环的形式将命名空间节点及其不存在的父节点全部创建(依次先创建父节点,再创建子节点)。

    private static CreateMode getCreateMode(boolean asContainers)
    {
        return asContainers ? getContainerCreateMode() : CreateMode.PERSISTENT;
    }
    public static CreateMode getContainerCreateMode()
    {
        return CreateModeHolder.containerCreateMode;
    }

命名空间节点一定是容器节点吗?答案是不一定,前提是使用的ZooKeeper版本支持容器节点,不然命名空间节点将是持久节点。

    private static final CreateMode NON_CONTAINER_MODE = CreateMode.PERSISTENT;
    private static class CreateModeHolder
    {
        private static final Logger log = LoggerFactory.getLogger(ZKPaths.class);
        private static final CreateMode containerCreateMode;

        static
        {
            CreateMode localCreateMode;
            try
            {
                localCreateMode = CreateMode.valueOf("CONTAINER");
            }
            catch ( IllegalArgumentException ignore )
            {
                localCreateMode = NON_CONTAINER_MODE;
                log.warn("The version of ZooKeeper being used doesn't support Container nodes. CreateMode.PERSISTENT will be used instead.");
            }
            containerCreateMode = localCreateMode;
        }
    }

The version of ZooKeeper being used doesn’t support Container nodes. CreateMode.PERSISTENT will be used instead.
正在使用的ZooKeeper版本不支持容器节点。将改用CreateMode.PERSISTENT。

创建命名空间节点成功后ensurePathNeeded的值会被设置为false,这样以后的操作就不会再次创建命名空间节点了。

this.ensurePathNeeded.set(false);

创建好了命名空间节点,关于forPath方法指定的路径该如何处理?这个问题留到Znode API的原理分析中再进行介绍。

总结

  • 命名空间不能加/前缀,不然会报错,事实上Curator框架会自动加上,并且命名空间可以使用更深层的节点,如/a/b/c/d,而对应的命名空间是a/b/c/d
  • 命名空间节点在Curator框架对ZooKeeper服务端进行第一次操作时被创建(指定该操作的路径时被创建,即在调用forPath方法的时候)。
  • 命名空间节点默认是容器节点(Curator框架版本不同,设定可能不一样),但前提是使用的ZooKeeper版本支持容器节点,不然命名空间节点将以持久节点类型被创建;如果命名空间表示一个深层的节点,如/a/b/c/dCurator框架只会以默认方式创建ZooKeeper服务端中不存在的节点(通过do-while循环的方式,依次先创建父节点,再创建子节点,并且默认为容器节点类型,除非使用的ZooKeeper版本不支持容器节点,就会以持久节点类型创建它们),如果节点存在不会进行覆盖。

如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐