天天看点

ZooKeeper : Curator框架namespace原理分析

ZooKeeper : Curator框架namespace原理分析

在上一篇博客中只是简单提及到了​

​namespace​

​​,并没有详细介绍​

​namespace​

​​,本篇博客,博主给大家详细介绍​

​Curator​

​​框架中的​

​namespace​

​。

  • ​​ZooKeeper : Curator框架重试策略和Session API介绍​​

博主使用的​

​Curator​

​​框架版本是​

​5.2.0​

​​,​

​ZooKeeper​

​​版本是​

​3.6.3​

​。

<dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>      
ZooKeeper : Curator框架namespace原理分析

​5.2.0​

​​版本的​

​Curator​

​​使用​

​3.6.3​

​​版本的​

​ZooKeeper​

​。

ZooKeeper : Curator框架namespace原理分析

namespace节点类型

​Curator​

​​框架中的命名空间对应到​

​ZooKeeper​

​中就是一个节点,而这个命名空间节点的节点类型是什么呢?

ZooKeeper : Curator框架namespace原理分析

首先不可能是临时节点,因为临时节点不能创建子节点,这种情况肯定不能满足业务需求。

ZooKeeper : Curator框架namespace原理分析

使用​

​TTL Znode​

​​需要配置​

​extendedTypesEnabled=true​

​​,不然创建​

​TTL Znode​

​​时会收到​

​Unimplemented​

​的报错,在​​重要概念​​这篇博客中进行了介绍。所以命名空间节点肯定不会是临时节点。

也不可能是持久节点,因为当客户端关闭一段时间后,该命名空间节点就被移除了,这显然不是持久节点。那就只剩下​

​TTL​

​​节点和容器节点这两种类型。其实也不可能是​

​TTL​

​​节点,因为​

​ZooKeeper​

​​服务端并不能创建​

​TTL​

​​节点(没有添加​

​extendedTypesEnabled=true​

​这个配置),所以命名空间这个节点的默认类型是容器节点。

由于​

​Curator​

​​框架是基于​

​ZooKeeper​

​​的​

​Java​

​​客户端原生​

​API​

​​来实现更高级、更易用的​

​API​

​​,所以在创建命名空间这个节点时,还是会调用​

​ZooKeeper​

​​类的​

​create​

​​方法(由​

​ZooKeeper​

​​的​

​Java​

​​客户端提供),因此通过​

​Debug​

​​就可以知道命名空间节点的类型了。先在​

​ZooKeeper​

​​类的​

​create​

​​方法打上​

​Debug​

​标记,如下图所示:

ZooKeeper : Curator框架namespace原理分析

测试代码:

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
 * @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​

​方法表示会检查节点是否存在,如果存在就返回该节点的状态信息。如下图所示,命名空间节点默认是容器节点类型。

ZooKeeper : Curator框架namespace原理分析

​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
 * @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​

​。如下图所示,命名空间节点默认还是容器节点类型。

ZooKeeper : Curator框架namespace原理分析

而​

​/curator/kaven​

​​节点是持久节点类型,这是调用​

​creatingParentsIfNeeded​

​方法的结果。

ZooKeeper : Curator框架namespace原理分析

​/curator/kaven/docker​

​​是临时节点类型,这是通过​

​withMode(CreateMode.EPHEMERAL)​

​直接指定的。

ZooKeeper : Curator框架namespace原理分析

因此,可以知道命名空间节点默认是容器节点类型。

如果想将命名空间节点设置成​

​/curator/namespace​

​​这种形式,即更深层的节点,可以如下所示进行定义(以此类推,不需要加​

​/​

​前缀):

namespace("curator/namespace")      

如果加​

​/​

​前缀会报错:

ZooKeeper : Curator框架namespace原理分析

并且这些节点都将以容器节点类型被创建(都不存在的情况下)。

ZooKeeper : Curator框架namespace原理分析
ZooKeeper : Curator框架namespace原理分析

如果只是部分节点存在,不会覆盖存在的节点,只会创建不存在的节点,还是以容器节点类型进行创建。

ZooKeeper : Curator框架namespace原理分析
ZooKeeper : Curator框架namespace原理分析

这些只是通过​

​Debug​

​​得到的结论,可能存在偶然情况,接下来博主通过分析​

​Curator​

​框架的源码来验证上述的结论。

源码分析

问题

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

带着这两个问题博主来分析一下​

​Curator​

​​框架的相关源码。命名空间节点什么时候被创建的?其实是在​

​Curator​

​​框架第一次对​

​ZooKeeper​

​​服务端进行操作的时候,​

​Curator​

​​框架每次操作都会指定一个路径(需要知道操作哪个节点),通过​

​forPath​

​​方法来指定,而这个路径是相对于命名空间而言,因此命名空间节点必须提前被创建。在每个操作的实现类中的​

​forPath​

​​方法都会调用​

​CuratorFrameworkImpl​

​​类中的​

​fixForNamespace​

​方法。

如下图所示(以​

​CreateBuilderImpl​

​类为例):

ZooKeeper : Curator框架namespace原理分析

调用​

​CuratorFrameworkImpl​

​​类中的​

​fixForNamespace​

​方法:

ZooKeeper : Curator框架namespace原理分析

这些操作最终都会调用​

​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​

​​,就创建该节点,还是通过​

​ZooKeeper​

​​的​

​Java​

​​客户端原生​

​API​

​​来进行创建的,如果节点存在不会覆盖该节点;而节点类型通过​

​getCreateMode​

​​方法获得,而这里的​

​asContainers​

​​参数默认为​

​true​

​​,也再一次说明命名空间节点默认是容器节点;​

​makeLastNode​

​​参数表示是否创建最后一个节点,默认是​

​true​

​​,因为最后一个节点的结尾没有​

​/​

​​字符,因此​

​path.indexOf(47, pos + 1)​

​​的结果是​

​-1​

​​,如果​

​makeLastNode​

​​为​

​true​

​​(​

​pos = path.length()​

​​),​

​subPath​

​​的值就和​

​path​

​​一样,所以会创建最后一个节点,而​

​makeLastNode​

​​为​

​false​

​​,就会通过​

​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);      

总结

  • 命名空间不能加​

    ​/​

    ​​前缀,不然会报错,事实上​

    ​Curator​

    ​​框架会自动加上,并且命名空间可以使用更深层的节点,如​

    ​/a/b/c/d​

    ​​,而对应的命名空间是​

    ​a/b/c/d​

    ​。
  • 命名空间节点在​

    ​Curator​

    ​​框架对​

    ​ZooKeeper​

    ​​服务端进行第一次操作时被创建(指定该操作的路径时被创建,即在调用​

    ​forPath​

    ​方法的时候)。
  • 命名空间节点默认是容器节点(​

    ​Curator​

    ​​框架版本不同,设定可能不一样),但前提是使用的​

    ​ZooKeeper​

    ​​版本支持容器节点,不然命名空间节点将以持久节点类型被创建;如果命名空间表示一个深层的节点,如​

    ​/a/b/c/d​

    ​​, ​

    ​Curator​

    ​​框架只会以默认方式创建​

    ​ZooKeeper​

    ​​服务端中不存在的节点(通过​

    ​do-while​

    ​​循环的方式,依次先创建父节点,再创建子节点,并且默认为容器节点类型,除非使用的​

    ​ZooKeeper​

    ​版本不支持容器节点,就会以持久节点类型创建它们),如果节点存在不会进行覆盖。