1. 程式人生 > >叢集容錯之 Directory

叢集容錯之 Directory

1. 簡介

前面文章分析了服務的匯出與引用過程,從本篇文章開始,我將開始分析 Dubbo 叢集容錯方面的原始碼。這部分原始碼包含四個部分,分別是服務目錄 Directory、服務路由 Router、叢集 Cluster 和負載均衡 LoadBalance。這幾個部分的原始碼邏輯比較獨立,我會分四篇文章進行分析。本篇文章作為叢集容錯的開篇文章,將和大家一起分析服務目錄相關的原始碼。在進行深入分析之前,我們先來了解一下服務目錄是什麼。服務目錄中儲存了一些和服務提供者有關的資訊,通過服務目錄,服務消費者可獲取到服務提供者的資訊,比如 ip、埠、服務協議等。通過這些資訊,服務消費者就可通過 Netty 等客戶端進行遠端呼叫。在一個服務叢集中,服務提供者數量並不是一成不變的,如果叢集中新增了一臺機器,相應地在服務目錄中就要新增一條服務提供者記錄。或者,如果服務提供者的配置修改了,服務目錄中的記錄也要做相應的更新。如果這樣說,服務目錄和註冊中心的功能不就雷同了嗎。確實如此,這裡這麼說是為了方便大家理解。實際上服務目錄在獲取註冊中心的服務配置資訊後,會為每條配置資訊生成一個 Invoker 物件,並把這個 Invoker 物件儲存起來,這個 Invoker 才是服務目錄最終持有的物件。Invoker 有什麼用呢?看名字就知道了,這是一個具有遠端呼叫功能的物件。講到這大家應該知道了什麼是服務目錄了,它可以看做是 Invoker 集合,且這個集合中的元素會隨註冊中心的變化而進行動態調整。

好了,關於服務目錄這裡就先介紹這些,大家先有個大致印象即可。接下來我們通過繼承體系圖來了解一下服務目錄的家族成員都有哪些。

2. 繼承體系

服務目錄目前內建的實現有兩個,分別為 StaticDirectory 和 RegistryDirectory,它們均是 AbstractDirectory 的子類。AbstractDirectory 實現了 Directory 介面,這個介面包含了一個重要的方法定義,即 list(Invocation),用於列舉 Invoker。下面我們來看一下他們的繼承體系圖。

如上,Directory 繼承自 Node 介面,Node 這個介面繼承者比較多,像 Registry、Monitor、Invoker 等繼承了這個介面。這個介面包含了一個獲取配置資訊的方法 getUrl,實現該介面的類可以向外提供配置資訊。另外,大家注意看 RegistryDirectory 實現了 NotifyListener 介面,當註冊中心節點資訊發生變化後,RegistryDirectory 可以通過此介面方法得到變更資訊,並根據變更資訊動態調整內部 Invoker 列表。

現在大家對服務目錄的繼承體系應該比較清楚了,下面我們深入到原始碼中,探索服務目錄是如何實現的。

3. 原始碼分析

本章我將分析 AbstractDirectory 和它兩個子類的原始碼。這裡之所以要分析 AbstractDirectory,而不是直接分析子類是有一定原因的。AbstractDirectory 封裝了 Invoker 列舉流程,具體的列舉邏輯則由子類實現,這是典型的模板模式。所以,接下來我們先來看一下 AbstractDirectory 的原始碼。

public List<Invoker<T>> list(Invocation invocation) throws RpcException {
    if (destroyed) {
        throw new RpcException("Directory already destroyed...");
    }
    
    // 呼叫 doList 方法列舉 Invoker,這裡的 doList 是模板方法,由子類實現
    List<Invoker<T>> invokers = doList(invocation);
    
    // 獲取路由器
    List<Router> localRouters = this.routers;
    if (localRouters != null && !localRouters.isEmpty()) {
        for (Router router : localRouters) {
            try {
                // 獲取 runtime 引數,並根據引數決定是否進行路由
                if (router.getUrl() == null || router.getUrl().getParameter(Constants.RUNTIME_KEY, false)) {
                    // 進行服務路由
                    invokers = router.route(invokers, getConsumerUrl(), invocation);
                }
            } catch (Throwable t) {
                logger.error("Failed to execute router: ...");
            }
        }
    }
    return invokers;
}

