前言

dubbo是一个成熟且被广泛运用的框架。饶是如此,在某些极端条件下基于dubbo的应用还会出现无法重连zookeeper的问题。由于此问题容易导致比较大的故障,所以笔者费了一番功夫去定位,现将排查过程写成博文分享出来。

Bug现场

这是一起在测试环境出现的故障。起因是网工做交换机切换演练,可能由于姿势不对,使得断网的时间从预估的秒级达到了分钟级。等网络恢复后,测试环境就炸开了锅,基本上所有应用再也无法提供服务,在dubbo控制台上也看不到任何提供者,他们和zk的连接都断开而且似乎完全没有重连的迹象。如下图所示:

无法快速恢复

为了不影响测试的进度,运维同学紧急进行了重启,但坑爹的是大部分系统都有启动依赖,盲目的重启只会因为xxx provider不存在而无法启动。只能从最基础的服务开始重启,慢慢恢复。如下图所示:



还好只是测试环境,但为了不让产线出现这种问题,必须一查到底,把这个Bug揪出来。

着手排查

模拟zookeeper连接断开

测试环境的好处是我们可以用各种手段去模拟复现,而不用和处理产线一样到处寻找蛛丝马迹然后进行逻辑推理(推理是一个非常烧脑的过程)。于是笔者联系了SA同学,通过iptables进行线下的断网模拟。命令如下所示:

// 禁用本机和zk三台机器的流量进出
iptables -A INPUT -s zk-1-ip/32 -j DROP
iptables -A INPUT -s zk-2-ip/32 -j DROP
iptables -A INPUT -s zk-3-ip/32 -j DROP iptables -A OUTPUT -s zk-1-ip/32 -j DROP
iptables -A OUTPUT -s zk-2-ip/32 -j DROP
iptables -A OUTPUT -s zk-3-ip/32 -j DROP

拓扑图如下:



发现在drop对zk的包之后,不管等待多长时间,只要连接一放开,立马就能重连zk!

看来dubbo对zookeeper的重连还是非常靠谱的。

同时模拟DNS断开

由于模拟zk断开不会导致无法重连的现象。于是笔者开始思考,是否交换机异常的时候导致了所有的包都无法发送/接收,而导致重连出问题的并不是对zookeeper发起连接。于是笔者看了看配置,是否还有其它和重连有关联的点,仔细观察下这个配置:

// 这其中有一个不容易注意到的点,就是域名解析也需要网络包的交互
dubbo.registry.address=zookeeper://dubbo-1.com?back=dubbo-2.com,dubbo-3.com

难道是DNS访问不到导致了这一问题?反正测试环境,继续模拟一发,命令如下所示:

// 禁用本机和zk三台机器的流量进出
iptables -A INPUT -s zk-1-ip/32 -j DROP
iptables -A INPUT -s zk-2-ip/32 -j DROP
iptables -A INPUT -s zk-3-ip/32 -j DROP
iptables -A OUTPUT -s zk-1-ip/32 -j DROP
iptables -A OUTPUT -s zk-2-ip/32 -j DROP
iptables -A OUTPUT -s zk-3-ip/32 -j DROP
// 禁用本机和DNS两台机器的流量进出
iptables -A INPUT -s dns-ip/32 -j DROP
iptables -A INPUT -s dns-ip/32 -j DROP
iptables -A OUTPUT -s dns-ip/32 -j DROP
iptables -A OUTPUT -s dns-ip/32 -j DROP

网络拓扑如下:



这次我们在禁用流量后,故意先放开对zk的流量,再放开对DNS的流量,如下图所示:



看来在dubbo对zookeeper重连过程中,如果DNS也无法响应,是会出现网络恢复后也再也无法重连的现象。但是,我们并不能下判断交换机的故障导致的无法重连肯定是这个Bug引起。需要找到证据来证明这一点!

它山之石,可以攻玉

有了DNS这个信息后,先google一下,看看能否有其它人遇到过这个坑。于是找到了这个链接

https://github.com/sgroschupf/zkclient/issues/23



按照github上的描述,zkclient在UnknownHostException抛出之后再也无法重连zookeeper。不过他是在Kafka中遇到的,但他推断所有用低版本org.apache.zookeper的都会有这个问题。按照上面给出的Bug Fix链接

https://issues.apache.org/jira/browse/ZOOKEEPER-1576

笔者发现其在3.5.0修复



于是将对应的应用的org.apache.zookeeper版本升级到3.5.5版本,重新实验后,发现问题解决了!

这里有个小技巧,我们可以通过

zip -d xxx.jar WEB-INF/lib/zookeeper-3.4.8.jar
zip -r 0 xxx.jar WEB-INF/lib/zookeeper-3.5.5.jar
// 以及zip -r 其它zookeeper-3.5.5新依赖的包

