写点什么

发布引发的 curator 报错:instance must be started before calling this method

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

问题描述

目前公司项目 RPC 框架采用的是阿里开源的 dubbo,zk 客户端使用的是 Apache 的 Curator,项目发布的方式采用的是发布计划发布,一次发布多个微服务。在一次项目发布过程中发现部分项目出现如下报错:

问题定位

根据报错定位到最近的报错代码为:


@Overridepublic GetChildrenBuilder getChildren(){    Preconditions.checkState(getState() == CuratorFrameworkState.STARTED, "instance must be started before calling this method");
return new GetChildrenBuilderImpl(this);}
复制代码


根据代码可以看出,报错的原因是因为:执行 getChildren()方法的时候,Curator 客户端状态不为 STARTED 导致的。由于没遇到过这个问题,先不管其它的先复制黏贴百度走起,网上能搜索到的相关结果都是说 CuratorFramework 和 zookeeper 的版本不兼容引起的,兼容性如下:


  • Curator 2.x.x-兼容两个 zk 3.4.x 和 zk 3.5.x

  • Curator 3.x.x-兼容 zk 3.5


查看自己的相关版本,发现 zk 为 3.4.14,Curator 为:2.13.0,不存在所说的版本兼容问题。还有一个更无语的问题是:线下环境我不管怎么发布单独的项目都无法复现出相同的报错。无奈只能硬着头皮点进去源码去看。


首先定位到 org.apache.curator.framework.recipes.cache.TreeCache 这个类头上有一段这样的注释:


/** * <p>A utility that attempts to keep all data from all children of a ZK path locally cached. This class * will watch the ZK path, respond to update/create/delete events, pull down the data, etc. You can * register a listener that will get notified when changes occur.</p> * <p></p> * <p><b>IMPORTANT</b> - it's not possible to stay transactionally in sync. Users of this class must * be prepared for false-positives and false-negatives. Additionally, always use the version number * when updating data to avoid overwriting another process' change.</p> */
复制代码


半吊子英语翻译下意思大概是说:此类将监视 ZK 路径,尝试保留本地缓存 ZK 路径的所有子级的所有数据,响应更新/创建/删除事件,下拉数据等。说白了就是一句话:ZK 数据的缓存。上面的报错可以理解为它在响应更新/创建/删除事件的时候去 ZK 刷新数据的时候由于 Curator 客户端连接断开导致了报错。现在又可以将报错情况分为如下:


  1. 应用在正常的情况下,由于各种问题(可能是 ZK 集群压力大)Curator 客户端连接断开产生了报错

  2. 应用在启动的时候,需要进行本地缓存进行刷新、但是 Curator 客户端还没初始化好导致报错

  3. 应用在关闭的时候会报错


首先分析下第 1 种情况:考虑是不是由于多个 dubbo 服务同时发布,导致 ZK 集群节点变更频繁,集群压力大,导致部分项目 Curator 客户端与 ZK 断开连接恰巧此时需要刷新本地节点缓存导致报错。查看监控指标,此时 ZK 集群 CPU 指标如下:


对比上面的报错日志时间点,确实此时集群 CPU 存在突增飙高的情况,但是进一步分析所有项目报错日志排查发现,它们都有一个共性:它们都是当时有发布重启的项目,没有更新的服务不会产生报错,所以第 1 种情况被我排除了(当然理论上如果本地缓存请求 ZK 的时候,恰巧此时 Curator 客户端与 ZK 断开连接还是有可能发生的,但是怎么复现出来没有深入研究,因为不是我本次的错误原因


接下来再分析第 2 种情况,查看 dubbo 代码、发现 dubbo 启动 Curator 的方法在 org.apache.dubbo.remoting.zookeeper.curator.CuratorZookeeperClient#CuratorZookeeperClient、代码如下:

public CuratorZookeeperClient(URL url) {    super(url);    try {        int timeout = url.getParameter(TIMEOUT_KEY, DEFAULT_CONNECTION_TIMEOUT_MS);        int sessionExpireMs = url.getParameter(ZK_SESSION_EXPIRE_KEY, DEFAULT_SESSION_TIMEOUT_MS);        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder()                .connectString(url.getBackupAddress())                .retryPolicy(new RetryNTimes(1, 1000))                .connectionTimeoutMs(timeout)                .sessionTimeoutMs(sessionExpireMs);        String authority = url.getAuthority();        if (authority != null && authority.length() > 0) {            builder = builder.authorization("digest", authority.getBytes());        }        client = builder.build();        client.getConnectionStateListenable().addListener(new CuratorConnectionStateListener(url));        client.start();      //这里很关键的是,dubbo会判断Curator是否能连上ZK,连不上就进行报错        boolean connected = client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS);        if (!connected) {            throw new IllegalStateException("zookeeper not connected");        }    } catch (Exception e) {        throw new IllegalStateException(e.getMessage(), e);    }}
复制代码


根据上面的代码分析,加上断点调试得出了 TreeCache 启动时刷新 ZK 节点信息在 Curator 连接上 ZK 之后,还有一个更重要的现象是,Curator 连接 ZK 是同步连接的、如果连接不上就报错、如果 Curator 连接上 ZK 在 TreeCache 刷新缓存之后,那应该每次启动都会报错、可是现象并没有如此。


好了、接下来只剩最后一种情况了,就是应用在关闭的时候会产生这种错误。但是为啥我自己不断的在测试环境重启关闭项目,没有出现报错呢?此时我想起了线上线下发布服务关键的区别一点:就是线上是多个服务同时发布,然而我在测试环境都是单独发布同一个服务,此时我想起会不会由于线上同时发布的服务比较多,导致 ZK 节点变更频繁,也就是 TreeCache 本地缓存刷新频繁,恰巧此时由于关闭服务 Curator 与 ZK 的连接先断开,但是还在请求刷新节点信息导致了报错。观察此时 ZK 的其它指标找到如下:


确实此时 ZK 节点数量突增频繁,好了接下来就是进行线下复现了。

问题复现

接下来先模拟 ZK 节点数量突增的场景、导入 curator 的包:


<dependency>    <groupId>org.apache.curator</groupId>    <artifactId>curator-framework</artifactId>    <version>2.8.0</version></dependency>
复制代码


用 curator 创建 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"; //连续创建节点 for (int i = 0; i < 100000; i++) { client.create().creatingParentsIfNeeded().forPath(path + i, "init!".getBytes()); //client.delete().deletingChildrenIfNeeded().forPath(path + i); }
}
复制代码


接下来执行如下步骤:


  1. 本地启动 dubbo 服务

  2. 在控制台输入: lsof -i:端口号获取对应的 java 进程 pid

  3. 执行 ZK 节点创建代码

  4. 在控制台执行 kill -15 pid(步骤 2 获取的 pid)模拟线上关闭进程


此时控制台出现相同的报错,这里要注意的是关闭进程如果使用 kill -9 pid 不会复现因为当使用 kill -15 时,系统会发送一个 SIGTERM 的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的,但是 kill -9 特别强硬,系统会发出 SIGKILL 信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。

解决方案

因为这里是在 dubbo 服务停机的报出的异常,所以我就想到了 dubbo 的优雅停机,这里有个 dubbo 优雅停机的介绍:


具体如何实现优雅停机可以参考:https://www.cnkirito.moe/dubbo-gracefully-shutdown/

用户头像

林一

关注

没有幽默的笔风👉 2020.02.13 加入

还未添加个人简介

评论 (2 条评论)

发布
用户头像
👍
2021 年 05 月 29 日 13:07
回复
用户头像
描述很详尽,分析丝丝入扣,深入浅出。难得的好文!
2021 年 05 月 28 日 10:27
回复
没有更多了
发布引发的curator报错:instance must be started before calling this method