// 模板方法,由子類實現
protected abstract List<Invoker<T>> doList(Invocation invocation) throws RpcException;

上面就是 AbstractDirectory 的 list 方法原始碼,這個方法封裝了 Invoker 的列舉過程。如下:

  1. 呼叫 doList 獲取 Invoker 列表
  2. 根據 Router 的 getUrl 返回值為空與否,以及 runtime 引數決定是否進行服務路由

以上步驟中,doList 是模板方法,需由子類實現。Router 的 runtime 引數這裡簡單說明一下,這個引數決定了是否在每次呼叫服務時都執行路由規則。如果 runtime 為 true,那麼每次呼叫服務前,都需要進行服務路由。這個對效能造成影響,慎重配置。關於該引數更詳細的說明,請參考官方文件

介紹完 AbstractDirectory,接下來我們開始分析子類的原始碼。

3.1 StaticDirectory

StaticDirectory 即靜態服務目錄,顧名思義,它內部存放的 Invoker 是不會變動的。所以,理論上它和不可變 List 的功能很相似。下面我們來看一下這個類的實現。

public class StaticDirectory<T> extends AbstractDirectory<T> {

    // Invoker 列表
    private final List<Invoker<T>> invokers;
    
    // 省略構造方法

    @Override
    public Class<T> getInterface() {
        // 獲取介面類
        return invokers.get(0).getInterface();
    }
    
    // 檢測服務目錄是否可用
    @Override
    public boolean isAvailable() {
        if (isDestroyed()) {
            return false;
        }
        for (Invoker<T> invoker : invokers) {
            if (invoker.isAvailable()) {
                // 只要有一個 Invoker 是可用的,就任務當前目錄是可用的
                return true;
            }
        }
        return false;
    }

    @Override
    public void destroy() {
        if (isDestroyed()) {
            return;
        }
        // 呼叫父類銷燬邏輯
        super.destroy();
        // 遍歷 Invoker 列表,並執行相應的銷燬邏輯
        for (Invoker<T> invoker : invokers) {
            invoker.destroy();
        }
        invokers.clear();
    }

    @Override
    protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
        // 列舉 Inovker,也就是直接返回 invokers 成員變數
        return invokers;
    }
}

以上就是 StaticDirectory 的程式碼邏輯,很簡單,大家都能看懂,我就不多說了。下面來看看 RegistryDirectory,這個類的邏輯比較複雜。

3.2 RegistryDirectory

RegistryDirectory 是一種動態服務目錄,它實現了 NotifyListener 介面。當註冊中心服務配置發生變化後,RegistryDirectory 可收到與當前服務相關的變化。收到變更通知後,RegistryDirectory 可根據配置變更資訊重新整理 Invoker 列表。RegistryDirectory 中有幾個比較重要的邏輯,第一是 Invoker 的列舉邏輯,第二是接受服務配置變更的邏輯,第三是 Invoker 的重新整理邏輯。接下來,我將按順序對這三塊邏輯。

3.2.1 列舉 Invoker

Invoker 列舉邏輯封裝在 doList 方法中,這是個模板方法,前面已經介紹過了。那這裡就不過多囉嗦了,我們直入主題吧。

