问题描述
目前公司项目 RPC 框架采用的是阿里开源的 dubbo,zk 客户端使用的是 Apache 的 Curator,项目发布的方式采用的是发布计划发布,一次发布多个微服务。在一次项目发布过程中发现部分项目出现如下报错:
问题定位
根据报错定位到最近的报错代码为:
@Override
public GetChildrenBuilder getChildren()
{
Preconditions.checkState(getState() == CuratorFrameworkState.STARTED, "instance must be started before calling this method");
return new GetChildrenBuilderImpl(this);
}
复制代码
根据代码可以看出,报错的原因是因为:执行 getChildren()方法的时候,Curator 客户端状态不为 STARTED 导致的。由于没遇到过这个问题,先不管其它的先复制黏贴百度走起,网上能搜索到的相关结果都是说 CuratorFramework 和 zookeeper 的版本不兼容引起的,兼容性如下:
查看自己的相关版本,发现 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 客户端连接断开导致了报错。现在又可以将报错情况分为如下:
应用在正常的情况下,由于各种问题(可能是 ZK 集群压力大)Curator 客户端连接断开产生了报错
应用在启动的时候,需要进行本地缓存进行刷新、但是 Curator 客户端还没初始化好导致报错
应用在关闭的时候会报错
首先分析下第 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);
}
}
复制代码
接下来执行如下步骤:
本地启动 dubbo 服务
在控制台输入: lsof -i:端口号获取对应的 java 进程 pid
执行 ZK 节点创建代码
在控制台执行 kill -15 pid(步骤 2 获取的 pid)模拟线上关闭进程
此时控制台出现相同的报错,这里要注意的是关闭进程如果使用 kill -9 pid 不会复现因为当使用 kill -15 时,系统会发送一个 SIGTERM 的信号给对应的程序。当程序接收到该信号后,具体要如何处理是自己可以决定的,但是 kill -9 特别强硬,系统会发出 SIGKILL 信号,他要求接收到该信号的程序应该立即结束运行,不能被阻塞或者忽略。
解决方案
因为这里是在 dubbo 服务停机的报出的异常,所以我就想到了 dubbo 的优雅停机,这里有个 dubbo 优雅停机的介绍:
具体如何实现优雅停机可以参考:https://www.cnkirito.moe/dubbo-gracefully-shutdown/
评论 (2 条评论)