dubbo源碼導讀三 dubbo服務引用過程

1. 簡介

上一篇文章詳細分析了服務導出的過程,本篇文章我們趁熱打鐵,繼續分析服務引用過程。在 Dubbo 中,我們可以通過兩種方式引用遠程服務。第一種是使用服務直連的方式引用服務,第二種方式是基於註冊中心進行引用。服務直連的方式僅適合在調試或測試服務的場景下使用,不適合在線上環境使用。因此,本文我將重點分析通過註冊中心引用服務的過程。從註冊中心中獲取服務配置只是服務引用過程中的一環,除此之外,服務消費者還需要經歷 Invoker 創建、代理類創建等步驟。這些步驟,將在後續章節中一一進行分析。

2.服務引用原理

Dubbo 服務引用的時機有兩個,第一個是在 Spring 容器調用 ReferenceBean 的 afterPropertiesSet 方法時引用服務,第二個是在 ReferenceBean 對應的服務被注入到其他類中時引用。這兩個引用服務的時機區別在於,第一個是餓漢式的,第二個是懶漢式的。默認情況下,Dubbo 使用懶漢式引用服務。如果需要使用餓漢式,可通過配置 <dubbo:reference> 的 init 屬性開啓。下面我們按照 Dubbo 默認配置進行分析,整個分析過程從 ReferenceBean 的 getObject 方法開始。當我們的服務被注入到其他類中時,Spring 會第一時間調用 getObject 方法,並由該方法執行服務引用邏輯。按照慣例,在進行具體工作之前,需先進行配置檢查與收集工作。接着根據收集到的信息決定服務用的方式,有三種,第一種是引用本地 (JVM) 服務,第二是通過直連方式引用遠程服務,第三是通過註冊中心引用遠程服務。不管是哪種引用方式,最後都會得到一個 Invoker 實例。如果有多個註冊中心,多個服務提供者,這個時候會得到一組 Invoker 實例,此時需要通過集羣管理類 Cluster 將多個 Invoker 合併成一個實例。合併後的 Invoker 實例已經具備調用本地或遠程服務的能力了,但並不能將此實例暴露給用戶使用,這會對用戶業務代碼造成侵入。此時框架還需要通過代理工廠類 (ProxyFactory) 爲服務接口生成代理類,並讓代理類去調用 Invoker 邏輯。避免了 Dubbo 框架代碼對業務代碼的侵入,同時也讓框架更容易使用。

以上就是服務引用的大致原理,下面我們深入到代碼中,詳細分析服務引用細節。

配置檢查及組裝

組裝過程中有一部是處理點對點調用邏輯,代碼中的url即point-to-point的url.

判斷是injvm調用還是遠程調用

則根據 url 的協議、scope 以及 injvm 等參數檢測是否需要本地引用

比如如果用戶顯式配置了 scope=local,或者consumer和provider同時在一個jvm中啓動, 此時 isInjvmRefer 返回 true

組裝註冊中心或者直連服務的urls

如果配置了多個註冊中心或者明確配置了多個提供者,則最終會得到多個url。

生成Invoker實例

DubboProtocol的refer方法

最核心的是getClients方法,這個方法最終會創建一個客戶端,默認是nettyClient

最終生成DubboInvoker,底層通過nettyClient通信

RegistryProtocol的refer方法

註冊consumerURL到註冊中心,並且訂閱/dubbo/#{serviceName}/providers、configurators、routers節點.

RegistryDirectory 是一種動態服務目錄,實現了 NotifyListener 接口。當註冊中心服務配置發生變化後,RegistryDirectory 可收到與當前服務相關的變化。收到變更通知後,RegistryDirectory 可根據配置變更信息刷新 Invoker 列表。RegistryDirectory 中有幾個比較重要的邏輯,第一是 Invoker 的列舉邏輯,第二是接收服務配置變更的邏輯,第三是 Invoker 列表的刷新邏輯。

RegistryDirectory的服務刷新及路由

Cluster

