dubbo泛化引发的生产故障之dubbo隐藏的坑

上个月公司zk集群发生了一次故障,然后要求所有项目组自检有无使用Dubbo编程式/泛化调用,强制使用@Reference生成Consumer。具体原因是线上某服务访问量在短时间大量访问zk并创建了240万+的节点,导致zk所有节点陆续崩溃导致,多个应用因无法连接到zk报错。原因是听说泛化调用时候,provider没启动,导致每次请求都在zk创建消费节点。

由于是和自己关联性不大的项目组,了解的并不是很清楚,但是想搞明白这个事情,因此就进行了如下实验:

试验1:泛化不使用缓存

dubbo泛化写法

public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
ApplicationConfig application = new ApplicationConfig();
application.setName("pangu-client-consumer-generic");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181");
// 服务消费者缺省值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0); reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setInterface(org.pangu.api.ProductService.class); // 弱类型接口名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 声明为泛化接口
GenericService svc = reference.get();
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//实际网关中,方法名、参数类型、参数是作为参数传入
return Result.success((Map)target);
}

这个写法,就没有缓存reference,因此每次请求这个方法,就会在zk创建个消费节点(无论provider是否启动),请求量大的时候,就会导致zk所有节点陆续崩溃。使用泛化不缓存,这个估计稍微看了官方文档都不会出现这个错误。引发这次故障的这个应用功能,又不是初次上线,运行了一段时间了,生产有zk节点数监控,不然初次就发现这个问题了。因此基本可以排除对方是没有使用缓存的问题。

试验2:泛化使用缓存

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache(); ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//缓存,否则每次请求都会创建一个ReferenceConfig,并在zk注册节点,最终可能导致zk节点过多影响性能
ApplicationConfig application = new ApplicationConfig();
application.setName("pangu-client-consumer-generic");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181"); // 服务消费者缺省值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0); reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setInterface(org.pangu.api.ProductService.class); // 弱类型接口名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 声明为泛化接口
GenericService svc = referenceCache.get(reference);//cache.get方法中会缓存 Reference对象,并且调用ReferenceConfig.get方法启动ReferenceConfig
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//实际网关中,方法名、参数类型、参数是作为参数传入
return Result.success((Map)target);
}

在provider端无论是否启动,都只会在zk创建一个消费节点

试验3:设置服务检查为true,reference.setCheck(true);

排除了前面两个试验,又查看了下dubbo源码,泛化使用ReferenceConfig,那么无论如何都会执行ReferenceConfig.get(),代码如下

public synchronized T get() {
if (destroyed) {
throw new IllegalStateException("Already destroyed!");
}
if (ref == null) {
init();
}
return ref;
}

ref为null,则执行初始化init,那么ref是怎么来的呢?是在init操作内由createProxy生成,createProxy代码如下:

//com.alibaba.dubbo.config.ReferenceConfig.createProxy(Map<String, String>)
private T createProxy(Map<String, String> map) {
//前面代码忽略
//使用Protocol创建Invoker,在zk创建consumer节点 Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {
// make it possible for consumer to retry later if provider is temporarily unavailable
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}

具体逻辑:

1.使用Protocol创建Invoker

2.检测服务端check=false,使用proxyFactory创建Invoker代理对象,即ref。

3.检测服务端check=true,如果provider未启动,则抛出IllegalStateException异常,自然ref就还是null了,那么下次访问,由于ref为null,则继续执行init->createProxy,在zk创建consumer节点。

那么是如何检测服务是否存活呢,即执行RegistryDirectory.isAvailable(),判断RegistryDirectory.urlInvokerMap是否为空,为空,肯定说明provider不存在。

PS:RegistryDirectory.urlInvokerMap缓存的是Invoker集合

问题大体明白了,因此试验下,设置check=true

