写点什么

dubbo 使用 curator 作为 zk 客户端优雅停机存在的问题

用户头像
林一
关注
发布于: 2021 年 06 月 04 日

前言

在上一篇:发布引发的 curator 报错:instance must be started before calling this method在结尾的时候,我提出了解决这个问题可以采用优雅停机来解决,但是在后来的实践中我发现这个问题并没有办法解决,所以又进一步对 curator 代码进行了分析、得出了以下个人的总结与分析(以下内容仅个人理解与观点,如果有误欢迎评论留言指出

为什么我之前会觉得优雅停机可以解决?

  1. 因为这个问题是在停机的时候由于缓存 TreeCache 刷新缓存事件没处理完导致的、我第一时间就想到了是否优雅停机可以解决这个问题

  2. 我在试验优雅停机(操作为休眠 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 方法如下:


@Overridepublic 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 方法是如何处理的:


@Overridepublic 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 开发者在写的时候已经考虑到了这种情况、并且综合考虑报错没啥影响,最优方案也只能那么实现。上面只是我个人对这个问题的思考,如果有误,欢迎评论指出~

用户头像

林一

关注

没有幽默的笔风👉 2020.02.13 加入

还未添加个人简介

评论 (1 条评论)

发布
用户头像
[强]
2021 年 06 月 05 日 15:35
回复
没有更多了
dubbo使用curator作为zk客户端优雅停机存在的问题