Dubbo 源碼分析 - 集羣容錯之 Router

1. 簡介

上一篇文章分析了集羣容錯的第一部分 -- 服務目錄 Directory。服務目錄在刷新 Invoker 列表的過程當中,會經過 Router 進行服務路由。上一篇文章關於服務路由相關邏輯沒有細緻分析,一筆帶過了,本篇文章將對此進行詳細的分析。首先,先來介紹一下服務路由是什麼。服務路由包含一條路由規則,路由規則決定了服務消費者的調用目標,即規定了服務消費者可調用哪些服務提供者。Dubbo 目前提供了三種服務路由實現,分別爲條件路由 ConditionRouter、腳本路由 ScriptRouter 和標籤路由 TagRouter。其中條件路由是咱們最常使用的,標籤路由暫未在我所分析的 2.6.4 版本中提供,該實現會在 2.7.0 版本中提供。本篇文章將分析條件路由相關源碼,腳本路由和標籤路由這裏就不分析了。下面進入正題。java

2. 源碼分析

條件路由規則有兩個條件組成,分別用於對服務消費者和提供者進行匹配。好比有這樣一條規則:正則表達式

host = 10.20.153.10 => host = 10.20.153.11json

該條規則表示 IP 爲 10.20.153.10 的服務消費者只可調用 IP 爲 10.20.153.11 機器上的服務,不可調用其餘機器上的服務。條件路由規則的格式以下:app

[服務消費者匹配條件] => [服務提供者匹配條件]框架

若是服務消費者匹配條件爲空,表示不對服務消費者進行限制。若是服務提供者匹配條件爲空,表示對某些服務消費者禁用服務。Dubbo 官方文檔對條件路由進行了比較詳細的介紹,你們能夠參考下,這裏就不過多說明了。dom

條件路由實現類 ConditionRouter 須要對用戶配置的路由規則進行解析,獲得一系列的條件。而後再根據這些條件對服務進行路由。本章將分兩節進行說明,2.1節介紹表達式解析過程。2.2 節介紹服務路由的過程。接下來,咱們先從表達式解析過程看起。ide

2.1 表達式解析

條件路由規則是一條字符串,對於 Dubbo 來講,它並不能直接理解字符串的意思,須要將其解析成內部格式才行。條件表達式的解析過程始於 ConditionRouter 的構造方法,下面一塊兒看一下:源碼分析

public ConditionRouter(URL url) {
    this.url = url;
    // 獲取 priority 和 force 配置
    this.priority = url.getParameter(Constants.PRIORITY_KEY, 0);
    this.force = url.getParameter(Constants.FORCE_KEY, false);
    try {
        // 獲取路由規則
        String rule = url.getParameterAndDecoded(Constants.RULE_KEY);
        if (rule == null || rule.trim().length() == 0) {
            throw new IllegalArgumentException("Illegal route rule!");
        }
        rule = rule.replace("consumer.", "").replace("provider.", "");
        // 定位 => 分隔符
        int i = rule.indexOf("=>");
        // 分別獲取服務消費者和提供者匹配規則
        String whenRule = i < 0 ? null : rule.substring(0, i).trim();
        String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
        // 解析服務消費者匹配規則
        Map<String, MatchPair> when = 
            StringUtils.isBlank(whenRule) || "true".equals(whenRule) 
                ? new HashMap<String, MatchPair>() : parseRule(whenRule);
        // 解析服務提供者匹配規則
        Map<String, MatchPair> then = 
            StringUtils.isBlank(thenRule) || "false".equals(thenRule) 
                ? null : parseRule(thenRule);
        this.whenCondition = when;
        this.thenCondition = then;
    } catch (ParseException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

如上,ConditionRouter 構造方法先是對路由規則作預處理,而後調用 parseRule 方法分別對服務提供者和消費者規則進行解析,最後將解析結果賦值給 whenCondition 和 thenCondition 成員變量。ConditionRouter 構造方法不是很複雜,這裏就很少說了。下面咱們把重點放在 parseRule 方法上,在詳細介紹這個方法以前,咱們先來看一個內部類。單元測試

private static final class MatchPair {
    final Set<String> matches = new HashSet<String>();
    final Set<String> mismatches = new HashSet<String>();
}

MatchPair 內部包含了兩個 Set 型的成員變量,分別用於存放匹配和不匹配的條件。這個類兩個成員變量會在 parseRule 方法中被用到,下面來看一下。測試

private static Map<String, MatchPair> parseRule(String rule)
        throws ParseException {
    // 定義條件映射集合
    Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
    if (StringUtils.isBlank(rule)) {
        return condition;
    }
    MatchPair pair = null;
    Set<String> values = null;
    // 經過正則表達式匹配路由規則,ROUTE_PATTERN = ([&!=,]*)\s*([^&!=,\s]+)
    // 這個表達式看起來不是很好理解,第一個括號內的表達式用於匹配"&", "!", "=" 和 "," 等符號。
    // 第二括號內的用於匹配英文字母,數字等字符。舉個例子說明一下:
    //    host = 2.2.2.2 & host != 1.1.1.1 & method = hello
    // 匹配結果以下:
    //     括號一      括號二
    // 1.  null       host
    // 2.   =         2.2.2.2
    // 3.   &         host
    // 4.   !=        1.1.1.1 
    // 5.   &         method
    // 6.   =         hello
    final Matcher matcher = ROUTE_PATTERN.matcher(rule);
    while (matcher.find()) {
        // 獲取括號一內的匹配結果
        String separator = matcher.group(1);
        // 獲取括號二內的匹配結果
        String content = matcher.group(2);
        // 分隔符爲空,表示匹配的是表達式的開始部分
        if (separator == null || separator.length() == 0) {
            // 建立 MatchPair 對象
            pair = new MatchPair();
            // 存儲 <匹配項, MatchPair> 鍵值對,好比 <host, MatchPair>
            condition.put(content, pair); 
        } 
        
        // 若是分隔符爲 &,代表接下來也是一個條件
        else if ("&".equals(separator)) {
            // 嘗試從 condition 獲取 MatchPair
            if (condition.get(content) == null) {
                // 未獲取到 MatchPair,從新建立一個,並放入 condition 中
                pair = new MatchPair();
                condition.put(content, pair);
            } else {
                pair = condition.get(content);
            }
        } 
        
        // 分隔符爲 =
        else if ("=".equals(separator)) {
            if (pair == null)
                throw new ParseException("Illegal route rule ...");

            values = pair.matches;
            // 將 content 存入到 MatchPair 的 matches 集合中
            values.add(content);
        } 
        
        //  分隔符爲 != 
        else if ("!=".equals(separator)) {
            if (pair == null)
                throw new ParseException("Illegal route rule ...");

            values = pair.mismatches;
            // 將 content 存入到 MatchPair 的 mismatches 集合中
            values.add(content);
        }
        
        // 分隔符爲 ,
        else if (",".equals(separator)) {
            if (values == null || values.isEmpty())
                throw new ParseException("Illegal route rule ...");
            // 將 content 存入到上一步獲取到的 values 中,多是 matches,也多是 mismatches
            values.add(content);
        } else {
            throw new ParseException("Illegal route rule ...");
        }
    }
    return condition;
}

以上就是路由規則的解析邏輯,該邏輯由正則表達式 + 一個 while 循環 + 數個條件分支組成。下面使用一個示例對解析邏輯進行演繹。示例爲 host = 2.2.2.2 & host != 1.1.1.1 & method = hello。正則解析結果以下:

括號一      括號二
1.  null       host
2.   =         2.2.2.2
3.   &         host
4.   !=        1.1.1.1
5.   &         method
6.   =         hello

如今線程進入 while 循環:

第一次循環:分隔符 separator = null,content = "host"。此時建立 MatchPair 對象,並存入到 condition 中,condition = {"host": MatchPair@123}

第二次循環:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@123。此時將 2.2.2.2 放入到 MatchPair@123 對象的 matches 集合中。

第三次循環:分隔符 separator = "&",content = "host"。host 已存在於 condition 中,所以 pair = MatchPair@123。

第四次循環:分隔符 separator = "!=",content = "1.1.1.1",pair = MatchPair@123。此時將 1.1.1.1 放入到 MatchPair@123 對象的 mismatches 集合中。

第五次循環:分隔符 separator = "&",content = "method"。condition.get("method") = null,所以新建一個 MatchPair 對象,並放入到 condition 中。此時 condition = {"host": MatchPair@123, "method": MatchPair@ 456}

第六次循環:分隔符 separator = "=",content = "2.2.2.2",pair = MatchPair@456。此時將 hello 放入到 MatchPair@456 對象的 matches 集合中。

循環結束,此時 condition 的內容以下:

{
    "host": {
        "matches": ["2.2.2.2"],
        "mismatches": ["1.1.1.1"]
    },
    "method": {
        "matches": ["hello"],
        "mismatches": []
    }
}

路由規則的解析過程稍微有點複雜,你們可經過 ConditionRouter 的測試類對該邏輯進行測試。而且找一個表達式,對照上面的代碼走一遍,加深理解。關於路由規則的解析過程就先到這,咱們繼續往下看。

2.2 服務路由

服務路由的入口方法是 ConditionRouter 的 router 方法,該方法定義在 Router 接口中。實現代碼以下:

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    if (invokers == null || invokers.isEmpty()) {
        return invokers;
    }
    try {
        // 先對服務消費者條件進行匹配,若是匹配失敗,代表當前消費者 url 不符合匹配規則,
        // 無需進行後續匹配,直接返回 Invoker 列表便可。好比下面的規則:
        //     host = 10.20.153.10 => host = 10.0.0.10
        // 這條路由規則但願 IP 爲 10.20.153.10 的服務消費者調用 IP 爲 10.0.0.10 機器上的服務。
        // 當消費者 ip 爲 10.20.153.11 時,matchWhen 返回 false,代表當前這條路由規則不適用於
        // 當前的服務消費者,此時無需再進行後續匹配,直接返回便可。
        if (!matchWhen(url, invocation)) {
            return invokers;
        }
        List<Invoker<T>> result = new ArrayList<Invoker<T>>();
        // 服務提供者匹配條件未配置,代表對指定的服務消費者禁用服務,也就是服務消費者在黑名單中
        if (thenCondition == null) {
            logger.warn("The current consumer in the service blacklist...");
            return result;
        }
        // 這裏能夠簡單的把 Invoker 理解爲服務提供者,如今使用服務消費者匹配規則對 
        // Invoker 列表進行匹配
        for (Invoker<T> invoker : invokers) {
            // 匹配成功,代表當前 Invoker 符合服務提供者匹配規則。
            // 此時將 Invoker 添加到 result 列表中
            if (matchThen(invoker.getUrl(), url)) {
                result.add(invoker);
            }
        }
        
        // 返回匹配結果,若是 result 爲空列表,且 force = true,表示強制返回空列表,
        // 不然路由結果爲空的路由規則將自動失效
        if (!result.isEmpty()) {
            return result;
        } else if (force) {
            logger.warn("The route result is empty and force execute ...");
            return result;
        }
    } catch (Throwable t) {
        logger.error("Failed to execute condition router rule: ...");
    }
    
    // 原樣返回,此時 force = false,表示該條路由規則失效
    return invokers;
}

router 方法先是調用 matchWhen 對服務消費者進行匹配,若是匹配失敗,直接返回 Invoker 列表。若是匹配成功,再對服務提供者進行匹配,匹配邏輯封裝在了 matchThen 方法中。下面來看一下這兩個方法的邏輯:

boolean matchWhen(URL url, Invocation invocation) {
    // 服務消費者條件爲 null 或空,均返回 true,好比:
    //     => host != 172.22.3.91
    // 表示全部的服務消費者都不得調用 IP 爲 172.22.3.91 的機器上的服務
    return whenCondition == null || whenCondition.isEmpty() 
        || matchCondition(whenCondition, url, null, invocation);  // 進行條件匹配
}

private boolean matchThen(URL url, URL param) {
    // 服務提供者條件爲 null 或空,表示禁用服務
    return !(thenCondition == null || thenCondition.isEmpty()) 
        && matchCondition(thenCondition, url, param, null);  // 進行條件匹配
}

這兩個方法長的有點像,不過邏輯上仍是有差異的,你們注意看。這兩個方法均調用了 matchCondition 方法,不過它們所傳入的參數是不一樣的,這個須要特別注意。否則後面的邏輯很差弄懂。下面咱們對這幾個參數進行溯源。matchWhen 方法向 matchCondition 方法傳入的參數爲 [whenCondition, url, null, invocation],第一個參數 whenCondition 爲服務消費者匹配條件,這個前面分析過。第二個參數 url 源自 route 方法的參數列表,該參數由外部類調用 route 方法時傳入。有代碼爲證,以下:

private List<Invoker<T>> route(List<Invoker<T>> invokers, String method) {
    Invocation invocation = new RpcInvocation(method, new Class<?>[0], new Object[0]);
    List<Router> routers = getRouters();
    if (routers != null) {
        for (Router router : routers) {
            if (router.getUrl() != null) {
                // 注意第二個參數
                invokers = router.route(invokers, getConsumerUrl(), invocation);
            }
        }
    }
    return invokers;
}

上面這段代碼來自 RegistryDirectory,第二個參數表示的是服務消費者 url。matchCondition 的 invocation 參數也是從這裏傳入的。

接下來再來看看 matchThen 向 matchCondition 方法傳入的參數 [thenCondition, url, param, null]。第一個參數不用解釋了。第二個和第三個參數來自 matchThen 方法的參數列表,這兩個參數分別爲服務提供者 url 和服務消費者 url。搞清楚這些參數來源後,接下倆就能夠分析 matchCondition 了。

private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
    // 將服務提供者或消費者 url 轉成 Map
    Map<String, String> sample = url.toMap();
    boolean result = false;
    // 遍歷 condition 列表
    for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
        // 獲取匹配項名稱,好比 host、method 等
        String key = matchPair.getKey();
        String sampleValue;
        // 若是 invocation 不爲空,且 key 爲 mehtod(s),表示進行方法匹配
        if (invocation != null && (Constants.METHOD_KEY.equals(key) || Constants.METHODS_KEY.equals(key))) {
            // 從 invocation 獲取調用方法名稱
            sampleValue = invocation.getMethodName();
        } else {
            // 從服務提供者或消費者 url 中獲取指定字段值,好比 host、application 等
            sampleValue = sample.get(key);
            if (sampleValue == null) {
                // 嘗試經過 default.xxx 獲取相應的值
                sampleValue = sample.get(Constants.DEFAULT_KEY_PREFIX + key);
            }
        }
        
        // --------------------✨ 分割線 ✨-------------------- //
        
        if (sampleValue != null) {
            // 調用 MatchPair 的 isMatch 方法進行匹配
            if (!matchPair.getValue().isMatch(sampleValue, param)) {
                // 只要有一個規則匹配失敗,當即返回 false 結束方法邏輯
                return false;
            } else {
                result = true;
            }
        } else {
            // sampleValue 爲空,代表服務提供者或消費者 url 中不包含相關字段。此時若是 
            // MatchPair 的 matches 不爲空,表示匹配失敗,返回 false。好比咱們有這樣
            // 一條匹配條件 loadbalance = random,假設 url 中並不包含 loadbalance 參數,
            // 此時 sampleValue = null。既然路由規則裏限制了 loadbalance = random,
            // 但 sampleValue = null,明顯不符合規則,所以返回 false
            if (!matchPair.getValue().matches.isEmpty()) {
                return false;
            } else {
                result = true;
            }
        }
    }
    return result;
}

如上,matchCondition 方法看起來有點複雜,這裏簡單縷縷。分割線以上的代碼實際上主要是用於獲取 sampleValue 的值,分割線如下才是進行條件匹配。條件匹配調用的邏輯封裝在 isMatch 中,代碼以下:

private boolean isMatch(String value, URL param) {
    // 狀況一:matches 非空,mismatches 爲空
    if (!matches.isEmpty() && mismatches.isEmpty()) {
        // 遍歷 matches 集合,檢測入參 value 是否能被 matches 集合元素匹配到。
        // 舉個例子,若是 value = 10.20.153.11,matches = [10.20.153.*],
        // 此時 isMatchGlobPattern 方法返回 true
        for (String match : matches) {
            if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                return true;
            }
        }
        
        // 若是全部匹配項都沒法匹配到入參,則返回 false
        return false;
    }

    // 狀況二:matches 爲空,mismatches 非空
    if (!mismatches.isEmpty() && matches.isEmpty()) {
        for (String mismatch : mismatches) {
            // 只要入參被 mismatches 集合中的任意一個元素匹配到,就返回 false
            if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                return false;
            }
        }
        // mismatches 集合中全部元素都沒法匹配到入參,此時返回 true
        return true;
    }

    // 狀況三:matches 非空,mismatches 非空
    if (!matches.isEmpty() && !mismatches.isEmpty()) {
        // matches 和 mismatches 均爲非空,此時優先使用 mismatches 集合元素對入參進行匹配。
        // 只要 mismatches 集合中任意一個元素與入參匹配成功,就當即返回 false,結束方法邏輯
        for (String mismatch : mismatches) {
            if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                return false;
            }
        }
        // mismatches 集合元素沒法匹配到入參,此時使用 matches 繼續匹配
        for (String match : matches) {
            // 只要 matches 集合中任意一個元素與入參匹配成功,就當即返回 true
            if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                return true;
            }
        }
        return false;
    }
    
    // 狀況四:matches 和 mismatches 均爲空,此時返回 false
    return false;
}

isMatch 方法邏輯比較清晰,由三個條件分支組成,用於處理四種狀況。這裏對四種狀況下的匹配邏輯進行簡單的總結,以下:

條件 動做
狀況一 matches 非空,mismatches 爲空 遍歷 matches 集合元素,並與入參進行匹配。只要有一個元素成功匹配入參,便可返回 true。若所有失配,則返回 false。
狀況二 matches 爲空,mismatches 非空 遍歷 mismatches 集合元素,並與入參進行匹配。只要有一個元素成功匹配入參,當即 false。若所有失配,則返回 true。
狀況三 matches 非空,mismatches 非空 優先使用 mismatches 集合元素對入參進行匹配,只要任一元素與入參匹配成功,就當即返回 false,結束方法邏輯。不然再使用 matches 中的集合元素進行匹配,只要有任意一個元素匹配成功,便可返回 true。若所有失配,則返回 false
狀況四 matches 爲空,mismatches 爲空 直接返回 false

isMatch 方法邏輯不是很難理解,你們本身再看看。下面繼續分析 isMatchGlobPattern 方法。

public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
    if (param != null && pattern.startsWith("$")) {
        // 引用服務消費者參數,param 參數爲服務消費者 url
        pattern = param.getRawParameter(pattern.substring(1));
    }
    // 調用重載方法繼續比較
    return isMatchGlobPattern(pattern, value);
}

public static boolean isMatchGlobPattern(String pattern, String value) {
    // 對 * 通配符提供支持
    if ("*".equals(pattern))
        // 匹配規則爲通配符 *,直接返回 true 便可
        return true;
    if ((pattern == null || pattern.length() == 0)
            && (value == null || value.length() == 0))
        // pattern 和 value 均爲空,此時可認爲二者相等,返回 true
        return true;
    if ((pattern == null || pattern.length() == 0)
            || (value == null || value.length() == 0))
        // pattern 和 value 其中有一個爲空,二者不相等,返回 false
        return false;

    // 查找 * 通配符位置
    int i = pattern.lastIndexOf('*');
    if (i == -1) {
        // 匹配規則中不包含通配符,此時直接比較 value 和 pattern 是否相等便可,並返回比較結果
        return value.equals(pattern);
    }
    // 通配符 "*" 在匹配規則尾部,好比 10.0.21.*
    else if (i == pattern.length() - 1) {
        // 檢測 value 是否以不含通配符的匹配規則開頭,並返回結果。好比:
        // pattern = 10.0.21.*,value = 10.0.21.12,此時返回 true
        return value.startsWith(pattern.substring(0, i));
    }
    // 通配符 "*" 在匹配規則頭部
    else if (i == 0) {
        // 檢測 value 是否以不含通配符的匹配規則結尾,並返回結果
        return value.endsWith(pattern.substring(i + 1));
    }
    // 通配符 "*" 在匹配規則中間位置
    else {
        // 經過通配符將 pattern 分紅兩半,獲得 prefix 和 suffix
        String prefix = pattern.substring(0, i);
        String suffix = pattern.substring(i + 1);
        // 檢測 value 是否以 prefix 變量開頭,且以 suffix 變量結尾,並返回結果
        return value.startsWith(prefix) && value.endsWith(suffix);
    }
}

以上就是 isMatchGlobPattern 兩個重載方法的所有邏輯,這兩個方法分別對普通的匹配,以及」引用消費者參數「和通配符匹配作了支持。這兩個方法的邏輯並非很複雜,並且我也在代碼上進行了比較詳細的註釋,你們本身看看吧,就很少說了。

3. 總結

本篇文章對條件路由的表達式解析和服務路由過程進行了較爲細緻的分析。總的來講,條件路由的代碼仍是有一些複雜的,須要耐下心來看。在閱讀條件路由代碼的過程當中,要多調試。通常的框架都會有單元測試,Dubbo 也不例外,所以你們能夠直接經過 ConditionRouterTest 對條件路由進行調試,無需本身手寫測試用例。

好了,關於條件路由就先分析到這,謝謝閱讀。

附錄:Dubbo 源碼分析系列文章

時間 文章
2018-10-01 Dubbo 源碼分析 - SPI 機制
2018-10-13 Dubbo 源碼分析 - 自適應拓展原理
2018-10-31 Dubbo 源碼分析 - 服務導出
2018-11-12 Dubbo 源碼分析 - 服務引用
2018-11-17 Dubbo 源碼分析 - 集羣容錯之 Directory
2018-11-20 Dubbo 源碼分析 - 集羣容錯之 Router

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

cc
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

相關文章
相關標籤/搜索