@Override
public Result<Map> getProductGenericCache(ProductDTO dto) {
ReferenceConfigCache referenceCache = ReferenceConfigCache.getCache(); ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();//缓存,否则每次请求都会创建一个ReferenceConfig,并在zk注册节点,最终可能导致zk节点过多影响性能
ApplicationConfig application = new ApplicationConfig();
application.setName("pangu-client-consumer-generic");
// 连接注册中心配置
RegistryConfig registry = new RegistryConfig();
registry.setAddress("zookeeper://127.0.0.1:2181"); // 服务消费者缺省值配置
ConsumerConfig consumer = new ConsumerConfig();
consumer.setTimeout(5000);
consumer.setRetries(0); reference.setApplication(application);
reference.setRegistry(registry);
reference.setConsumer(consumer);
reference.setCheck(true);//试验3,设置检测服务存活
reference.setInterface(org.pangu.api.ProductService.class); // 弱类型接口名
// reference.setVersion("");
// reference.setGroup("");
reference.setGeneric(true); // 声明为泛化接口
GenericService svc = referenceCache.get(reference);//cache.get方法中会缓存 Reference对象,并且调用ReferenceConfig.get方法启动ReferenceConfig
Object target = svc.$invoke("findProduct", new String[]{ProductDTO.class.getName()}, new Object[]{dto});//实际网关中,方法名、参数类型、参数是作为参数传入
return Result.success((Map)target);
}

验证1:先启动provider服务,然后启动消费端泛化,请求此泛化方法,在zk只注册了一个consumer节点;停止provider,再请求此泛化方法,发现zk上此节点数量不变化。为什么呢?provider停止后,请求不再创建zk节点的原因是RegistryConfig的ref已经在启动时候生成了代理(由于启动时候provider服务存在,check=true校验过通过),因此不再创建。

验证2:不启动provider服务,直接启动消费端泛化,请求此泛化方法,发现每请求一次,在zk就会创建一个消费节点。至此验证到故障。

那么这种情况,为什么会每次请求都在zk创建消费节点呢?根本原因是什么?

private T createProxy(Map<String, String> map) {
//忽略其它代码 if (isJvmRefer) {
//忽略其它代码
} else {
if (url != null && url.length() > 0) {
//忽略其它代码
} else { // assemble URL from register center's configuration
List<URL> us = loadRegistries(false);//代码@1
if (us != null && !us.isEmpty()) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));//代码@2
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
} if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));//代码@3
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {//代码@4
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
if (registryURL != null) { // registry url is available
// use AvailableCluster only when register's cluster is available
URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME);
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry url
invoker = cluster.join(new StaticDirectory(invokers));
}
}
} Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {//check=true,provider服务不存在,抛出异常
// make it possible for consumer to retry later if provider is temporarily unavailable
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}

1.首次请求泛化方法,由于ReferenceConfig的ref为null,因此执行createProxy,执行的是代码@1、@2、@3,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。

2.第二次请求泛化方法,由于ReferenceConfig已经被缓存,这次的ReferenceConfig对象就是首次的ReferenceConfig对象,获取ReferenceConfig的代理对象ref,由于ReferenceConfig的ref为null,因此执行createProxy,执行的是代码@1、@2、@4,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。

3.第三次,以及后续的请求,都和第二次请求是一样效果。

为什么每次在zk都创建消费节点,只能说明订阅url不同导致的,如果url相同,在zk是不会创建的。那么订阅url的组成对一个服务来说有哪些不同呢?查看ReferenceConfig.init(),发现订阅url上有timestamp,是当前时间戳,这也说明了为什么每次都去注册,因为订阅url不同,如下图

那么订阅url上加上这个timestamp是否有些不合理呢?经过查看官方,在2.7.5版本中已经将订阅的URL中的timestamp去掉了,只会对一个URL订阅一次。

下图是故障时刻,对zk的dump解析,发现当时的ZK 目录节点数为170W,实际平时也就10w。

dubbo consumer泛化check=true对应用端的影响

