我發(fā)現(xiàn)一個關于Dubbo服務調用的Bug
理論知識
結合我對Dubbo的理解,通常dubbo調用出現(xiàn) No provider available for the service xxx,其原因通常如下:
1.服務方未啟動。
2.代碼內客戶端和服務端的group、version不匹配。
3.有dubbo tag路由過濾,標簽不匹配。
4.動態(tài)配置過濾,沒有匹配的服務(比如disable等)。
但這次遇到一個非以上問題,因此研究了一番,發(fā)現(xiàn)了dubbo在實現(xiàn)上有一些瑕疵。
背景
在做JT808協(xié)議指令數(shù)據上行指令,指令通過808采集平臺(netty長連接),解析后,通過dubbo調用服務,做指令的業(yè)務邏輯處理,奇怪是服務存在,但是卻報錯No provider available for the service com.xxx.ioc.api.service.JTService,錯誤截圖如下:
我覺得很奇怪,服務明明是啟動的,也沒有動態(tài)配置,為什么服務竟然會很奇怪的找不到呢?然后debug下看了代碼,發(fā)現(xiàn)是dubbo編碼階段報錯。
io.netty.handler.codec.EncoderException: java.lang.RuntimeException: Serialized class com.xxx.ioc.codec.util.KeyValuePair must implement java.io.Serializable Java field: private com.xxx.ioc.codec.util.KeyValuePair com.xxx.ioc.protocol.t808.T0900.message.
原來是參數(shù)T009的內部類KeyValuePair未實現(xiàn)序列化導致,但是如果是未實現(xiàn)序列化,應該報錯Serialized class xxx must implement java.io.Serializable的錯誤,但是為什么收到的錯誤卻是No provider available for the service xxx,帶著這個問題,分析一波。
分析過程
調用鏈路根據之前自己分析的dubbo transport層記錄,dubbo客戶端調用時序圖如下(可以參考鏈接的泳道圖):
dubbo客戶端的調用的基本流程說明如下:
- 客戶端經netty pipeline的TailContext處理,業(yè)務線程切換到reactor IO線程,業(yè)務線程在DefaultFuture.get()阻塞等待響應。
- dubbo的編碼/解碼是在reactor IO線程處理,編碼拋出異常,消息不會發(fā)送給服務方,此時異常(錯誤碼BAD_REQUEST)被被封裝為Response,繼而喚醒業(yè)務線程在DefaultFuture.get()阻塞等待。
這里有個疑問,編碼失敗,那么是如何返回響應消息的呢?后面下篇文章分析),在執(zhí)行com.alibaba.dubbo.remoting.exchange.support.DefaultFuture#returnFromResponse,把異常信息封裝為RemotingException進行拋出,代碼如下:
private Object returnFromResponse() throws RemotingException {
Response res = response;
if (res == null) {
throw new IllegalStateException("response cannot be null");
}
if (res.getStatus() == Response.OK) {
return res.getResult();
}
if (res.getStatus() == Response.CLIENT_TIMEOUT || res.getStatus() == Response.SERVER_TIMEOUT) {
throw new TimeoutException(res.getStatus() == Response.SERVER_TIMEOUT, channel, res.getErrorMessage());
}
throw new RemotingException(channel, res.getErrorMessage());//序列化異常(錯誤碼BAD_REQUEST)被封裝為RemotingException向上拋出
}
重點關注一下調用鏈中的異常處理:
- 在com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker#doInvoke內異常被catch,異常信息被封裝為RpcException(異常code=NETWORK_EXCEPTION)向上拋。
- 接著在com.alibaba.dubbo.rpc.protocol.AbstractInvoker#invoke內異常RpcException被catch,由于異常code=NETWORK_EXCEPTION,非業(yè)務異常代碼,因此異常繼續(xù)向上拋。
- 最后異常RpcException在com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoke內被catch,由于是failover失效轉移策略默認重試2次,因此接著嘗試去調用調用其它節(jié)點,如果服務的節(jié)點數(shù)少于重試的次數(shù)+1(即3次),則沒有匹配的服務節(jié)點,因此在com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker#checkInvokers操作內,會報錯No provider available for the service xxx.
詳細的代碼調用截圖如下所示:
因此就淹沒了序列化異常,導致真正的異常失真。這也是dubbo錯誤提示的一點小問題,如果要修復,解決方法也簡單:FailoverClusterInvoker新增如下方法:
private void checkInvokers(List<Invoker<T>> invokers, Invocation invocation, RpcException le) {
if (invokers == null || invokers.isEmpty()) {
if (le != null) {
throw le;
}
checkInvokers(invokers, invocation);//請求父類
}
}
同時修改方法doInvoke如下: