前言
在上一篇:发布引发的 curator 报错:instance must be started before calling this method在结尾的时候,我提出了解决这个问题可以采用优雅停机来解决,但是在后来的实践中我发现这个问题并没有办法解决,所以又进一步对 curator 代码进行了分析、得出了以下个人的总结与分析(以下内容仅个人理解与观点,如果有误欢迎评论留言指出)
为什么我之前会觉得优雅停机可以解决?
因为这个问题是在停机的时候由于缓存 TreeCache 刷新缓存事件没处理完导致的、我第一时间就想到了是否优雅停机可以解决这个问题
我在试验优雅停机(操作为休眠 20s)的时候确实报错消失了、但是后来排查到原因是因为:在 20s 期间所有节点变更事件都被处理完了,所以报错消失了、并不是解决了问题
优雅停机核心实现代码如下:
@Configuration
@Order(2147483646) //DubboBootstrapApplicationListener中LOWEST_PRECEDENCE = 2147483647,这里相当于执行顺序在dubbo关闭之前
public class GracefullyShutdownListener implements ApplicationListener<ContextClosedEvent> {
public GracefullyShutdownListener() {
DubboBootstrap.getInstance().unRegisterShutdownHook();
}
public void onApplicationEvent(ContextClosedEvent event) {
//指定休眠时间默认20秒
long timeSleepInMills = Long.parseLong(System.getProperty("dubbo.shutdown.sleepInMills", "20000"));
try {
Thread.sleep(timeSleepInMills);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
上一节中制造 zk 数据变更的代码如下:
public static void main(String[] args) throws Exception {
//zk 地址
String connectString = "localhost:2181";
// 连接时间 和重试次数
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
client.start();
String path = "/dubbo/config/mapping/test";
//注意这里的i如果数值较小在sleep20期间变更全部被处理完了此时就不会报错
for (int i = 0; i < 1000; i++) {
client.create().creatingParentsIfNeeded().forPath(path + i, "init!".getBytes());
// client.delete().deletingChildrenIfNeeded().forPath(path + i);
}
}
复制代码
通过上面代码来分析,我上次说优雅停机可以解决报错的原因就是因为我在 20s 之内处理完了 1000 个节点创建的事件。但是如果调大数值,让它在 20s 之内处理不完、那就无法解决报错。对应于生产情况来分析就是:如果在真正执行停机的时候,此时还有对应的 zk 事件需要处理,此时 curator 客户端就会报错。
这个问题是否可以解决?
这个目前在我这边得出的结论是无法解决,但是也没啥影响。(如果我这里讲的有误、有解决方案希望可以留言告诉我)下面通过源码分析来说明为啥是无法解决的。
org.apache.zookeeper.ClientCnxn.EventThread#run 方法如下:
@Override
public void run() {
try {
isRunning = true;
while (true) {
//waitingEvents是存放zk事件的队列
Object event = waitingEvents.take();
if (event == eventOfDeath) {
wasKilled = true;
} else {
//处理事件核心方法
processEvent(event);
}
if (wasKilled)
//就算已经接收到eventOfDeath事件也要继续把队列处理完了
//按到执行顺序接收到waitingEvents事件的时候curator已经关闭了所有这里也会导致报错
synchronized (waitingEvents) {
if (waitingEvents.isEmpty()) {
isRunning = false;
break;
}
}
}
} catch (InterruptedException e) {
LOG.error("Event thread exiting due to interruption", e);
}
LOG.info("EventThread shut down for session: 0x{}",
Long.toHexString(getSessionId()));
}
复制代码
根据代码可知 zookeeper 中有一个 while(true)死循环来不断处理 zk 的数据变更,停止条件为接收到 eventOfDeath。再来看看 curator 客户端是何时被 close 的。断点调试 org.apache.curator.framework.imps.CuratorFrameworkImpl#close 方法得到如下调用栈:
这里要说明的是在 CuratorFrameworkImpl 的 close 方法执行之前没有方法往上面 zookeeper 的 waitingEvents 中加入 eventOfDeath 事件,所以此时 EventThread 的 run 方法还是在后台持续执行的。再来看看 CuratorFrameworkImpl 的 close 方法是如何处理的:
@Override
public void close()
{
log.debug("Closing");
//先设置Curator客户端状态为关闭
if ( state.compareAndSet(CuratorFrameworkState.STARTED, CuratorFrameworkState.STOPPED) )
{
//此处省略无关代码。。。
listeners.clear();
unhandledErrorListeners.clear();
connectionStateManager.close();
//这里执行zookeeper的关闭,也就是里面会发送eventOfDeath事件
client.close();
namespaceWatcherMap.close();
}
}
复制代码
下面进入上面代码的 client.close();方法找到发送 eventOfDeath 的地方在 org.apache.zookeeper.ClientCnxn 的 disconnect 方法,此时调用栈如下:
通过调用链以及代码可以看到 curator 是先把自己的 CuratorFrameworkState 设置为 STOPPED 然后 zookeeper 才发送 eventOfDeath,那么在这个过程中就会存在时间差,此时 org.apache.zookeeper.ClientCnxn.EventThread#run 方法在处理事件的时候由于 CuratorFrameworkState 的状态为 STOPPED 就会报错,代码如下:
@Overridepublic GetChildrenBuilder getChildren(){ Preconditions.checkState(getState() == CuratorFrameworkState.STARTED, "instance must be started before calling this method");
return new GetChildrenBuilderImpl(this);}
复制代码
所以按照代码的意思,这个报错就是无法避免的,可能是 curator 客户端实现不够优雅导致的,也不能算是一个 bug。理由如下:
不能够先关闭 zookeeper 链接然后再关闭 curator 客户端吗?
按我的理解不能,因为你作为 zk 客户端,自己状态肯定要先改成 stop 表明要停止服务,此时才能关闭 zk 链接,肯定不能先关闭 zk 链接再把自己状态设置为 stop,因为如果你先把 zk 链接关闭了,但是自身状态还是为 STARTED,此时如果还有请求还是认为你处于可用状态,发起 zk 调用,但是由于 zk 关闭,所以照常会报错。并且关闭 zk 链接与关闭 zk 客户端无法是一个原子操作
这个报错会有啥影响吗?
这个报错没有啥影响,TreeCache 内部实现中会捕获此类异常,然后仅仅是打印日志而已,并且更重要的是,TreeCache 顾名思义只是一个类似缓存的东西,此时服务也准备关闭了,TreeCache 的内容已经不重要了,重启的时候又会刷新一次
总结
这个问题按自己的理解目前并没有啥有效的解决方案,网上能查到的相关知识也寥寥无几,并且 curator 只是 zookeeper 的客户端,EventThread#run 是在 zookeeper 包里的是 zk 的实现并不属于 curator 包的实现所以在不改 zk 实现的情况下目前我也想不出其它更好的实现了,可能 curator 开发者在写的时候已经考虑到了这种情况、并且综合考虑报错没啥影响,最优方案也只能那么实现。上面只是我个人对这个问题的思考,如果有误,欢迎评论指出~
评论 (1 条评论)