爲了避免單點故障,現在的應用通常至少會部署在兩臺服務器上。對於一些負載比較高的服務,會部署更多的服務器。這樣,在同一環境下的服務提供者數量會大於1。對於服務消費者來說,同一環境下出現了多個服務提供者。這時會出現一個問題,服務消費者需要決定選擇哪個服務提供者進行調用。另外服務調用失敗時的處理措施也是需要考慮的,是重試呢,還是拋出異常,亦或是隻打印異常等。爲了處理這些問題,Dubbo 定義了集羣接口 Cluster 以及 Cluster Invoker。集羣 Cluster 用途是將多個服務提供者合併爲一個 Cluster Invoker,並將這個 Invoker 暴露給服務消費者。這樣一來,服務消費者只需通過這個 Invoker 進行遠程調用即可,至於具體調用哪個服務提供者,以及調用失敗後如何處理等問題,現在都交給集羣模塊去處理。集羣模塊是服務提供者和服務消費者的中間層,爲服務消費者屏蔽了服務提供者的情況,這樣服務消費者就可以專心處理遠程調用相關事宜。比如發請求,接受服務提供者返回的數據等。這就是集羣的作用。

Dubbo 提供了多種集羣實現,包含但不限於 Failover Cluster、Failfast Cluster 和 Failsafe Cluster 等。每種集羣實現類的用途不同,接下來會一一進行分析。

2. 集羣容錯

在對集羣相關代碼進行分析之前,這裏有必要先來介紹一下集羣容錯的所有組件。包含 Cluster、Cluster Invoker、Directory、Router 和 LoadBalance 等。

集羣工作過程可分爲兩個階段,第一個階段是在服務消費者初始化期間,集羣 Cluster 實現類爲服務消費者創建 Cluster Invoker 實例,即上圖中的 merge 操作。第二個階段是在服務消費者進行遠程調用時。以 FailoverClusterInvoker 爲例,該類型 Cluster Invoker 首先會調用 Directory 的 list 方法列舉 Invoker 列表(可將 Invoker 簡單理解爲服務提供者)。Directory 的用途是保存 Invoker,可簡單類比爲 List<Invoker>。其實現類 RegistryDirectory 是一個動態服務目錄,可感知註冊中心配置的變化,它所持有的 Invoker 列表會隨着註冊中心內容的變化而變化。每次變化後,RegistryDirectory 會動態增刪 Invoker,並調用 Router 的 route 方法進行路由,過濾掉不符合路由規則的 Invoker。當 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表後,它會通過 LoadBalance 從 Invoker 列表中選擇一個 Invoker。最後 FailoverClusterInvoker 會將參數傳給 LoadBalance 選擇出的 Invoker 實例的 invoke 方法,進行真正的遠程調用。

以上就是集羣工作的整個流程,這裏並沒介紹集羣是如何容錯的。Dubbo 主要提供了這樣幾種容錯方式:

  • Failover Cluster - 失敗自動切換
  • Failfast Cluster - 快速失敗
  • Failsafe Cluster - 失敗安全
  • Failback Cluster - 失敗自動恢復
  • Forking Cluster - 並行調用多個服務提供者

我們在上一章看到了兩個概念,分別是集羣接口 Cluster 和 Cluster Invoker,這兩者是不同的。Cluster 是接口,而 Cluster Invoker 是一種 Invoker。服務提供者的選擇邏輯,以及遠程調用失敗後的的處理邏輯均是封裝在 Cluster Invoker 中。那麼 Cluster 接口和相關實現類有什麼用呢?用途比較簡單,僅用於生成 Cluster Invoker。下面我們來看一下源碼。

public class FailoverCluster implements Cluster {

    public final static String NAME = "failover";

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        // 創建並返回 FailoverClusterInvoker 對象
        return new FailoverClusterInvoker<T>(directory);
    }
}

如上,FailoverCluster 總共就包含這幾行代碼,用於創建 FailoverClusterInvoker 對象,很簡單。下面再看一個。

public class FailbackCluster implements Cluster {

    public final static String NAME = "failback";

    @Override
    public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
        // 創建並返回 FailbackClusterInvoker 對象
        return new FailbackClusterInvoker<T>(directory);
    }

}

我們首先從各種 Cluster Invoker 的父類 AbstractClusterInvoker 源碼開始說起。前面說過,集羣工作過程可分爲兩個階段,第一個階段是在服務消費者初始化期間,這個在服務引用那篇文章中分析過,就不贅述。第二個階段是在服務消費者進行遠程調用時,此時 AbstractClusterInvoker 的 invoke 方法會被調用。列舉 Invoker,負載均衡等操作均會在此階段被執行。因此下面先來看一下 invoke 方法的邏輯。