private T createProxy(Map<String, String> map) {
//忽略其它代码 if (isJvmRefer) {
//忽略其它代码
} else {
if (url != null && url.length() > 0) {
//忽略其它代码
} else { // assemble URL from register center's configuration
List<URL> us = loadRegistries(false);//代码@1
if (us != null && !us.isEmpty()) {
for (URL u : us) {
URL monitorUrl = loadMonitor(u);
if (monitorUrl != null) {
map.put(Constants.MONITOR_KEY, URL.encode(monitorUrl.toFullString()));
}
urls.add(u.addParameterAndEncoded(Constants.REFER_KEY, StringUtils.toQueryString(map)));//代码@2
}
}
if (urls.isEmpty()) {
throw new IllegalStateException("No such any registry to reference " + interfaceName + " on the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion() + ", please config <dubbo:registry address=\"...\" /> to your spring config.");
}
} if (urls.size() == 1) {
invoker = refprotocol.refer(interfaceClass, urls.get(0));//代码@3
} else {
List<Invoker<?>> invokers = new ArrayList<Invoker<?>>();
URL registryURL = null;
for (URL url : urls) {//代码@4
invokers.add(refprotocol.refer(interfaceClass, url));
if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
registryURL = url; // use last registry url
}
}
if (registryURL != null) { // registry url is available
// use AvailableCluster only when register's cluster is available
URL u = registryURL.addParameterIfAbsent(Constants.CLUSTER_KEY, AvailableCluster.NAME);
invoker = cluster.join(new StaticDirectory(u, invokers));
} else { // not a registry url
invoker = cluster.join(new StaticDirectory(invokers));
}
}
} Boolean c = check;
if (c == null && consumer != null) {
c = consumer.isCheck();
}
if (c == null) {
c = true; // default true
}
if (c && !invoker.isAvailable()) {//check=true,provider服务不存在,抛出异常
// make it possible for consumer to retry later if provider is temporarily unavailable
initialized = false;
throw new IllegalStateException("Failed to check the status of the service " + interfaceName + ". No provider available for the service " + (group == null ? "" : group + "/") + interfaceName + (version == null ? "" : ":" + version) + " from the url " + invoker.getUrl() + " to the consumer " + NetUtils.getLocalHost() + " use dubbo version " + Version.getVersion());
}
if (logger.isInfoEnabled()) {
logger.info("Refer dubbo service " + interfaceClass.getName() + " from url " + invoker.getUrl());
}
// create service proxy
return (T) proxyFactory.getProxy(invoker);
}

1.首次请求泛化方法,由于ReferenceConfig的ref为null,因此执行createProxy,执行的是代码@1、@2、@3,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。把带时间戳的url加入到ReferenceConfig.urls集合。创建1个RegistryDirectory。

2.第二次请求泛化方法,由于ReferenceConfig已经被缓存,这次的ReferenceConfig对象就是首次的ReferenceConfig对象,获取ReferenceConfig的代理对象ref,由于ReferenceConfig的ref为null,因此执行createProxy,执行的是代码@1、@2、@4,在zk创建消费节点,但是由于check=true,因此抛出IllegalStateException异常,最终ReferenceConfig的ref依然为null。此时ReferenceConfig.urls集合是两个url,那么遍历urls,执行refprotocol.refer(interfaceClass, url),就创建了2个RegistryDirectory。

3.第三此请求泛化方法,基本同2,但是此时ReferenceConfig.urls集合是3个url,那么遍历urls,执行refprotocol.refer(interfaceClass, url),就创建了3个RegistryDirectory。

依次类推,第n次请求后,总计创建的RegistryDirectory对象1+2+3+....+n,因此dubbo泛化在设置check=true的情况下,不仅最终会导致zk故障,本地应用也会出现oom。

用这个测试下oom问题,学会分析下dump

jmeter配置

具体在pangu-client-parent工程内

效果图如下

参考 https://cloud.tencent.com/developer/article/1760931

