踩坑系列之--dubbo異步調用傳遞性致使嵌套調用返回null值的bug

1、現象

有三個應用serviceA,serviceB,serviceC,在確保消費沒有錯亂的前提下(都只有單個服務提供者),指望其調用關係爲java

sequenceDiagram 
    serviceA-->>serviceB:serviceA 異步調用serviceB
    serviceB->>serviceC:serviceB 同步調用serviceC
    serviceC->>serviceB:同步返回調用結果true

serviceA dubbo消費serviceB 配置爲異步消費async="true",配置以下:異步

// serviceA 異步消費serviceB 配置以下
<dubbo:reference id="serviceB" interface="cn.ServiceB"
                     version="1.0.0" check="false">
        <dubbo:method name="reRunJob" async="true"/>
    </dubbo:reference>
// serviceB 同步消費serviceC配置以下
<dubbo:reference id="serviceC" interface="cn.ServiceC"
                     version="1.0.0" check="false">
        <dubbo:method name="xxx"/>
    </dubbo:reference>

然而,上面配置後,實際調用關係變爲下圖async

sequenceDiagram 
    serviceA-->>serviceB:serviceA 異步調用serviceB
    serviceB-->>serviceC:serviceB 異步調用serviceC
    serviceC->>serviceB: 返回null,致使指望值boolean永遠爲false
如上所述,因爲B->C 因爲dubbo異步配置的傳遞性,致使變爲了異步調用,結果返回了null,致使指望的同步調用結果異常, 可是B第二次調用C會正常返回

2、尋找問題根源--源碼

1. 咱們的排查思路

現象是因爲dubbo異步調用,而後服務提供者內部又有dubbo嵌套調用,==因此咱們須要找出dubbo的內部嵌套調用是否存在異步傳遞性==,那麼既然是傳遞,就須要上下文環境,進而,咱們想到了dubbo中的上下文環境==RpcContext==,咱們推測是由此類傳遞了異步參數ide

2. 預備知識:RpcContext簡介

RpcContext 是一個 ThreadLocal 的臨時狀態記錄器,當接收到 RPC 請求,或發起 RPC 請求時,RpcContext 的狀態都會變化。 <br/>
好比:A調B,B再調C,則B機器上,在B調C以前,==RpcContext記錄的是A調B的信息==,在B調C以後,RpcContext記錄的是B調C的信息。

咱們知道dubbo的方法調用,都是由invoker代理調用的,咱們找到AbstractInvoker,查看底層的invoke方法,源碼以下:.net

public Result invoke(Invocation inv) throws RpcException {
        ··· 省略無關代碼
        
        // 這裏的代碼在此處對咱們的serviceB-->serviceC時,會取出RpcContext,語句上面的RpcContext知識儲備,咱們知道,這裏的context存的是serviceA->serviceB的上下文,也就是說是異步的,到這裏謎團解開
        Map<String, String> context = RpcContext.getContext().getAttachments();
        if (context != null) {
            invocation.addAttachmentsIfAbsent(context);
        }
        // 注意此處代碼,若是提供者方法配置了異步參數
        if (getUrl().getMethodParameter(invocation.getMethodName(), Constants.ASYNC_KEY, false)){
        // 會將異步參數值設置到當前調用對象中    
        invocation.setAttachment(Constants.ASYNC_KEY, Boolean.TRUE.toString());
        }
        // 最後再將異步參數存入上下文
        RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
        
        
        try {
            return doInvoke(invocation);
        } catch (InvocationTargetException e) {     ···
        } catch (RpcException e) {
            ···
        } catch (Throwable e) {
            ···
        }
    }

3. 上面還有個小問題,serviceB第二次調用serviceC的某個方法,會正常返回,這又是爲何呢?

這裏涉及到一個Filter ConsumerContextFilter 源碼以下:線程

@Activate(group = Constants.CONSUMER, order = -10000)
public class ConsumerContextFilter implements Filter {

    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext.getContext()
                .setInvoker(invoker)
                .setInvocation(invocation)
                .setLocalAddress(NetUtils.getLocalHost(), 0)
                .setRemoteAddress(invoker.getUrl().getHost(), 
                                  invoker.getUrl().getPort());
        if (invocation instanceof RpcInvocation) {
            ((RpcInvocation)invocation).setInvoker(invoker);
        }
        try {
            return invoker.invoke(invocation);
        } finally {
           // ①注意這裏 ,進行了上下文清理
          RpcContext.getContext().clearAttachments();
        }
    }

}

==如上代碼,serviceB第一次調用serviceC結束時,在consumer的filter chain中,有一個ConsumerContextFilter,在調用結束後會執行RpcContext.getContext().clearAttachments()方法,清除RpcContext中的信息,也就清除了async標識==,因此,第二次調用serviceC,就正常==同步==調用了,至此,咱們的疑問獲得解釋代理

3、解決方法

分析了問題產生的緣由後,在不修改dubbo源碼的狀況,能夠有一下幾種處理方式。code

  1. 將serviceB改成同步調用,若是業務上確實須要異步調用,有如下2種處理方式
  2. serviceB的方法無需返回值,可採用oneway的方式(在消費者端配置dubbo:method中return="false")
  3. 有返回值,而且須要異步,最簡單的方式爲在實現中使用線程池執行業務。
  4. 增長一個Provider端的Filter,保證在filter鏈的結尾,在執行方法前,清除attachment中的async標誌。也可達到一樣的效果

轉載請註明出處 阿布的夏天xml

相關文章
相關標籤/搜索