解Bug之路-dubbo应用没法重连zookeeper


前言

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

Bug现场

这是一块儿在测试环境出现的故障。原由是网工作交换机切换演练,可能因为姿式不对,使得断网的时间从预估的秒级达到了分钟级。等网络恢复后,测试环境就炸开了锅,基本上全部应用再也没法提供服务,在dubbo控制台上也看不到任何提供者,他们和zk的链接都断开并且彷佛彻底没有重连的迹象。以下图所示:
git

没法快速恢复

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

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

着手排查

模拟zookeeper链接断开

测试环境的好处是咱们能够用各类手段去模拟复现,而不用和处理产线同样处处寻找蛛丝马迹而后进行逻辑推理(推理是一个很是烧脑的过程)。因而笔者联系了SA同窗,经过iptables进行线下的断网模拟。命令以下所示:apache

// 禁用本机和zk三台机器的流量进出
iptables -A INPUT -s zk-1-ip/32 -j DROPiptables -A INPUT -s zk-2-ip/32 -j DROPiptables -A INPUT -s zk-3-ip/32 -j DROPiptables -A OUTPUT -s zk-1-ip/32 -j DROPiptables -A OUTPUT -s zk-2-ip/32 -j DROPiptables -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访问不到致使了这一问题?反正测试环境,继续模拟一发,命令以下所示:session

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

网络拓扑以下:

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

看来在dubbo对zookeeper重连过程当中,若是DNS也没法响应,是会出现网络恢复后也再也没法重连的现象。可是,咱们并不能下判断交换机的故障致使的没法重连确定是这个Bug引发。须要找到证据来证实这一点!app

它山之石,能够攻玉

有了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.jarzip -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 expired2020-03-19 21:07:29.793 [DubboZkclientConnector-EventThread] ERROR ClientCxnn - Error while calling watcherjava.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起事故隐患。因此对测试环境的任何问题都要引发重视,从而把问题消灭在萌芽阶段。