dubbo泛化引发的生产故障之dubbo隐藏的坑的更多相关文章

  1. dubbo泛化调用 小demo

    前两天刚好有个同事来问是否用过 dubbo泛化 调用,不需要通过指定配置.第一次听到的时候,还是有点懵,但觉得有意思,可以学点东西. 立马百度了,找了demo,这篇比较容易上手(http://www. ...

  2. dubbo+zookeeper+springBoot框架整合与dubbo泛型调用演示

    dubbo + zookeeper + spring Boot框架整合与dubbo泛型调用演示   By:客 授客 QQ:1033553122  欢迎加入全国软件测试交流 QQ  群:7156436 ...

  3. 【Dubbo实战】 Dubbo+Zookeeper+Spring整合应用篇-Dubbo基于Zookeeper实现分布式服务(转)

    Dubbo与Zookeeper.Spring整合使用 Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spri ...

  4. Dubbo+Zookeeper+Spring整合应用篇-Dubbo基于Zookeeper实现分布式服务(转)

    Dubbo与Zookeeper.Spring整合使用 Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spri ...

  5. Dubbo学习总结(2)——Dubbo架构详解

    一.前言 部门去年年中开始各种改造,第一步是模块服务化,这边初选dubbo试用在一些非重要模块上,慢慢引入到一些稍微重要的功能上,半年时间,学习过程及线上使用遇到的些问题在此总结下. 整理这篇文章差不 ...

  6. Dubbo学习总结(4)——Dubbo基于Zookeeper实现分布式实例

    入门实例解析 第一:provider-提供服务和相应的接口 创建DemoService接口 [java] view plaincopyprint? <span style="font- ...

  7. dubbo 2.7.0 中缺乏 <dubbo:annotation /> 的解决方案

    一.背景  从 dubbo 2.6.5 升级到 2.7.0,突然发现好多地方不能用了,dubbo:annotation 直接报红,原先的 @Service 和 @Reference 中直接报了过时,源 ...

  8. 【Dubbo&&Zookeeper】6、 给dubbo接口添加白名单——dubbo Filter的使用

    在开发中,有时候需要限制访问的权限,白名单就是一种方法.对于Java Web应用,Spring的拦截器可以拦截Web接口的调用:而对于dubbo接口,Spring的拦截器就不管用了. dubbo提供了 ...

  9. Dubbo学习笔记11:使用Dubbo中需要注意的一些事情

    指定方法异步调用 前面我们讲解了通过设置ReferenceConfig的setAsync()方法来让整个接口里的所有方法变为异步调用,那么如何指定某些方法为异步调用呢?下面讲解下如何正确地设置默写方法 ...

随机推荐

  1. 资源工作表中与资源有关的操作(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 这个内容,我需要专门写一篇吗? 不写吧,好像对不起我那股学习的劲:写吧,实在是--一句话就够了:所有与任务有关的新建.修改 ...

  2. java 图形化小工具Abstract Window Toolit 菜单项

    AWT 中的菜单由如下几个类组合而成 MenuBar: 菜单条,菜单的容器. Menu: 菜单组件,菜单项的容器,它也是Menultem的子类,所以可作为菜单项使用. PopupMenu: 上下文菜单 ...

  3. java 图形化小工具Abstract Window Toolit

      老掉牙的历史 Java1.0在发布的时候,就为我们提供了GUI操作的库,这个库系统在所有的平台下都可以运行,这套基本的类库被称作抽象窗口工具集(Abstract Window Toolit),简称 ...

  4. nim_duilib(17)之xml配置窗口调整大小

    本文目标 xml配置窗口,使得窗口可以调整大小. sizebox windows的属性sizebo的作用就是设置窗口可以调整大小.鼠标放到窗口边缘,拖动窗口并改变大小. 一个例子 <Window ...

  5. 【LeetCode】98. Validate Binary Search Tree 解题报告(Python & C++ & Java)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 递归 BST的中序遍历是有序的 日期 题目地址:ht ...

  6. spoj-SUBSUMS - Subset Sums

    SUBSUMS - Subset Sums Given a sequence of N (1 ≤ N ≤ 34) numbers S1, ..., SN (-20,000,000 ≤ Si ≤ 20, ...

  7. 【LeetCode】93. Restore IP Addresses 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 回溯法 日期 题目地址:https://leetco ...

  8. @Transactional 注解实现

    @Transactional注解简介 @Transactional是spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚.@Transactional注解可以帮助我们把事务开启. ...

  9. Orcale

    oracleoracle中不存在引擎的概念,数据处理大致可以分成两大类:联机事务处理OLTP(on-line transaction processing).联机分析处理OLAP(On-Line An ...

  10. Java Web程序设计笔记 • 【第8章 会话跟踪技术进阶】

    全部章节   >>>> 本章目录 8.1 Session机制 8.1.1 Session 简介 8.1.2 创建 HttpSession 实例 8.1.3 HttpSesiso ...