使得不用重新编译打包的方式即可修改应用使用的jar包版本,这样在快速验证的时候就不需要通知对应的开发修改依赖了。

寻找支持jdk1.6的zookeeper jar包

由于笔者所在的产线环境有很多老系统用的jdk1.6,而zookeeper-3.5.5之支持1.8及以上版本,所以需要寻找能够给jdk1.6使用的包。此时,笔者的同事由于负责kafka,其对kafa做过混沌测试,坚信kafka没有这个问题,于是笔者就用kafka依赖的zookeeper-3.4.13包继续进行测试,发现zookeeper-3.4.13也是okay的,具体代码改动将会在下面讲到。

搜索日志寻找证据

由于可能是UnknownHostException导致了这一问题,笔者在出问题的应用里面寻找,确实发现了UnknownHostException。日志如下所示:

// 下面过滤了一大堆disconnected连接断开日志,只列出核心有关的
2020-03-19 21:06:28.926 [DubboZkclientConnector-EventThread] zookeeper state changed (Disconnected)
2020-03-19 21:06:28.926 [ZkClient-EventThread-101-dubbo.com] Zookeeper失去连接
2020-03-19 21:06:49.758 [DubboZkclientConnector-EventThread] zookeeper state changed (Expired)
2020-03-19 21:06:49:759 [DubboZkclientConnecto-SendThread] Unable to reconnect to ZooKeeper sercice ,session 0xXXXXX has expired
2020-03-19 21:07:29.793 [DubboZkclientConnector-EventThread] ERROR ClientCxnn - Error while calling watcher
java.lang.RuntimeException: Exception while restarting zk client
......
......
Caused by: java.net.UnknownHostException: dubbo-1.com
at...lookupAllHostAddr...
......
at...StaticHostProvier...
......
at...reconnect...[zookeeper-3.4.8.jar:3.4.8--1]

上面日志反应出在zookeeper session expired之后重新建立session的过程中如果抛出java.net.UnknownHostException后,zkclient对应的线程就再也不会有其它动作。

代码分析

旧版本代码逻辑

上面的证据再配合实验的结果基本就能确定这个DNS异常会导致dubbo无法重连zookeeper的现象。于是笔者开发翻阅代码,首先我们看下dubbo重连的逻辑:

