这个上半年我们在线上发现的一个bug,现在想起来,还是记录一下。希望官方也能修复
这是一个Dubbo的bug,
版本是2.8.4 (这个版本号是当当网基于2.5.3增加了几个特性后的分支版本,中央仓库应该没有)
如果应用配置了 monitor=registry,当DNS异常时有可能导致应用OOM,并且无法恢复。
这个情况比较极端,一般很难出现,但是 。。。 我们碰到了。。。
模拟过程:
在测试环境部署一个 dubbo应用,配置 monitor=registry
dubbo的注册中心配置,配的是域名,并且域名是通过DNS解析(非/etc/hosts解析)
启动一个测试脚本,循环调用该应用提供的几个服务。
修改 /etc/resolv.conf 将DNS的IP 修改为错误IP
脚本继续运行,引发dubbo应用的内存泄漏问题
dubbo 封装了一个工具方法:
com.alibaba.dubbo.common.utils.NetUtils.getIpByHost(host)
public static String getIpByHost(String hostName) {
try{
return InetAddress.getByName(hostName).getHostAddress();
}catch (UnknownHostException e) {
return hostName;
}
}
当DNS发生故障时,通过域名获取IP这个工具方法
返回的不是IP地址而是域名。
在配置了dubbo的 monitor=registry的情况下,dubbo完成业务后将调用
com.alibaba.dubbo.monitor.support.MonitorFilter 的 collect()方法
【请自行看源码】
该方法将执行
Monitor monitor = monitorFactory.getMonitor(url);
获取monitor对象,dubbo设计为可支持多个monitor,因此内部使用了Map保存多个monitor,每次调用getMonitor()时将从url计算出key,代码片段如下:
代码位于:
com.alibaba.dubbo.monitor.support.AbstractMonitorFactory
其中的第49行
url.toServiceString()
里面使用了之前提到过的
NetUtils.getIpByHost(host) 方法
因此,
当DNS正常时,计算出来的key是包含IP的形如 172.32.42.34/*
当DNS异常时,计算出来的key是包含域名的形如 zookeeper.service.zbj.com/*
so
在DNS出问题之前,MONITORS这个map中只有一个k-v,key是”172.32.42.34/*”,代码走入第54行,直接返回已有的monitor。
而当DNS发生异常时,计算出来的key不同了,代码进入第56行。
继续进入第56行代码的具体实现:
….略….
将进入
AbstractRegistryFactory 的 getRegistry()方法
代码片段如下:
类似的,key在DNS正常时,异常时返回的值不同。
在异常情况下,将进入第94行。
createRegistry是抽象方法,在我们的配置情况下,将执行如下代码:
由于ZookeeperRegistry extends FailbackRegistry
先看 ZookeeperRegistry的构造方法:
再仔细研读FailbackRegistry代码,FailbackRegistry 将是一个 Deamon类,创建后不会被销毁。
继续。。。
回到ZookeeperRegistry的构造方法,代码走到第69行
zkClient = zookeeperTransporter.connect(url);
由于DNS异常,该方法将抛出异常。
再回到
AbstractRegistryFactory 的 getRegistry()方法
这里是一个关键点:
异常抛出后,直接进入finnaly块,REGISTERS这个map里面依然只有一个k-v。
同理
com.alibaba.dubbo.monitor.support.AbstractMonitorFactory 的 getMonitor() 方法
也是一样的情况,异常抛出后 map里只有一个k-v。
当下一次请求进来时,从两个地方的map里面依然无法找到,然后就会按照刚刚分析的代码一样的执行,就又会进入getRegistry() 第94行,继续抛出异常,map继续只有一个k-v。
从上面的分析,可以得到如下结论:
如果DNS有问题、且配置了dubbo monitor=registry,每次请求都会new出一个ZookeeperRegistry,ZookeeperRegistry是一个Demon类,创建之后无法销毁。
继续往下分析:
当yongGC时,ZookeeperRegistry会直接进入老年代,dubbo应用 JVM的老年代是有限的,我们用了CMSGC配置为达到80%以上触发FullGC,然而 FullGC也不会将 ZookeeperRegistry清理掉。
于是,在DNS无法联通的这段时间里,非常多的 ZookeeperRegistry实例被创建(每一个请求创建一个该对象的实例),当达到 老年代的80%后, CMSGC将循环无限被执行,这个会使JVM 进入非常多的 stop-the-world 状态,该JVM接受到的请求的响应时间会变得很长(试CMSGC的stw时间而定,有的很长有的有点长)。
而由于内存根本无法被回收,即使 DNS恢复了,JVM也无法恢复。
(注:若DNS异常的时间更短一些,新创建的ZookeeperRegistry的数量还不多-没达到老年代的80%,情况将会好很多)