如果你用過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的順序,對於同名字段,子類的該字段值會被賦值兩次,總是被父類的值覆蓋,導致子類的字段值丟失。
解決方案:
- 更改序列化方式(不建議);
- 刪掉子類中與父類同名屬性(建議);
自定義異常被包裝成RuntimeException
首先需要說明的是,出現這個問題有一定的條件。如果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會直接拋出異常:
- 如果是checked異常(不是RuntimeException但是是Exception.java類型的異常),直接拋出;
- 在方法簽名上有聲明(例如String saySomething()throws MyException ),直接拋出;
- 異常類和接口類在同一jar包里,直接拋出;
- 是JDK自帶的異常(全類名以java或者javax開頭,例如java.lang.IllegalStateException),直接拋出;
- 是Dubbo本身的異常RpcException,直接拋出;
否則,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中。
IP暴露問題
在某些復雜環境下,例如Docker、雙網卡、虛擬機等環境下,Dubbo默認綁定的IP可能並不是我們期望的正確IP,Dubbo綁定IP默認行為如下(核心源碼在NetUtils.java中):
- 通過InetAddress.getLocalHost()獲取本機地址,如果本機地址有效則返回(有效地址需要滿足這幾點:1. 不能為空,2. 不是loopback地址(類似127.x.x.x),3. 不能是0.0.0.0,也不能是127.0.0.1);
- 如果本機地址無效,那么再遍歷網卡地址,然后通過isValidAddress校驗ip是否正常並返回第一個有效的IP地址。這樣的話,Dubbo就不能保證返回的是內網IP還是外網IP。
事實上,復雜環境下這個IP綁定問題不太好自動化解決,不過我們可以利用dubbo的擴展能力解決這些問題。
- Docker環境
如果你的dubbo部署在Docker上,那么需要注意了。我們需要解決Dubbo幾個特定參數來解決這個問題:
- DUBBO_IP_TO_REGISTRY --- Registering to the IP address of the registration center
- DUBBO_PORT_TO_REGISTRY --- Registering to the port of the registration center
- DUBBO_IP_TO_BIND --- Listening IP addresses
- DUBBO_PORT_TO_BIND --- Listening ports
假設主機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。
- 暴露外網IP
如果你服務的調用方和消費方不在同一個內網中,那么就會希望Dubbo服務通過外網IP暴露。不過不好意思,dubbo默認的服務暴露行為搞不定,因為dubbo默認暴露的是內網IP地址。
這個時候,我們就需要借助兩個參數:dubbo.protocol.host和dubbo.protocol.port,通過這兩個參數顯示申明我們暴露服務的IP和Port,這兩個參數即可以通過配置文件方式指定,也可以通過JVM參數方式指定,具體怎么使用,UP TO YOU!!!
- 雙網卡問題
當服務器上有多個網卡時,Dubbo服務提供者啟動時,會將錯誤的IP注冊到注冊中心,從而導致消費端連接不上。這種情況的筆者提供兩種解決辦法:
- 配置dubbo.protocol.host=192.168.0.1
- 配置/etc/hosts,例如afeiserver01 = 192.168.0.1,其中afeiserver01是機器名;
Data length too large
這個異常的詳細堆棧信息如下所示:
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。
這個問題比較簡單,筆者在這里提供兩個解決方案:
- 修改payload的值,將其調大,例如16777216,即16k,不推薦;
- 減少請求/響應報文長度。例如Provider提供的服務,最大批量限制為1000,比如最多只能批量查詢1000個用戶ID的用戶信息,推薦;
說明:
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。那么什么時候會出現這種異常呢:
- Provider側接口處理太慢。如果是這種原因的話,我們可以通過jstack命令,在線程棧中看到超過200個狀態為RUNNING、且命名為"DubboServerHandler-"的線程,通過線程棧我們大概知道是哪部分代碼引起的,然后優化問題代碼,提升處理能力從而解決這個問題;
- Provider處理能力確實不夠。這個原因是指,Consumer可能會達到10000TPS,但是Provider單機處理能力可能只有1000TPS,如果沒有10台以上的Provider服務實例,那么就確實需要擴容了。
- Provier由於某些原因阻塞。這個原因一般是Provider側依賴的某些服務或者中間件出問題導致的;
根據下面這段日志可知,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日志。這兩個問題一般有如下幾種原因:
- Provider服務全部下線,即沒有一個存活的Provider服務進程。
- 存在一個或多個Provider服務,但是version或者group不匹配。例如Consumer側申明version=1.0.0,而Provider側申明version=2.0.0,或者group不匹配,都會出現這個ERROR。
- 暴露的IP有問題。例如暴露的是內網IP,但是調用卻是通過外網IP;
dubbo spring schema
在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()方法時,首先會判斷需要序列化的對象的類型。當對象是如下類型時會特殊處理:
- 原生類型或者封裝類型;
- JSONNode類型;
- 枚舉;
- 數組;
- Map;
- 集合類型;
如果需要序列化的對象是其他類型,比如這里的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。
修復的代碼片段如下所示,主要改動點有:
- 如果序列化對象是Locale類型,那么序列化方式就是調用toString()方法;
- 如果反序列化目標對象類型是Locale,那么將value以下划線分割,然后構造Locale對象,用法參考:JSON.parse("zhCN", Locale.class);