天天看點

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​

    ​版本不支援容器節點,就會以持久節點類型建立它們),如果節點存在不會進行覆寫。