Dubbo原始碼分析十一、服務路由

vinylon1022發表於2020-12-29

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

host = 10.20.153.10 => host = 10.20.153.11

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

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

如果服務消費者匹配條件為空,表示不對服務消費者進行限制。如果服務提供者匹配條件為空,表示對某些服務消費者禁用服務。

初始化

首先看一下路由是如何初始化的。

當發起服務目錄的notify通知時,會根據通知的url列表篩選出有路由規則的,然後新增到路由列表中:

List<URL> routerURLs = categoryUrls.getOrDefault(ROUTERS_CATEGORY, Collections.emptyList());
// 這裡設定routers
toRouters(routerURLs).ifPresent(this::addRouters);

我們看一下url轉Router的方法:

private Optional<List<Router>> toRouters(List<URL> urls) {
    if (urls == null || urls.isEmpty()) {
        return Optional.empty();
    }

    List<Router> routers = new ArrayList<>();
    for (URL url : urls) {
        if (EMPTY_PROTOCOL.equals(url.getProtocol())) {
            continue;
        }
        String routerType = url.getParameter(ROUTER_KEY);
        if (routerType != null && routerType.length() > 0) {
            url = url.setProtocol(routerType);
        }
        try {
            Router router = ROUTER_FACTORY.getRouter(url);
            if (!routers.contains(router)) {
                routers.add(router);
            }
        } catch (Throwable t) {
            logger.error("convert router url to router error, url: " + url, t);
        }
    }

    return Optional.of(routers);
}

通過ROUTER_FACTORY建立:

private static final RouterFactory ROUTER_FACTORY = ExtensionLoader.getExtensionLoader(RouterFactory.class)
        .getAdaptiveExtension();

這裡提供了幾種路由方式:

file=org.apache.dubbo.rpc.cluster.router.file.FileRouterFactory
script=org.apache.dubbo.rpc.cluster.router.script.ScriptRouterFactory
condition=org.apache.dubbo.rpc.cluster.router.condition.ConditionRouterFactory
service=org.apache.dubbo.rpc.cluster.router.condition.config.ServiceRouterFactory
app=org.apache.dubbo.rpc.cluster.router.condition.config.AppRouterFactory
tag=org.apache.dubbo.rpc.cluster.router.tag.TagRouterFactory
mock=org.apache.dubbo.rpc.cluster.router.mock.MockRouterFactory

我們以條件路由為例:

public class ConditionRouterFactory implements RouterFactory {

    public static final String NAME = "condition";

    @Override
    public Router getRouter(URL url) {
        return new ConditionRouter(url);
    }

}

根據URL建立了一個ConditionRouter物件,看一下它的初始化方法:

public ConditionRouter(URL url) {
    this.url = url;
    // 獲取 priority 和 force 配置
    this.priority = url.getParameter(PRIORITY_KEY, 0);
    this.force = url.getParameter(FORCE_KEY, false);
    this.enabled = url.getParameter(ENABLED_KEY, true);
    // 獲取路由規則
    init(url.getParameterAndDecoded(RULE_KEY));
}

然後是init方法:

public void init(String rule) {
    try {
        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);
        // NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
        this.whenCondition = when;
        this.thenCondition = then;
    } catch (ParseException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

ConditionRouter 構造方法先是對路由規則做預處理,然後呼叫 parseRule 方法分別對服務提供者和消費者規則進行解析,最後將解析結果賦值給 whenCondition 和 thenCondition 成員變數。

首先先看一下內部類MatchPair的定義:

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

    private boolean isMatch(String value, URL param) {
        ...
    }
}

isMatch方法後面規則匹配時再分析。這裡我們知道定義了兩個集合,分別用於存放匹配和不匹配的條件。

這裡我們關注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;
    }
    // Key-Value pair, stores both match and mismatch conditions
    MatchPair pair = null;
    // Multiple values
    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()) { // Try to match one by one
        // 獲取括號一內的匹配結果
        String separator = matcher.group(1);
        // 獲取括號二內的匹配結果
        String content = matcher.group(2);
        // Start part of the condition expression.
        // 分隔符為空,表示匹配的是表示式的開始部分
        if (StringUtils.isEmpty(separator)) {
            // 建立 MatchPair 物件
            pair = new MatchPair();
            // 儲存 <匹配項, MatchPair> 鍵值對,比如 <host, MatchPair>
            condition.put(content, pair);
        }
        // The KV part of the condition expression
        // 如果分隔符為 &,表明接下來也是一個條件
        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);
            }
        }
        // The Value in the KV part.
        // 分隔符為 =
        else if ("=".equals(separator)) {
            if (pair == null) {
                throw new ParseException("Illegal route rule \""
                        + rule + "\", The error char '" + separator
                        + "' at index " + matcher.start() + " before \""
                        + content + "\".", matcher.start());
            }

            values = pair.matches;
            // 將 content 存入到 MatchPair 的 matches 集合中
            values.add(content);
        }
        // The Value in the KV part.
        //  分隔符為 !=
        else if ("!=".equals(separator)) {
            if (pair == null) {
                throw new ParseException("Illegal route rule \""
                        + rule + "\", The error char '" + separator
                        + "' at index " + matcher.start() + " before \""
                        + content + "\".", matcher.start());
            }

            values = pair.mismatches;
            // 將 content 存入到 MatchPair 的 mismatches 集合中
            values.add(content);
        }
        // The Value in the KV part, if Value have more than one items.
        // 分隔符為 ,
        else if (",".equals(separator)) { // Should be separated by ','
            if (values == null || values.isEmpty()) {
                throw new ParseException("Illegal route rule \""
                        + rule + "\", The error char '" + separator
                        + "' at index " + matcher.start() + " before \""
                        + content + "\".", matcher.start());
            }
            // 將 content 存入到上一步獲取到的 values 中,可能是 matches,也可能是 mismatches
            values.add(content);
        } else {
            throw new ParseException("Illegal route rule \"" + rule
                    + "\", The error char '" + separator + "' at index "
                    + matcher.start() + " before \"" + content + "\".", matcher.start());
        }
    }
    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": []
    }
}

