Dubbo原始碼分析-叢集容錯之Router

tianxiaobo.com發表於2018-11-21

1. 簡介

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

2. 原始碼分析

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

host = 10.20.153.10 => host = 10.20.153.11

該條規則表示 IP 為 10.20.153.10 的服務消費者只可呼叫 IP 為 10.20.153.11 機器上的服務,不可呼叫其他機器上的服務。條件路由規則的格式如下:

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

如果服務消費者匹配條件為空,表示不對服務消費者進行限制。如果服務提供者匹配條件為空,表示對某些服務消費者禁用服務。Dubbo 官方文件對條件路由進行了比較詳細的介紹,大家可以參考下,這裡就不過多說明了。

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

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 國際許可協議進行許可。


相關文章