public class ZkclientZookeeperClient extends AbstractZookeeperClient<IZkChildListener> {
......
public ZkclientZookeeperClient(URL url) {
super(url);
client = new ZkClientWrapper(url.getBackupAddress(), 30000);
client.addListener(new IZkStateListener() {
public void handleStateChanged(KeeperState state) throws Exception {
ZkclientZookeeperClient.this.state = state;
if (state == KeeperState.Disconnected) {
stateChanged(StateListener.DISCONNECTED);
} else if (state == KeeperState.SyncConnected) {
stateChanged(StateListener.CONNECTED);
}
}
// 这边是session重建的过程
public void handleNewSession() throws Exception {
stateChanged(StateListener.RECONNECTED);
}
});
client.start();
}
......
}
// StateListener.RECONNECTED的处理在zookeeperResigry.java中
public class ZookeeperRegistry extends FailbackRegistry{
......
zkClient.addStateListener(new StateListener() {
// 在收到RECONNECTED事件后,进行recover也就是恢复的过程
public void stateChanged(int state) {
if (state == RECONNECTED) {
try {
recover();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
});
......
}

由上面的代码我们可以得知,在session expired之后内部会重建session,在新建session之后,dubbo的Statelistener会发送reconnected事件从而执行恢复的过程,如下图所示:



那么我们看下UnknownHostException抛出后会导致什么现象,代码如下所示:

public class ZkClient implements Watcher {
......
private void processStateChanged(WatchedEvent event) {
// 这边对应于session expired日志
LOG.info("zookeeper state changed (" + event.getState() + ")");
setCurrentState(event.getState());
if (getShutdownTrigger()) {
return;
}
try {
fireStateChangedEvent(event.getState()); if (event.getState() == KeeperState.Expired) {
// UnknownHostException就是在这抛出。
reconnect();
fireNewSessionEvents();
}
} catch (final Exception e) {
// 这边对应于Error while calling watcher日志
throw new RuntimeException("Exception while restarting zk client", e);
}
} ......
}

异常是在reconnect抛出的,异常抛出后,就不会运行fireNewSessionEvents这个逻辑,也就不会执行listener中的handleNewSession逻辑,进而不会recover,从而导致dubbo无法重连!如下图所示:

新版本如何修复

由于UnknownHostException是在StaticHostProviver中触发,在这边笔者给出了新旧版本的对应代码,旧版本zookeeper-3.4.8

public final class StaticHostProvider implements HostProvider{
......
public StaticHostProvider(Collection<InetSocketAddress> serverAddresses)
throws UnknownHostException {
for (InetSocketAddress address : serverAddresses) {
InetAddress ia = address.getAddress();
// 这边没有抓住UnknownHostException异常
InetAddress resolvedAddresses[] = InetAddress.getAllByName((ia!=null) ? ia.getHostAddress():
address.getHostName());
for (InetAddress resolvedAddress : resolvedAddresses) {
......
if (resolvedAddress.toString().startsWith("/")
&& resolvedAddress.getAddress() != null) {
this.serverAddresses.add(
new InetSocketAddress(InetAddress.getByAddress(
address.getHostName(),
resolvedAddress.getAddress()),
address.getPort()));
} else {
this.serverAddresses.add(new InetSocketAddress(resolvedAddress.getHostAddress(), address.getPort()));
}
}
} if (this.serverAddresses.isEmpty()) {
throw new IllegalArgumentException(
"A HostProvider may not be empty!");
}
Collections.shuffle(this.serverAddresses);
}
......
}

新版本zookeeper-3.4.13小小的重构了一下,将DNS的逻辑放到next函数里面并抓住了UnknownHostException异常。

public final class StaticHostProvider implements HostProvider{
......
public InetSocketAddress next(long spinDelay) {
currentIndex = ++currentIndex % serverAddresses.size();
if (currentIndex == lastIndex && spinDelay > 0) {
try {
Thread.sleep(spinDelay);
} catch (InterruptedException e) {
LOG.warn("Unexpected exception", e);
}
} else if (lastIndex == -1) {
// We don't want to sleep on the first ever connect attempt.
lastIndex = 0;
} InetSocketAddress curAddr = serverAddresses.get(currentIndex);
try {
String curHostString = getHostString(curAddr);
List<InetAddress> resolvedAddresses = new ArrayList<InetAddress>(Arrays.asList(this.resolver.getAllByName(curHostString)));
if (resolvedAddresses.isEmpty()) {
return curAddr;
}
Collections.shuffle(resolvedAddresses);
return new InetSocketAddress(resolvedAddresses.get(0), curAddr.getPort());
} catch (UnknownHostException e) {
// 就是这边抓住了UnknownHostException进而修复了这一问题
return curAddr;
}
}
......
}

我们可以看到新版zookeeper-3.4.13抓住了这个UnknownHostException进而修复了这一问题。

BUG触发条件复盘

在与zookeeper服务连接异常并session expired(默认30s),DNS缓存也超时(默认30s)同时用的低版本zookeeper jar包就很容易达成上述Bug的情况。而之前测试环境由于交换机切换的某些原因使得网络断开超过了30s从而诱发了这一问题。而且不仅仅是Dubbo,任何用zookeeper低版本jar包都有可能出现这个问题!

总结

海恩法则指出,每一起严重事故的背后,必然有29次轻微事故和300起未遂先兆以及1000起事故隐患。所以对测试环境的任何问题都要引起重视,从而把问题消灭在萌芽阶段。

公众号

关注笔者公众号,获取更多干货文章:

原文链接

https://my.oschina.net/alchemystar/blog/3222767

解Bug之路-dubbo应用无法重连zookeeper的更多相关文章

  1. 解Bug之路-dubbo流量上线时的非平滑问题

    前言 笔者最近解决了一个困扰了业务系统很久的问题.这个问题只在发布时出现,每次只影响一两次调用,相较于其它的问题来说,这个问题有点不够受重视.由于种种原因,使得这个问题到了业务必须解决的程度,于是就到 ...

  2. 解Bug之路-TCP粘包Bug

    解Bug之路-TCP粘包Bug - 无毁的湖光-Al的个人空间 - 开源中国 https://my.oschina.net/alchemystar/blog/880659 解Bug之路-TCP粘包Bu ...

  3. 解Bug之路-Nginx 502 Bad Gateway

    解Bug之路-Nginx 502 Bad Gateway 前言 事实证明,读过Linux内核源码确实有很大的好处,尤其在处理问题的时刻.当你看到报错的那一瞬间,就能把现象/原因/以及解决方案一股脑的在 ...

  4. 解Bug之路-记一次对端机器宕机后的tcp行为

    解Bug之路-记一次对端机器宕机后的tcp行为 前言 机器一般过质保之后,就会因为各种各样的问题而宕机.而这一次的宕机,让笔者观察到了平常观察不到的tcp在对端宕机情况下的行为.经过详细跟踪分析原因之 ...

  5. 解Bug之路-主从切换"未成功"?

    解Bug之路-主从切换"未成功"? 前言 数据库主从切换是个非常有意思的话题.能够稳定的处理主从切换是保证业务连续性的必要条件.今天笔者就来讲讲主从切换过程中一个小小的问题. 故障 ...

  6. 解Bug之路-ZooKeeper集群拒绝服务

    解Bug之路-ZooKeeper集群拒绝服务 前言 ZooKeeper作为dubbo的注册中心,可谓是重中之重,线上ZK的任何风吹草动都会牵动心弦.最近笔者就碰到线上ZK Leader宕机后,选主无法 ...

  7. 解Bug之路-记一次中间件导致的慢SQL排查过程

    解Bug之路-记一次中间件导致的慢SQL排查过程 前言 最近发现线上出现一个奇葩的问题,这问题让笔者定位了好长时间,期间排查问题的过程还是挺有意思的,正好博客也好久不更新了,就以此为素材写出了本篇文章 ...

  8. 解Bug之路-记一次存储故障的排查过程

    解Bug之路-记一次存储故障的排查过程 高可用真是一丝细节都不得马虎.平时跑的好好的系统,在相应硬件出现故障时就会引发出潜在的Bug.偏偏这些故障在应用层的表现稀奇古怪,很难让人联想到是硬件出了问题, ...

  9. 解Bug之路-记一次JVM堆外内存泄露Bug的查找

    解Bug之路-记一次JVM堆外内存泄露Bug的查找 前言 JVM的堆外内存泄露的定位一直是个比较棘手的问题.此次的Bug查找从堆内内存的泄露反推出堆外内存,同时对物理内存的使用做了定量的分析,从而实锤 ...

随机推荐

  1. 4.24 省选模拟赛 欧珀瑞特 主席树 可持久化trie树

    很容易的一道题目.大概.不过我空间计算失误MLE了 我草草的计算了一下没想到GG了. 关键的是 我学了一个dalao的空间回收的方法 但是弄巧成拙了. 题目没有明确指出 在任意时刻数组长度为有限制什么 ...

  2. bzoj 1738 [Usaco2005 mar]Ombrophobic Bovines 发抖的牛 最大流+二分

    题目要求所有牛都去避雨的最长时间最小. 显然需要二分 二分之后考虑如何判定. 显然每头牛都可以去某个地方 但是前提是最短路径<=mid. 依靠二分出来的东西建图.可以发现这是一个匹配问题 din ...

  3. 题解 Luogu P1514 【引水入城】

    有一种神奇的算法叫做floodfill 就是一个n*m的矩阵,a[i][j]为当前高度,我们可以任选一个点倒水,开始bfs,如果要搜的点没有被搜到过,并且高度小于当前的点,我们就把这个点加入队列中 而 ...

  4. 【BZOJ2724】蒲公英 题解(分块+区间众数)

    题目链接 题目大意:给定一段长度为$n$的序列和$m$次询问,每次询问区间$[l,r]$内的最小的众数.$n\leq 40000,a_i\leq 10^9$ --------------------- ...

  5. Java泛型是什么?实战demo

    1. 概述 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用. 什么是泛型?为什么要使用泛型? 泛型,即“参数化类型”.一提到参数,最熟悉的就是定义方法时有形参,然后调用 ...

  6. 重写ThreadPoolTaskExecutor

    目录 主类开启异步注解 创建线程池配置类 创建线程池实现类 创建一个测试类Controller 创建异步Service方法 定义异步的实现类 ThreadPoolExecutor:JDK内置线程池实现 ...

  7. Vue组件通信之子传父

    子组件向父组件通信主要通过自定义事件实现. 这里我记录一个小例子来帮助自己记忆. 通过点击子组件的按钮去执行父组件的函数并使用子组件传来的数据. 子组件定义如下: <template id=&q ...

  8. DB2 创建存储过程保存:XX 后面找到异常标记 "END-OF-STATEMENT"。

    存储过程最后一行加结束符@: 然后执行:db2 -td@ -vf /home/WGJ/proc_data_calculate.sql [slsadmin@localhost /]$ db2 -td@ ...

  9. Ambiguous mapping. Cannot map 'xxxController' method

    @GetMapping public JsonResp<List<DtoLandRegion>> getLandRegionList() { List<DtoLandRe ...

  10. Java—面向对象、类与对象、封装

    理解什么是面向过程.面向对象 面向过程与面向对象都是我们编程中,编写程序的一种思维方式. 面向过程的程序设计方式,是遇到一件事时,思考“我该怎么做”,然后一步步实现的过程. 面向对象的程序设计方式,是 ...