路由匹配

在服務目錄doList時觸發路由匹配:

invokers = routerChain.route(getConsumerUrl(), invocation);

我們看一下routerChain的route方法:

public List<Invoker<T>> route(URL url, Invocation invocation) {
    List<Invoker<T>> finalInvokers = invokers;
    for (Router router : routers) {
        finalInvokers = router.route(finalInvokers, url, invocation);
    }
    return finalInvokers;
}

就是對前面notify時新增的路由遍歷執行route方法

我們看一下ConditionRouter的route方法:

@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
        throws RpcException {
    if (!enabled) {
        return invokers;
    }

    if (CollectionUtils.isEmpty(invokers)) {
        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. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
            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. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(RULE_KEY));
            return result;
        }
    } catch (Throwable t) {
        logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t);
    }
    // 原樣返回,此時 force = false,表示該條路由規則失效
    return invokers;
}

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

boolean matchWhen(URL url, Invocation invocation) {
    // 服務消費者條件為 null 或空,均返回 true,比如:
    //     => host != 172.22.3.91
    // 表示所有的服務消費者都不得呼叫 IP 為 172.22.3.91 的機器上的服務
    return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
}

private boolean matchThen(URL url, URL param) {
    // 服務提供者條件為 null 或空,表示禁用服務
    return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
}

這兩個方法長的有點像,不過邏輯上還是有差別的,大家注意看。這兩個方法均呼叫了 matchCondition 方法,但它們所傳入的引數是不同的。這個需要特別注意一下,不然後面的邏輯不好弄懂。下面我們對這幾個引數進行溯源。matchWhen 方法向 matchCondition 方法傳入的引數為 [whenCondition, url, null, invocation],第一個引數 whenCondition 為服務消費者匹配條件,這個前面分析過。第二個引數 url 源自 route 方法的引數列表,該引數由外部類呼叫 route 方法時傳入。接下來再來看看 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;
        //get real invoked method name from invocation
        // 如果 invocation 不為空,且 key 為 mehtod(s),表示進行方法匹配
        if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
            // 從 invocation 獲取被呼叫方法的名稱
            sampleValue = invocation.getMethodName();
        } else if (ADDRESS_KEY.equals(key)) {
            sampleValue = url.getAddress();
        } else if (HOST_KEY.equals(key)) {
            sampleValue = url.getHost();
        } else {
            // 從服務提供者或消費者 url 中獲取指定欄位值,比如 host、application 等
            sampleValue = sample.get(key);
            if (sampleValue == null) {
                // 嘗試通過 default.xxx 獲取相應的值
                sampleValue = sample.get(key);
            }
        }
        if (sampleValue != null) {
            // 呼叫 MatchPair 的 isMatch 方法進行匹配
            if (!matchPair.getValue().isMatch(sampleValue, param)) {
                // 只要有一個規則匹配失敗,立即返回 false 結束方法邏輯
                return false;
            } else {
                result = true;
            }
        } else {
            //not pass the condition
            // 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;
}

前半部分獲取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()) {
        // 只要入參被 mismatches 集合中的任意一個元素匹配到,就返回 false
        for (String mismatch : mismatches) {
            if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                return false;
            }
        }
        // mismatches 集合中所有元素都無法匹配到入參,此時返回 true
        return true;
    }

    // 情況三:matches 非空,mismatches 非空
    if (!matches.isEmpty() && !mismatches.isEmpty()) {
        //when both mismatches and matches contain the same value, then using mismatches first
        // 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;
            }
        }
        // 全部失配,則返回 false
        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 方法是通過 UrlUtils 的 isMatchGlobPattern 方法進行匹配,因此下面我們再來看看 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 (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
        // pattern 和 value 均為空,此時可認為兩者相等,返回 true
        return true;
    }
    if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
        // pattern 和 value 其中有一個為空,表明兩者不相等,返回 false
        return false;
    }


    // 定位 * 萬用字元位置
    int i = pattern.lastIndexOf('*');
    // doesn't find "*"
    if (i == -1) {
        // 匹配規則中不包含萬用字元,此時直接比較 value 和 pattern 是否相等即可,並返回比較結果
        return value.equals(pattern);
    }
    // 萬用字元 "*" 在匹配規則尾部,比如 10.0.21.*
    // "*" is at the end
    else if (i == pattern.length() - 1) {
        // 檢測 value 是否以“不含萬用字元的匹配規則”開頭,並返回結果。比如:
        // pattern = 10.0.21.*,value = 10.0.21.12,此時返回 true
        return value.startsWith(pattern.substring(0, i));
    }
    // "*" is at the beginning
    // 萬用字元 "*" 在匹配規則頭部
    else if (i == 0) {
        // 檢測 value 是否以“不含萬用字元的匹配規則”結尾,並返回結果
        return value.endsWith(pattern.substring(i + 1));
    }
    // "*" is in the middle
    // 萬用字元 "*" 在匹配規則中間位置
    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);
    }
}

這兩個方法分別對普通的匹配過程,以及”引用消費者引數“和萬用字元匹配等特性提供了支援。

總結

  • 路由物件的初始化
  • 路由鏈呼叫
  • 單個路由的匹配邏輯

相關文章