ZooKeeper : Curator框架namespace原理分析
ZooKeeper : Curator框架Session API介绍在学习Curator框架API之前,可以先了解Java客户端原生API,这样不仅可以更好的理解Curator框架API,还可以突出Curator框架的方便和强大。ZooKeeper :Java客户端Session、ACL、Znode API介绍ZooKeeper :Java客户端Watcher API介绍ZooKeeper :Ja
ZooKeeper : Curator框架namespace原理分析
在上一篇博客中只是简单提及到了namespace
,并没有详细介绍namespace
,本篇博客,博主给大家详细介绍Curator
框架中的namespace
。
博主使用的Curator
框架版本是5.2.0
,ZooKeeper
版本是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
框架是基于ZooKeeper
的Java
客户端原生API
来实现更高级、更易用的API
,所以在创建命名空间这个节点时,还是会调用ZooKeeper
类的create
方法(由ZooKeeper
的Java
客户端提供),因此通过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
,就创建该节点,还是通过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);
创建好了命名空间节点,关于forPath
方法指定的路径该如何处理?这个问题留到Znode API
的原理分析中再进行介绍。
总结
- 命名空间不能加
/
前缀,不然会报错,事实上Curator
框架会自动加上,并且命名空间可以使用更深层的节点,如/a/b/c/d
,而对应的命名空间是a/b/c/d
。 - 命名空间节点在
Curator
框架对ZooKeeper
服务端进行第一次操作时被创建(指定该操作的路径时被创建,即在调用forPath
方法的时候)。 - 命名空间节点默认是容器节点(
Curator
框架版本不同,设定可能不一样),但前提是使用的ZooKeeper
版本支持容器节点,不然命名空间节点将以持久节点类型被创建;如果命名空间表示一个深层的节点,如/a/b/c/d
,Curator
框架只会以默认方式创建ZooKeeper
服务端中不存在的节点(通过do-while
循环的方式,依次先创建父节点,再创建子节点,并且默认为容器节点类型,除非使用的ZooKeeper
版本不支持容器节点,就会以持久节点类型创建它们),如果节点存在不会进行覆盖。
如果博主有说错的地方或者大家有不同的见解,欢迎大家评论补充。
更多推荐
所有评论(0)