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