dubbo泛化引发的生产故障之dubbo隐藏的坑
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隐藏的坑的更多相关文章
- dubbo泛化调用 小demo
前两天刚好有个同事来问是否用过 dubbo泛化 调用,不需要通过指定配置.第一次听到的时候,还是有点懵,但觉得有意思,可以学点东西. 立马百度了,找了demo,这篇比较容易上手(http://www. ...
- dubbo+zookeeper+springBoot框架整合与dubbo泛型调用演示
dubbo + zookeeper + spring Boot框架整合与dubbo泛型调用演示 By:客 授客 QQ:1033553122 欢迎加入全国软件测试交流 QQ 群:7156436 ...
- 【Dubbo实战】 Dubbo+Zookeeper+Spring整合应用篇-Dubbo基于Zookeeper实现分布式服务(转)
Dubbo与Zookeeper.Spring整合使用 Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spri ...
- Dubbo+Zookeeper+Spring整合应用篇-Dubbo基于Zookeeper实现分布式服务(转)
Dubbo与Zookeeper.Spring整合使用 Dubbo采用全Spring配置方式,透明化接入应用,对应用没有任何API侵入,只需用Spring加载Dubbo的配置即可,Dubbo基于Spri ...
- Dubbo学习总结(2)——Dubbo架构详解
一.前言 部门去年年中开始各种改造,第一步是模块服务化,这边初选dubbo试用在一些非重要模块上,慢慢引入到一些稍微重要的功能上,半年时间,学习过程及线上使用遇到的些问题在此总结下. 整理这篇文章差不 ...
- Dubbo学习总结(4)——Dubbo基于Zookeeper实现分布式实例
入门实例解析 第一:provider-提供服务和相应的接口 创建DemoService接口 [java] view plaincopyprint? <span style="font- ...
- dubbo 2.7.0 中缺乏 <dubbo:annotation /> 的解决方案
一.背景 从 dubbo 2.6.5 升级到 2.7.0,突然发现好多地方不能用了,dubbo:annotation 直接报红,原先的 @Service 和 @Reference 中直接报了过时,源 ...
- 【Dubbo&&Zookeeper】6、 给dubbo接口添加白名单——dubbo Filter的使用
在开发中,有时候需要限制访问的权限,白名单就是一种方法.对于Java Web应用,Spring的拦截器可以拦截Web接口的调用:而对于dubbo接口,Spring的拦截器就不管用了. dubbo提供了 ...
- Dubbo学习笔记11:使用Dubbo中需要注意的一些事情
指定方法异步调用 前面我们讲解了通过设置ReferenceConfig的setAsync()方法来让整个接口里的所有方法变为异步调用,那么如何指定某些方法为异步调用呢?下面讲解下如何正确地设置默写方法 ...
随机推荐
- IDEA 2021.2.1 破解版下载_激活安装图文教程(永久激活,亲测有效)
1.IntelliJ IDEA 2021 链接:https://pan.baidu.com/s/1Pwz3GrrkJdDZzg-wg5UjMw 提取码:56o6 无限重置 30 天试用期补丁 链接:h ...
- PowerDotNet平台化软件架构设计与实现系列(08):缓存平台
几乎所有后端应用都会或多或少用到缓存,尤其是分布式缓存服务,以及和本地缓存构造的二级缓存.根据我们一贯的节约代码的风格,为了复用的目标,抽象出缓存平台,进行缓存管理. 考虑到很多公司都会自己造或者直接 ...
- CF102B Sum of Digits 题解
Content 给定一个数 \(n\),每次操作可以将 \(n\) 变成 \(n\) 各位数之和.问你几次操作之后可以将 \(n\) 变为一位数. 数据范围:\(1\leqslant n\leqsla ...
- MIUI12.5扫码之后无法连接MIUI+,显示连接失败
设置-应用设置-应用管理-小米互联通信服务(如果没有找到,进行搜索即可)-清除数据 重新扫码连接就可以连上了 (感觉不怎么样,不知道是不是我网卡,用起来卡卡的...)
- Xshell连接Ubuntu服务器连接不上 显示拒绝了密码
确保下方两个都安装了 sudo apt-get install openssh-server sudo apt-get install ssh 修改 vim /etc/ssh/sshd_config ...
- 鱼书_第一章_Python入门
Python版本 Python有Python 2.x和Python 3.x两个版本.两个版本不兼容,可能出现用Python 3.x编的代码不能被Python 2.x执行的情况. Python安装 An ...
- 【LeetCode】978. Longest Turbulent Subarray 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 虫取法 日期 题目地址:https://leetco ...
- 【LeetCode】657. Judge Route Circle 解题报告
[LeetCode]657. Judge Route Circle 标签(空格分隔): LeetCode 题目地址:https://leetcode.com/problems/judge-route- ...
- 【LeetCode】658. Find K Closest Elements 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/find-k-c ...
- ROC and AUC
目录 概 TPR, FPR ROC and AUC 代码 ROC-wiki 概 AUC常常在文章中作为评价一个分类器优劣的指标, 却总是忘记其原由, 索性记上一笔. TPR, FPR 首先理解TP, ...