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>

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