如果你用过Dubbo,但是没碰到过什么坑,那只能说明你还没有深交Dubbo,看看笔者那些年使用Dubbo踩过的坑!
假设Provider提供的服务中某个服务的参数是WordDTO,并且WordDTO继承自BaseDTO,两个类的定义如下:
@Datapublic class BaseDTO implements Serializable { private Long id;}@Datapublic class WordDTO extends BaseDTO { private Long id; private String uuid; private Long timestamp; private String word;}
问题描述:在Consumer侧给WordDTO赋的值,其id属性的值无法在Provider侧获取到。假设Consumer传的值是:{"id":68,"timestamp":1570928394380,"uuid":"
f774f99f-987c-4506-8ab8-366cd619bb15","word":"hello world"},在Provider拿到的却是:{"timestamp":1570928394380,"uuid":"f774f99f-987c-4506-8ab8-366cd619bb15","word":"hello world"}。
原因分析:dubbo默认采用的是hessian序列化&反序列化方式,JavaDeserializer在获取fileds时,采用了Map去重。但是在读取值时,根据serializer的顺序,对于同名字段,子类的该字段值会被赋值两次,总是被父类的值覆盖,导致子类的字段值丢失。
解决方案:
首先需要说明的是,出现这个问题有一定的条件。如果Provider中的api和自定义Exception定义都是在一个api.jar中,那么是不会有任何问题的。但是如果自定义Exception是在一个单独的比如common.jar包中就会出现这个问题(此时api和model在另一个api.jar中)。
下面是一段调用一个会抛出自定义异常的服务的代码:
try { String hello = demoService.saySomething(wordDTO); System.out.println(hello);}catch (WrongArgumentException e){ System.err.println("wrong argument 1: " + e.getMessage());}catch (RuntimeException e){ System.err.println("wrong argument 2: " + e.getMessage());}
但是,调用的日志却是如下所示,通过日志我们可以发现,在Consumer中并没有捕获到自定义的WrongArgumentException异常,只能捕获到RuntimeException中的异常,且这个异常信息是封装自定义的WrongArgumentException异常:
wrong argument 2: com.afei.dev.maven.exception.WrongArgumentException: word不允许为空com.afei.dev.maven.exception.WrongArgumentException: word不允许为空 at com.afei.test.dubbo.provider.facade.impl.DemoServiceImpl.saySomething(DemoServiceImpl.java:11) at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
这是什么原因呢?这是因为dubbo Provider的ExceptionFilter.java对异常统一封装导致的,其封装的核心源码我就不贴出来了,你如果有兴趣可以自己下载查看。我这里只贴出它的处理逻辑,当碰到如下这些情况时,dubbo会直接抛出异常:
否则,Dubbo通过如下代码将异常包装成RuntimeException抛给客户端:
return new RpcResult(new RuntimeException(StringUtils.toString(exception)));
通过上面对ExceptionFilter的源码分析可知,如果要让Provider抛出自定义异常,有如下几个解决办法:
那么Dubbo为什么这样设计?我相信没有谁比Dubbo的作者梁飞更有发言权了!这里就引用Dubbo作者梁飞在Github上的原话(原话出处:
https://github.com/apache/dubbo/issues/111):
这个是为了防止服务提供方抛出了消费方没有的异常,比如数据库异常类,导致消费方反序列化失败,使异常信息更奇怪,建议在业务接口上RuntimeException也声明在throws中。
在某些复杂环境下,例如Docker、双网卡、虚拟机等环境下,Dubbo默认绑定的IP可能并不是我们期望的正确IP,Dubbo绑定IP默认行为如下(核心源码在NetUtils.java中):
事实上,复杂环境下这个IP绑定问题不太好自动化解决,不过我们可以利用dubbo的扩展能力解决这些问题。
如果你的dubbo部署在Docker上,那么需要注意了。我们需要解决Dubbo几个特定参数来解决这个问题:
假设主机IP地址为30.5.97.6,docker启动dubbo服务参考命令,启动后,这个Provider服务注册的地址就是30.5.97.6:20881,我们可以通过命令(telnet 30.5.97.6 20881,invoke
org.apache.dubbo.test.docker.DemoService.hello("world"))检查并调用Provider提供的服务:
docker run -e DUBBO_IP_TO_REGISTRY=30.5.97.6 -e DUBBO_PORT_TO_REGISTRY=20881 -p 30.5.97.6:20881:20880 --link zkserver:zkserver -it --rm dubbo-docker-sample
参考地址:
https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-docker。
如果你服务的调用方和消费方不在同一个内网中,那么就会希望Dubbo服务通过外网IP暴露。不过不好意思,dubbo默认的服务暴露行为搞不定,因为dubbo默认暴露的是内网IP地址。
这个时候,我们就需要借助两个参数:dubbo.protocol.host和dubbo.protocol.port,通过这两个参数显示申明我们暴露服务的IP和Port,这两个参数即可以通过配置文件方式指定,也可以通过JVM参数方式指定,具体怎么使用,UP TO YOU!!!
当服务器上有多个网卡时,Dubbo服务提供者启动时,会将错误的IP注册到注册中心,从而导致消费端连接不上。这种情况的笔者提供两种解决办法:
这个异常的详细堆栈信息如下所示:
org.apache.dubbo.remoting.transport.ExceedPayloadLimitException:Data length too large: 10356612, max payload: 8388608,channel: NettyChannel [channel=[id: 0xd36132c0, L:/192.168.1.6:55078 - R:/192.168.1.6:20880]]
日志中提到max payload为8388608,等价于8 * 1024 * 1024,即8k。所以这个问题的原因非常清晰了,就是请求或者响应的报文体长度超过了8k。
这个问题比较简单,笔者在这里提供两个解决方案:
说明:
dubbo在小报文的场景下表现最佳,所以,除非确实无法饶过。否则强烈不建议调大payload的值;
dubbo服务Provider侧如果线程耗尽,会跑出类似如下的异常信息:
19-10-17 00:00:00.033 [New I/O server worker #1-6] WARN com.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport - [DUBBO] Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-10.0.0.77:20703, Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 5897697 (completed: 5897197), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in dubbo://10.0.0.77:20703!, dubbo version: 2.5.3, current host: 127.0.0.1
对dubbo有基本了解的都知道,Provider默认是fixed线程池,且线程数为200。那么什么时候会出现这种异常呢:
根据下面这段日志可知,Dubbo线程都阻塞在发送ActiveMQ消息的地方,我们可以通过异步发送MQ消息,或者检查是不是ActiveMQ服务吞吐量不行并优化它的吞吐量等手段来解决:
"DubboServerHandler-127.0.0.1:20880-thread-128" daemon prio=10 tid=0x00007fd574193811 nid=0x16cf1 waiting for monitor entry [0x00007fd691887000..0x00007fd691888810] java.lang.Thread.State: BLOCKED (on object monitor) at org.apache.activemq.transport.MutexTransport.oneway(MutexTransport.java:40) - waiting to lock <0x00007fd6c9fa4ba8> (a java.lang.Object) at org.apache.activemq.transport.ResponseCorrelator.oneway(ResponseCorrelator.java:60) at org.apache.activemq.ActiveMQConnection.doAsyncSendPacket(ActiveMQConnection.java:1265) at org.apache.activemq.ActiveMQConnection.asyncSendPacket(ActiveMQConnection.java:1259)
异常堆栈信息如下所示:
Forbid consumer 0 access service com.afei.dubbo.demo.api.QueryService from registry 127.0.0.1:2181 use dubbo version 2.5.3,Please check registry access list (whitelist/blacklist)
或者异常堆栈信息如下所示:
org.apache.dubbo.rpc.RpcException:Failed to invoke the method saySomething in the service com.afei.test.dubbo.provider.facade.DemoService.No provider available for the service com.afei.test.dubbo.provider.facade.DemoService:2.0.0from registry 224.5.6.7:1234
我相信,每一个使用过dubbo服务的同学,肯定会碰到上面这两个ERROR日志。这两个问题一般有如下几种原因:
在dubbo进入apache之前,dubbo的spring schema申明如下:
http://code.alibabatech.com/schema/dubbo
当dubbo进入apache后,dubbo的spring schema能兼容两种方式:
http://dubbo.apache.org/schema/dubbohttp://code.alibabatech.com/schema/dubbo
这是由dubbo.jar中META-INF/spring.schemas文件决定的:
http\://dubbo.apache.org/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsdhttp\://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/compat/dubbo.xsd
dubbo新版本是即能兼容
http://dubbo.apache.org/schema/dubbo,也能兼容http://code.alibabatech.com/schema/dubbo。但是dubbo老版本只能兼容http://code.alibabatech.com/schema/dubbo。如果老版本也配置http://dubbo.apache.org/schema/dubbo,就会抛出如下日常:
org.springframework.beans.factory.parsing.BeanDefinitionParsingException:Configuration problem: Unable to locate Spring NamespaceHandle
下面问题的场景基于dubbo-2.5.3版本。
如果你对StackOverflowError有一定的了解,就可以知道出现这个问题的主要原因就是调用栈太深,比如常见的无限递归调用。那本文要介绍的Dubbo抛出的这个StackOverflowError又是什么原因呢?且往下看。
重现问题
话不多说,直入主题。这次碰到的StackOverflowError非常好重现,只需要如下简短的代码即可。需要注意的是这里调用的是
com.alibaba.dubbo.common.json.JSON,而不是fastjson中的com.alibaba.fastjson.JSON:
运行这段代码能得到如下异常:
分析原因
由这个异常堆栈信息,我们很容易知道在GenericJSONConverter中的第73行和129行之间出现了无限递归调用,打开dubbo源码并debug,发现在调用GenericJSONConverter中的writeValue()方法时,首先会判断需要序列化的对象的类型。当对象是如下类型时会特殊处理:
如果需要序列化的对象是其他类型,比如这里的Locale类型,序列化逻辑如下所示:
通过这段源码的分析,我们大概可以知道Locale的属性中肯定有Locale类型的属性。由于有Locale类型的属性,导致继续调用GenericJSONConverter中的writeValue()方法,从而无限递归下去,让我们继续Debug源码验证这个猜想。
Debug到String pns[] = w.getPropertyNames();,我们通过查看Locale的属性pns[]可以验证我们前面的猜想,如下图所示。Locale属性availableLocales的类型还是Locale,从而出现死循环直到抛出StackOverflowError:
解决问题
那么如何解决这个问题呢?很简单,不要使用dubbo中的JSON,改为使用fastjson中的JSON,或者jackson和GSON都可以:
Locale locale = Locale.getDefault(); System.out.println(com.alibaba.fastjson.JSON.toJSON(locale)); System.out.println(new com.google.gson.Gson().toJson(locale));
Dubbo Fix
笔者翻看dubbo issue历史,发现dubbo在2018-05-09修复了这个问题,对应的dubbo版本是2.6.3,描述为:add Locale serialize & deserialize support。pull地址如下:
https://github.com/apache/dubbo/pull/1761/commits。
修复的代码片段如下所示,主要改动点有:
链接来自:https://m.toutiaocdn.com/i6750543857512890887/?app=news_article×tamp=1622185266&use_new_style=1&req_id=20210528150105010212070098000B0E76&group_id=6750543857512890887&tt_from=mobile_qq&utm_source=mobile_qq&utm_medium=toutiao_android&utm_campaign=client_share&share_token=35dbf458-75e1-4471-a8d8-6b89a3cf61d5
链接来自:https://m.toutiaocdn.com/i6749487711037424132/?app=news_article×tamp=1622195524&use_new_style=1&req_id=202105281752030102120612013D0106D7&group_id=6749487711037424132&tt_from=mobile_qq&utm_source=mobile_qq&utm_medium=toutiao_android&utm_campaign=client_share&share_token=38f3322b-a840-4ae1-a0aa-102633a32642