zk版本:3.5.6
1. 引入
在3.5.6版本中,有两种节点需要清理:临时节点(会话结束,会被清除)和容器节点(如果没有子节点,会被清理)。
2. 节点清理
清理节点的操作是通过ContainerManager类完成,在单机启动的博客中其他可以看到它的身影。
ZooKeeperServerMain.java
------------------------
public void runFromConfig(ServerConfig config)
throws IOException, AdminServerException {
// 定时清除容器节点
containerManager = new ContainerManager(zkServer.getZKDatabase(), zkServer.firstProcessor,
Integer.getInteger("znode.container.checkIntervalMs", (int) TimeUnit.MINUTES.toMillis(1)),
Integer.getInteger("znode.container.maxPerMinute", 10000)
);
containerManager.start();
containerManager.start就是清理节点的入口:
ContainerManager.java
---------------------
public void start() {
if (task.get() == null) {
// 检查并清除无用的节点task
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
try {
checkContainers();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOG.info("interrupted");
cancel();
} catch ( Throwable e ) {
LOG.error("Error checking containers", e);
}
}
};
// 定时执行
if (task.compareAndSet(null, timerTask)) {
timer.scheduleAtFixedRate(timerTask, checkIntervalMs,
checkIntervalMs);
}
}
}
和日志快照清理差不多,这个也是创建一个定时器,然后根据配置定时执行清理操作,主要逻辑在checkContainers完成。
ContainerManager.java
---------------------
public void checkContainers()
throws InterruptedException {
long minIntervalMs = getMinIntervalMs();
for (String containerPath : getCandidates()) {
long startMs = Time.currentElapsedTime();
ByteBuffer path = ByteBuffer.wrap(containerPath.getBytes());
// 删除节点请求(由于删除普通节点和容器节点的操作是一样的,这里统一使用)
Request request = new Request(null, 0, 0,
ZooDefs.OpCode.deleteContainer, path, null);
try {
containerPath);
// 处理删除节点请求
requestProcessor.processRequest(request);
} catch (Exception e) {
LOG.error("Could not delete container: {}",
containerPath, e);
}
// 等待处理一下个路径
long elapsedMs = Time.currentElapsedTime() - startMs;
long waitMs = minIntervalMs - elapsedMs;
if (waitMs > 0) {
Thread.sleep(waitMs);
}
}
}
主要步骤是:
(1) 找到需要清理的节点
(2) 发起deleteContainer请求,清理节点
(3) 通过通过间隔时间控制执行的周期
我们现在来分析一下是如何找到需要清理的节点。
ContainerManager.java
---------------------
protected Collection<String> getCandidates() {
Set<String> candidates = new HashSet<String>();
// 容器节点中没有子节点
for (String containerPath : zkDb.getDataTree().getContainers()) {
DataNode node = zkDb.getDataTree().getNode(containerPath);
/*
cversion>0 保证已经操作过子节点
*/
if ((node != null) && (node.stat.getCversion() > 0) &&
(node.getChildren().size() == 0)) {
candidates.add(containerPath);
}
}
// 过期节点
for (String ttlPath : zkDb.getDataTree().getTtls()) {
DataNode node = zkDb.getDataTree().getNode(ttlPath);
if (node != null) {
Set<String> children = node.getChildren();
if ((children == null) || (children.size() == 0)) {
if ( EphemeralType.get(node.stat.getEphemeralOwner()) == EphemeralType.TTL ) {
long elapsed = getElapsed(node);
long ttl = EphemeralType.TTL.getValue(node.stat.getEphemeralOwner());
if ((ttl != 0) && (getElapsed(node) > ttl)) {
candidates.add(ttlPath);
}
}
}
}
}
return candidates;
}
通过上面的代码可知,查找的节点类型有:
(1) 容器节点(要求节点的cversion>0,因为cversion表示对此znode的子节点进行的更改次数,即表示拥有过子节点)
(2) 临时节点
3. 触发节点清理的位置
单机启动时会触发节点清理,集群启动方式也会触发清理,它在哪里呢?其实就是在LeaderZooKeeperServer类中:
LeaderZooKeeperServer.java
--------------------------
private synchronized void setupContainerManager() {
containerManager = new ContainerManager(getZKDatabase(), prepRequestProcessor,
Integer.getInteger("znode.container.checkIntervalMs", (int) TimeUnit.MINUTES.toMillis(1)),
Integer.getInteger("znode.container.maxPerMinute", 10000)
);
}
即在leader启动时会触发节点清理,但是在其他角色中没有这个操作的,这是因为节点清理最终是发送deleteContainer请求实现的,只要发送一次就行,集群中的所有zookeeper节点都会执行。
3. 发现与总结
- 虽然类名是ContainerManager看似他只会清除容器节点,其实清理的节点包含容器节点和临时节点两种
- 清理两种节点为什么只是发起了deleteContainer请求,因为服务端对于deleteContainer和delete的处理方式是一样的