1. 程式人生 > >dubbo非同步呼叫的bug

dubbo非同步呼叫的bug

現象
現有3個服務,關係如下,serviceA非同步呼叫serviceB,serviceB同步呼叫serviceC。其中serviceB暴露出的介面為非同步方式。表現的現象為,serviceB每次呼叫serviceC時,第一次的返回結果為null,後面幾次呼叫時均能正常返回結果。

問題排查
專案中對於所有的dubbo呼叫均有記錄日誌,每次呼叫主要包含2條日誌,CS和CR日誌。CS為consumer send,開始呼叫時日誌。CR為接收到provider的應答日誌。 
通過觀察日誌發現。呼叫serviceC的日誌有不合理的地方。在正常的同步呼叫中,如有多次呼叫,日誌現實順序為CS,CR,CS,CR。實際出現的日誌順序為CS,CS,CR,CR。初步懷疑第一次返回為null是由於非同步呼叫所致。 
通過dubbo admin檢視介面的配置資訊,發現引數均無異常。並且在serviceC端的provider上觀察到每次呼叫均有返回資料。所以排除了serviceC的provider的問題。 
再次編寫testcase單獨測試serviceC服務,發現testcase返回資料均正常。也證實了serviceC服務沒有問題。 
最後通過詢問是否有非同步配置時,發現serviceB暴露的介面是非同步方式。修改serviceB為同步方式,重新測試,發現問題解決。每次呼叫均正常接收到結果。調整serviceB為非同步,問題重現。 
最終確認了問題發生場景,即serviceB為非同步服務,在serviceB裡面同步呼叫serviceC,最終表現為serivceC的呼叫是非同步,導致問題產生。

程式碼分析
dubbo的provider和consumer均由Invoker演變而來。並且都是AbstractInvoker的實現類。 
AbstractInvoker
Map<String, String> context = RpcContext.getContext().getAttachments();
        if (context != null) {
            // A點
            invocation.addAttachmentsIfAbsent(context);
        }
        // B點
        if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)){
            invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
        }

DubboInvoker

// C點
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
....
if (isAsync) {
                ResponseFuture future = currentClient.request(inv, timeout) ;
                RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
                // D點
                return new RpcResult();
                }

 

非同步呼叫serviceB時,此時B點的判斷為true,會在attachment中設定async的值為true,RpcContext中設定了async的值。
隨後呼叫serviceB的實際方法。serviceB呼叫serviceC是,程式碼在A點,會將RpcContext的值設定到當前的invocation中
由於RpcContext是採用ThreadLocal儲存的資料,並且在第一步時設定了async值,導致在2方法執行後,invocation中的async的值為true。
在進行serviceC的遠端呼叫時,由於invocation中async值為true,導致C點判斷為非同步呼叫,在D點時new了一個RpcResult。所以在第一次呼叫serviceC時,返回結果為null。
第一次呼叫serviceC結束時,在consumer的filter chain中,有一個ConsumerContextFilter,在呼叫結束後會執行RpcContext.getContext().clearAttachments()方法,清除RpcContext中的資訊,也就清除了async標識。
第二次呼叫serviceC時,由於RpcContext中沒有了async標識,判斷為同步呼叫,所以會正常返回結果。
 

解決方式
分析了問題產生的原因後,在不修改dubbo原始碼的情況,可以有一下幾種處理方式。 
1. 將serviceB改為同步呼叫,如果業務上確實需要非同步呼叫,有以下2種處理方式 
* serviceB的方法無需返回值,可採用oneway的方式 
* 有返回值,並且需要非同步,最簡單的方式為在實現中使用執行緒池執行業務。 
2. 增加一個Provider端的Filter,保證在filter鏈的結尾,在執行方法前,清除attachment中的async標誌。也可達到同樣的效果。
   或寫個dubbo介面的重新封裝,再加個springboot的async標識,代理非同步。