public List<Invoker<T>> doList(Invocation invocation) {
    if (forbidden) {
        // 服務提供者關閉或禁用了服務,此時丟擲 No provider 異常
        throw new RpcException(RpcException.FORBIDDEN_EXCEPTION,
            "No provider available from registry ...");
    }
    List<Invoker<T>> invokers = null;
    // 獲取 Invoker 本地快取
    Map<String, List<Invoker<T>>> localMethodInvokerMap = this.methodInvokerMap;
    if (localMethodInvokerMap != null && localMethodInvokerMap.size() > 0) {
        // 獲取方法名和引數列表
        String methodName = RpcUtils.getMethodName(invocation);
        Object[] args = RpcUtils.getArguments(invocation);
        // 檢測引數列表的第一個引數是否為 String 或 enum 型別
        if (args != null && args.length > 0 && args[0] != null
                && (args[0] instanceof String || args[0].getClass().isEnum())) {
            // 通過 方法名 + 第一個引數名稱 查詢 Invoker 列表,具體的使用場景暫時沒想到
            invokers = localMethodInvokerMap.get(methodName + "." + args[0]);
        }
        if (invokers == null) {
            // 通過方法名獲取 Invoker 列表
            invokers = localMethodInvokerMap.get(methodName);
        }
        if (invokers == null) {
            // 通過星號 * 獲取 Invoker 列表
            invokers = localMethodInvokerMap.get(Constants.ANY_VALUE);
        }
        if (invokers == null) {
            Iterator<List<Invoker<T>>> iterator = localMethodInvokerMap.values().iterator();
            if (iterator.hasNext()) {
                // 通過迭代器獲取 Invoker 列表
                invokers = iterator.next();
            }
        }
    }

    // 返回 Invoker 列表
    return invokers == null ? new ArrayList<Invoker<T>>(0) : invokers;
}

以上程式碼進行多次嘗試,以期從 localMethodInvokerMap 中獲取到 Invoker 列表。一般情況下,普通的呼叫可通過方法名獲取到對應的 Invoker 列表,泛化呼叫可通過 ***** 獲取到 Invoker 列表。按現有的邏輯,不管什麼情況下,***** 到 Invoker 列表的對映關係 <*****, invokers> 總是存在的,也就意味著 localMethodInvokerMap.get(Constants.ANY_VALUE) 總是有值返回。除非這個值是 null,才會通過通過迭代器獲取 Invoker 列表。至於什麼情況下為空,我暫時未完全搞清楚,我猜測是被路由規則(使用者可基於 Router 介面實現自定義路由器)處理後,可能會得到一個 null。目前僅是猜測,未做驗證。

本節的邏輯主要是從 localMethodInvokerMap 中獲取 Invoker,localMethodInvokerMap 源自 RegistryDirectory 類的成員變數 methodInvokerMap。doList 方法可以看做是對 methodInvokerMap 變數的讀操作,至於對 methodInvokerMap 變數的寫操作,這個將在後續進行分析。

3.2.2 接收服務變更通知

RegistryDirectory 是一個動態服務目錄,它需要接受註冊中心配置進行動態調整。因此 RegistryDirectory 實現了 NotifyListener 介面,通過這個介面獲取註冊中心變更通知。下面我們來看一下具體的邏輯。

public synchronized void notify(List<URL> urls) {
    // 定義三個集合,分別用於存放服務提供者 url,路由 url,配置器 url
    List<URL> invokerUrls = new ArrayList<URL>();
    List<URL> routerUrls = new ArrayList<URL>();
    List<URL> configuratorUrls = new ArrayList<URL>();
    for (URL url : urls) {
        String protocol = url.getProtocol();
        // 獲取 category 引數
        String category = url.getParameter(Constants.CATEGORY_KEY, Constants.DEFAULT_CATEGORY);
        // 根據 category 引數將 url 分別放到不同的列表中
        if (Constants.ROUTERS_CATEGORY.equals(category)
                || Constants.ROUTE_PROTOCOL.equals(protocol)) {
            // 新增路由器 url
            routerUrls.add(url);
        } else if (Constants.CONFIGURATORS_CATEGORY.equals(category)
                || Constants.OVERRIDE_PROTOCOL.equals(protocol)) {
            // 新增配置器 url
            configuratorUrls.add(url);
        } else if (Constants.PROVIDERS_CATEGORY.equals(category)) {
            // 新增服務提供者 url
            invokerUrls.add(url);
        } else {
            // 忽略不支援的 category
            logger.warn("Unsupported category ...");
        }
    }
    if (configuratorUrls != null && !configuratorUrls.isEmpty()) {
        // 將 url 轉成 Configurator
        this.configurators = toConfigurators(configuratorUrls);
    }
    if (routerUrls != null && !routerUrls.isEmpty()) {
        // 將 url 轉成 Router
        List<Router> routers = toRouters(routerUrls);
        if (routers != null) {
            setRouters(routers);
        }
    }
    List<Configurator> localConfigurators = this.configurators;
    this.overrideDirectoryUrl = directoryUrl;
    if (localConfigurators != null && !localConfigurators.isEmpty()) {
        for (Configurator configurator : localConfigurators) {
            // 配置 overrideDirectoryUrl
            this.overrideDirectoryUrl = configurator.configure(overrideDirectoryUrl);
        }
    }

    // 重新整理 Invoker 列表
    refreshInvoker(invokerUrls);
}

如上,notify 方法首先是根據 url 的 category 引數對 url 進行分門別類儲存,然後通過 toRouters 和 toConfigurators 將 url 列表轉成 Router 和 Configurator 列表。最後呼叫 refreshInvoker 方法重新整理 Invoker 列表。這裡的 toRouters 和 toConfigurators 方法邏輯不復雜,大家自行分析。接下來,我們把重點放在 refreshInvoker 方法上。

3.2.3 重新整理 Invoker 列表

接著上一節繼續分析,refreshInvoker 方法是保證 RegistryDirectory 隨註冊中心變化而變化的關鍵所在。這一塊邏輯比較多,接下來一一進行分析。

private void refreshInvoker(List<URL> invokerUrls) {
    // invokerUrls 僅有一個元素,且 url 協議頭為 empty,此時表示禁用所有服務
    if (invokerUrls != null && invokerUrls.size() == 1 && invokerUrls.get(0) != null
            && Constants.EMPTY_PROTOCOL.equals(invokerUrls.get(0).getProtocol())) {
        // 設定 forbidden 為 true
        this.forbidden = true;
        this.methodInvokerMap = null;
        // 銷燬所有 Invoker
        destroyAllInvokers();
    } else {
        this.forbidden = false;
        Map<String, Invoker<T>> oldUrlInvokerMap = this.urlInvokerMap;
        if (invokerUrls.isEmpty() && this.cachedInvokerUrls != null) {
            // 新增快取 url 到 invokerUrls 中
            invokerUrls.addAll(this.cachedInvokerUrls);
        } else {
            this.cachedInvokerUrls = new HashSet<URL>();
            // 快取 invokerUrls
            this.cachedInvokerUrls.addAll(invokerUrls);
        }
        if (invokerUrls.isEmpty()) {
            return;
        }
        // 將 url 轉成 Invoker
        Map<String, Invoker<T>> newUrlInvokerMap = toInvokers(invokerUrls);
        // 將 newUrlInvokerMap 轉成方法名到 Invoker 列表的對映
        Map<String, List<Invoker<T>>> newMethodInvokerMap = toMethodInvokers(newUrlInvokerMap);
        // 轉換出錯,直接列印異常,並返回
        if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
            logger.error(new IllegalStateException("urls to invokers error ..."));
            return;
        }
        // 合併多個組的 Invoker
        this.methodInvokerMap = multiGroup ? toMergeMethodInvokerMap(newMethodInvokerMap) : newMethodInvokerMap;
        // 儲存為本地快取
        this.urlInvokerMap = newUrlInvokerMap;
        try {
            // 銷燬無用 Invoker
            destroyUnusedInvokers(oldUrlInvokerMap, newUrlInvokerMap);
        } catch (Exception e) {
            logger.warn("destroyUnusedInvokers error. ", e);
        }
    }
}

上面方法的程式碼不是很多,但是邏輯卻不少。首先時根據入參 invokerUrls 的數量和協議頭判斷是否禁用所有的服務,如果禁用,則將 forbidden 設為 true,並銷燬所有的 Invoker。若不禁用,則將 url 轉成 Invoker,得到 <url, Invoker> 的對映關係。然後進一步進行轉換,得到 <methodName, Invoker 列表>。之後進行多組 Invoker 合併操作,並將合併結果賦值給 methodInvokerMap。methodInvokerMap 變數在 doList 方法中會被用到,doList 會對該變數進行讀操作,在這裡是寫操作。當新的 Invoker 列表生成後,還要一個重要的工作要做,就是銷燬無用的 Invoker,避免服務消費者呼叫已下線的服務的服務。

接下里,我將對上面涉及到的呼叫進行分析。按照順序,這裡先來分析 url 到 Invoker 的轉換過程。

private Map<String, Invoker<T>> toInvokers(List<URL> urls) {
    Map<String, Invoker<T>> newUrlInvokerMap = new HashMap<String, Invoker<T>>();
    if (urls == null || urls.isEmpty()) {
        return newUrlInvokerMap;
    }
    Set<String> keys = new HashSet<String>();
    // 獲取服務消費端配置的協議
    String queryProtocols = this.queryMap.get(Constants.PROTOCOL_KEY);
    for (URL providerUrl : urls) {
        if (queryProtocols != null && queryProtocols.length() > 0) {
            boolean accept = false;
            String[] acceptProtocols = queryProtocols.split(",");
            // 檢測服務提供者協議是否被服務消費者所支援
            for (String acceptProtocol : acceptProtocols) {
                if (providerUrl.getProtocol().equals(acceptProtocol)) {
                    accept = true;
                    break;
                }
            }
            if (!accept) {
                // 若服務消費者協議頭不被消費者所支援,則忽略當前 providerUrl
                continue;
            }
        }
        // 忽略 empty 協議
        if (Constants.EMPTY_PROTOCOL.equals(providerUrl.getProtocol())) {
            continue;
        }
        // 通過 SPI 檢測服務端協議是否被消費端支援
        if (!ExtensionLoader.getExtensionLoader(Protocol.class).hasExtension(providerUrl.getProtocol())) {
            logger.error(new IllegalStateException("Unsupported protocol..."));
            continue;
        }
        
        // 合併 url
        URL url = mergeUrl(providerUrl);

        String key = url.toFullString();
        if (keys.contains(key)) {
            // 忽略重複 url
            continue;
        }
        keys.add(key);
        // 本地 Invoker 快取列表
        Map<String, Invoker<T>> localUrlInvokerMap = this.urlInvokerMap;
        Invoker<T> invoker = localUrlInvokerMap == null ? null : localUrlInvokerMap.get(key);
        // 快取未命中
        if (invoker == null) {
            try {
                boolean enabled = true;
                if (url.hasParameter(Constants.DISABLED_KEY)) {
                    // 獲取 disable 配置,並修改 enable 變數
                    enabled = !url.getParameter(Constants.DISABLED_KEY, false);
                } else {
                    enabled = url.getParameter(Constants.ENABLED_KEY, true);
                }
                if (enabled) {
                    // 呼叫 refer 獲取 Invoker
                    invoker = new InvokerDelegate<T>(protocol.refer(serviceType, url), url, providerUrl);
                }
            } catch (Throwable t) {
                logger.error("Failed to refer invoker for interface...");
            }
            if (invoker != null) {
                // 快取 Invoker 例項
                newUrlInvokerMap.put(key, invoker);
            }
        } else {
            // 快取命中,將 invoker 儲存到 newUrlInvokerMap 中
            newUrlInvokerMap.put(key, invoker);
        }
    }
    keys.clear();
    return newUrlInvokerMap;
}

toInvokers 方法一開始會對服務提供者 url 進行檢測,若服務消費端的配置不支援服務端的協議,或服務端 url 協議頭為 empty 時,toInvokers 均會忽略服務提供方 url。必要的檢測做完後,緊接著是合併 url,然後訪問快取,嘗試獲取與 url 對應的 invoker。如果快取命中,直接將 Invoker 存入 newUrlInvokerMap 中即可。如果未命中,則需要新建 Invoker。Invoker 是通過 Protocol 的 refer 方法建立的,這個我在上一篇文章中已經分析過了,這裡就不贅述了。

toInvokers 方法返回的是 <url, Invoker> 對映關係表,接下來還要對這個結果進行進一步處理,得到方法名到 Invoker 列表的對映關係。這個過程由 toMethodInvokers 方法完成,如下:

private Map<String, List<Invoker<T>>> toMethodInvokers(Map<String, Invoker<T>> invokersMap) {
    // 方法名 -> Invoker 列表
    Map<String, List<Invoker<T>>> newMethodInvokerMap = new HashMap<String, List<Invoker<T>>>();
    List<Invoker<T>> invokersList = new ArrayList<Invoker<T>>();
    if (invokersMap != null && invokersMap.size() > 0) {
        for (Invoker<T> invoker : invokersMap.values()) {
            // 獲取 methods 引數
            String parameter = invoker.getUrl().getParameter(Constants.METHODS_KEY);
            if (parameter != null && parameter.length() > 0) {
                // 切分 methods 引數值,得到方法名陣列
                String[] methods = Constants.COMMA_SPLIT_PATTERN.split(parameter);
                if (methods != null && methods.length > 0) {
                    for (String method : methods) {
                        // 方法名不為 *
                        if (method != null && method.length() > 0
                                && !Constants.ANY_VALUE.equals(method)) {
                            // 根據方法名獲取 Invoker 列表
                            List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
                            if (methodInvokers == null) {
                                methodInvokers = new ArrayList<Invoker<T>>();
                                newMethodInvokerMap.put(method, methodInvokers);
                            }
                            // 儲存 Invoker 到列表中
                            methodInvokers.add(invoker);
                        }
                    }
                }
            }
            invokersList.add(invoker);
        }
    }
    
    // 進行服務級別路由,參考:https://github.com/apache/incubator-dubbo/pull/749
    List<Invoker<T>> newInvokersList = route(invokersList, null);
    // 儲存 <*, newInvokersList> 對映關係
    newMethodInvokerMap.put(Constants.ANY_VALUE, newInvokersList);
    if (serviceMethods != null && serviceMethods.length > 0) {
        for (String method : serviceMethods) {
            List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
            if (methodInvokers == null || methodInvokers.isEmpty()) {
                methodInvokers = newInvokersList;
            }
            // 進行方法級別路由
            newMethodInvokerMap.put(method, route(methodInvokers, method));
        }
    }
    // 排序,轉成不可變列表
    for (String method : new HashSet<String>(newMethodInvokerMap.keySet())) {
        List<Invoker<T>> methodInvokers = newMethodInvokerMap.get(method);
        Collections.sort(methodInvokers, InvokerComparator.getComparator());
        newMethodInvokerMap.put(method, Collections.unmodifiableList(methodInvokers));
    }
    return Collections.unmodifiableMap(newMethodInvokerMap);
}

上面方法主要做了三件事情, 第一是對入參進行遍歷,然後獲取 methods 引數,並切分成陣列。隨後以方法名為鍵,Invoker 列表為值,將對映關係儲存到 newMethodInvokerMap 中。第二是分別基於類和方法對 Invoker 列表進行路由操作。第三是對 Invoker 列表進行排序,並轉成不可變列表。關於 toMethodInvokers 方法就先分析到這,我們繼續向下分析,這次要分析的多組服務的合併邏輯。

private Map<String, List<Invoker<T>>> toMergeMethodInvokerMap(Map<String, List<Invoker<T>>> methodMap) {
    Map<String, List<Invoker<T>>> result = new HashMap<String, List<Invoker<T>>>();
    // 遍歷入參
    for (Map.Entry<String, List<Invoker<T>>> entry : methodMap.entrySet()) {
        String method = entry.getKey();
        List<Invoker<T>> invokers = entry.getValue();
        // group -> Invoker 列表
        Map<String, List<Invoker<T>>> groupMap = new HashMap<String, List<Invoker<T>>>();
        // 遍歷 Invoker 列表
        for (Invoker<T> invoker : invokers) {
            // 獲取分組配置
            String group = invoker.getUrl().getParameter(Constants.GROUP_KEY, "");
            List<Invoker<T>> groupInvokers = groupMap.get(group);
            if (groupInvokers == null) {
                groupInvokers = new ArrayList<Invoker<T>>();
                // 快取 <group, List<Invoker>> 到 groupMap 中
                groupMap.put(group, groupInvokers);
            }
            // 儲存 invoker 到 groupInvokers
            groupInvokers.add(invoker);
        }
        if (groupMap.size() == 1) {
            // 如果 groupMap 中僅包含一組鍵值對,此時直接取出該鍵值對的值即可
            result.put(method, groupMap.values().iterator().next());
        
        // groupMap 中包含多組鍵值對,比如:
        // {
        //     "dubbo": [invoker1, invoker2, invoker3, ...],
        //     "hello": [invoker4, invoker5, invoker6, ...]
        // }
        } else if (groupMap.size() > 1) {
            List<Invoker<T>> groupInvokers = new ArrayList<Invoker<T>>();
            for (List<Invoker<T>> groupList : groupMap.values()) {
                // 通過叢集類合併每個分組對應的 Invoker 列表
                groupInvokers.add(cluster.join(new StaticDirectory<T>(groupList)));
            }
            // 快取結果
            result.put(method, groupInvokers);
        } else {
            result.put(method, invokers);
        }
    }
    return result;
}