public Result invoke(final Invocation invocation) throws RpcException {
    checkWhetherDestroyed();
    LoadBalance loadbalance = null;

    // 綁定 attachments 到 invocation 中.
    Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
    if (contextAttachments != null && contextAttachments.size() != 0) {
        ((RpcInvocation) invocation).addAttachments(contextAttachments);
    }

    // 列舉 Invoker
    List<Invoker<T>> invokers = list(invocation);
    if (invokers != null && !invokers.isEmpty()) {
        // 加載 LoadBalance
        loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
                .getMethodParameter(RpcUtils.getMethodName(invocation), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
    }
    RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
    
    // 調用 doInvoke 進行後續操作
    return doInvoke(invocation, invokers, loadbalance);
}

// 抽象方法,由子類實現
protected abstract Result doInvoke(Invocation invocation, List<Invoker<T>> invokers,
                                       LoadBalance loadbalance) throws RpcException;

AbstractClusterInvoker 的 invoke 方法主要用於列舉 Invoker,以及加載 LoadBalance。最後再調用模板方法 doInvoke 進行後續操作。下面我們來看一下 Invoker 列舉方法 list(Invocation) 的邏輯,如下:

protected List<Invoker<T>> list(Invocation invocation) throws RpcException {
    // 調用 Directory 的 list 方法列舉 Invoker
    List<Invoker<T>> invokers = directory.list(invocation);
    return invokers;
}

如上,AbstractClusterInvoker 中的 list 方法做的事情很簡單,只是簡單的調用了 Directory 的 list 方法,沒有其他更多的邏輯了。Directory 即相關實現類在前文已經分析過,這裏就不多說了。接下來,我們把目光轉移到 AbstractClusterInvoker 的各種實現類上,來看一下這些實現類是如何實現 doInvoke 方法邏輯的。

3.2.1 FailoverClusterInvoker

FailoverClusterInvoker 在調用失敗時,會自動切換 Invoker 進行重試。默認配置下,Dubbo 會使用這個類作爲缺省 Cluster Invoker。下面來看一下該類的邏輯。

public class FailoverClusterInvoker<T> extends AbstractClusterInvoker<T> {

    // 省略部分代碼

    @Override
    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        List<Invoker<T>> copyinvokers = invokers;
        checkInvokers(copyinvokers, invocation);
        // 獲取重試次數
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        if (len <= 0) {
            len = 1;
        }
        RpcException le = null;
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size());
        Set<String> providers = new HashSet<String>(len);
        // 循環調用,失敗重試
        for (int i = 0; i < len; i++) {
            if (i > 0) {
                checkWhetherDestroyed();
                // 在進行重試前重新列舉 Invoker,這樣做的好處是,如果某個服務掛了,
                // 通過調用 list 可得到最新可用的 Invoker 列表
                copyinvokers = list(invocation);
                // 對 copyinvokers 進行判空檢查
                checkInvokers(copyinvokers, invocation);
            }

            // 通過負載均衡選擇 Invoker
            Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
            // 添加到 invoker 到 invoked 列表中
            invoked.add(invoker);
            // 設置 invoked 到 RPC 上下文中
            RpcContext.getContext().setInvokers((List) invoked);
            try {
                // 調用目標 Invoker 的 invoke 方法
                Result result = invoker.invoke(invocation);
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) {
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        
        // 若重試失敗,則拋出異常
        throw new RpcException(..., "Failed to invoke the method ...");
    }
}

如上,FailoverClusterInvoker 的 doInvoke 方法首先是獲取重試次數,然後根據重試次數進行循環調用,失敗後進行重試。在 for 循環內,首先是通過負載均衡組件選擇一個 Invoker,然後再通過這個 Invoker 的 invoke 方法進行遠程調用。如果失敗了,記錄下異常,並進行重試。重試時會再次調用父類的 list 方法列舉 Invoker。整個流程大致如此,不是很難理解。

負載均衡

LoadBalance 中文意思爲負載均衡,它的職責是將網絡請求,或者其他形式的負載“均攤”到不同的機器上。避免集羣中部分服務器壓力過大,而另一些服務器比較空閒的情況。通過負載均衡,可以讓每臺服務器獲取到適合自己處理能力的負載。在爲高負載服務器分流的同時,還可以避免資源浪費,一舉兩得。負載均衡可分爲軟件負載均衡和硬件負載均衡。在我們日常開發中,一般很難接觸到硬件負載均衡。但軟件負載均衡還是可以接觸到的,比如 Nginx。在 Dubbo 中,也有負載均衡的概念和相應的實現。Dubbo 需要對服務消費者的調用請求進行分配,避免少數服務提供者負載過大。服務提供者負載過大,會導致部分請求超時。因此將負載均衡到每個服務提供者上,是非常必要的。Dubbo 提供了4種負載均衡實現,分別是基於權重隨機算法的 RandomLoadBalance、基於最少活躍調用數算法的 LeastActiveLoadBalance、基於 hash 一致性的 ConsistentHashLoadBalance,以及基於加權輪詢算法的 RoundRobinLoadBalance。

服務調用過程

在前面的文章中,我們分析了 Dubbo SPI、服務導出與引入、以及集羣容錯方面的代碼。經過前文的鋪墊,本篇文章我們終於可以分析服務調用過程了。Dubbo 服務調用過程比較複雜,包含衆多步驟,比如發送請求、編解碼、服務降級、過濾器鏈處理、序列化、線程派發以及響應請求等步驟。限於篇幅原因,本篇文章無法對所有的步驟一一進行分析。本篇文章將會重點分析請求的發送與接收、編解碼、線程派發以及響應的發送與接收等過程,至於服務降級、過濾器鏈和序列化大家自行進行分析,也可以將其當成一個黑盒,暫時忽略也沒關係。介紹完本篇文章要分析的內容,接下來我們進入正題吧。

2. 源碼分析

在進行源碼分析之前,我們先來通過一張圖瞭解 Dubbo 服務調用過程。

首先服務消費者通過代理對象 Proxy 發起遠程調用,接着通過網絡客戶端 Client 將編碼後的請求發送給服務提供方的網絡層上,也就是 Server。Server 在收到請求後,首先要做的事情是對數據包進行解碼。然後將解碼後的請求發送至分發器 Dispatcher,再由分發器將請求派發到指定的線程池上,最後由線程池調用具體的服務。這就是一個遠程調用請求的發送與接收過程。至於響應的發送與接收過程,這張圖中沒有表現出來。對於這兩個過程,我們也會進行詳細分析。

Dubbo 將底層通信框架中接收請求的線程稱爲 IO 線程。如果一些事件處理邏輯可以很快執行完,比如只在內存打一個標記,此時直接在 IO 線程上執行該段邏輯即可。但如果事件的處理邏輯比較耗時,比如該段邏輯會發起數據庫查詢或者 HTTP 請求。此時我們就不應該讓事件處理邏輯在 IO 線程上執行,而是應該派發到線程池中去執行。原因也很簡單,IO 線程主要用於接收請求,如果 IO 線程被佔滿,將導致它不能接收新的請求。

以上就是線程派發的背景,下面我們再來通過 Dubbo 調用圖,看一下線程派發器所處的位置。

如上圖,紅框中的 Dispatcher 就是線程派發器。需要說明的是,Dispatcher 真實的職責創建具有線程派發能力的 ChannelHandler,比如 AllChannelHandler、MessageOnlyChannelHandler 和 ExecutionChannelHandler 等,其本身並不具備線程派發能力。Dubbo 支持 5 種不同的線程派發策略,下面通過一個表格列舉一下。

策略 用途
all 所有消息都派發到線程池,包括請求,響應,連接事件,斷開事件等
direct 所有消息都不派發到線程池,全部在 IO 線程上直接執行
message 只有請求響應消息派發到線程池,其它消息均在 IO 線程上執行
execution 只有請求消息派發到線程池,不含響應。其它消息均在 IO 線程上執行
connection 在 IO 線程上,將連接斷開事件放入隊列,有序逐個執行,其它消息派發到線程池

默認配置下,Dubbo 使用 all 派發策略,即將所有的消息都派發到線程池中。

2.1 服務調用方式

Dubbo 支持同步和異步兩種調用方式,其中異步調用還可細分爲“有返回值”的異步調用和“無返回值”的異步調用。所謂“無返回值”異步調用是指服務消費方只管調用,但不關心調用結果,此時 Dubbo 會直接返回一個空的 RpcResult。若要使用異步特性,需要服務消費方手動進行配置。默認情況下,Dubbo 使用同步調用方式。

獲取對應的exporter的步驟

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章