上面方法首先是生成 group 到 Invoker 類比的對映關係表,若關係表中的對映關係數量大於1,表示有多組服務。此時通過叢集類合併每組 Invoker,並將合併結果儲存到 groupInvokers 中。之後將方法名與 groupInvokers 存到到 result 中,並返回,整個邏輯結束。

接下來我們再來看一下 Invoker 列表重新整理邏輯的最後一個動作 -- 刪除無用 Invoker。如下:

private void destroyUnusedInvokers(Map<String, Invoker<T>> oldUrlInvokerMap, Map<String, Invoker<T>> newUrlInvokerMap) {
    if (newUrlInvokerMap == null || newUrlInvokerMap.size() == 0) {
        destroyAllInvokers();
        return;
    }
   
    List<String> deleted = null;
    if (oldUrlInvokerMap != null) {
        // 獲取新生成的 Invoker 列表
        Collection<Invoker<T>> newInvokers = newUrlInvokerMap.values();
        // 遍歷老的 <url, Invoker> 對映表
        for (Map.Entry<String, Invoker<T>> entry : oldUrlInvokerMap.entrySet()) {
            // 檢測 newInvokers 中是否包含老的 Invoker
            if (!newInvokers.contains(entry.getValue())) {
                if (deleted == null) {
                    deleted = new ArrayList<String>();
                }
                // 若不包含,則將老的 Invoker 對應的 url 存入 deleted 列表中
                deleted.add(entry.getKey());
            }
        }
    }

    if (deleted != null) {
        // 遍歷 deleted 集合,併到老的 <url, Invoker> 對映關係表查出 Invoker,銷燬之
        for (String url : deleted) {
            if (url != null) {
                // 從 oldUrlInvokerMap 中移除 url 對應的 Invoker
                Invoker<T> invoker = oldUrlInvokerMap.remove(url);
                if (invoker != null) {
                    try {
                        // 銷燬 Invoker
                        invoker.destroy();
                    } catch (Exception e) {
                        logger.warn("destroy invoker...");
                    }
                }
            }
        }
    }
}

destroyUnusedInvokers 方法的主要邏輯是通過 newUrlInvokerMap 找出待刪除 Invoker 對應的 url,並將 url 存入到 deleted 列表中。然後再遍歷 deleted 列表,並從 oldUrlInvokerMap 中移除相應的 Invoker,銷燬之。整個邏輯大致如此,不是很難理解。

到此關於 Invoker 列表的重新整理邏輯就分析了,這裡對整個過程進行簡單總結。如下:

  1. 檢測入參是否僅包含一個 url,且 url 協議頭為 empty
  2. 若第一步檢測結果為 true,表示禁用所有服務,此時銷燬所有的 Invoker
  3. 若第一步檢測結果為 false,此時將入參轉為 Invoker 列表
  4. 對將上一步邏輯刪除的結果進行進一步處理,得到方法名到 Invoker 的對映關係表
  5. 合併多組 Invoker
  6. 銷燬無用 Invoker

Invoker 的重新整理邏輯還是比較複雜的,大家在看的過程中多寫點 demo 進行除錯。好了,本節就到這。

4. 總結

本篇文章對 Dubbo 服務目錄進行了較為詳細的分析,篇幅主要集中在 RegistryDirectory 的原始碼分析上。分析下來,不由得感嘆,想讓本地服務目錄和註冊中心保持一致還是需要做很多事情的,並不簡單。服務目錄是 Dubbo 叢集容錯的一部分,也是比較基礎的部分,所以大家務必搞懂。

好了,本篇文章就先到這了。感謝大家閱讀。

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
作者:田小波
本文同步釋出在我的個人部落格:http://www.tianxiaobo.com