dubbo source code series 8-cluster fault-tolerant service routing router

1. Frontier

In the previous section, we analyzed the first part of cluster fault tolerance— Service Directory Directory . In the process of enumerating the Invoker list, the service directory will perform service routing through the Router and filter out providers that meet the routing rules. Next, we will analyze the cluster fault tolerance. Part 2-Service Routing Router

Service routing Router definition: Router contains a routing rule, which determines the calling target of the consumer, that is, it specifies which providers the consumer can call

There are currently three service routing implementations provided in dubbo, namely:

ConditionRouter: Conditional routing

ScriptRouter: script routing

TagRouter: Tag routing

Among them, ConditionRouter is the one we use most often, so this article only analyzes the relevant source code of ConditionRouter. As for the source code of ScriptRouter and TagRouter, we will not analyze the source code here. If you are interested, you can analyze the source code yourself.

2. Service routing structure

The overall structure of the Router is as follows:

 

In the above figure, it can be seen that ConditionRouter, ScriptRouter, and TagRouter all inherit the AbstractRouter abstract class, and the AbstractRouter class implements the Router interface. This interface defines a very important method route, which is the routing invoker list, which filters out the invokers that do not conform to the rules.

Three, Router source code

Conditional routing rules consist of two conditions , which are used to match consumers and providers respectively. For example, there is such a rule:

host = 10.20.153.10 => host = 10.20.153.11

This rule indicates that the consumer with the IP 10.20.153.10 can only call the service of the provider with the IP 10.20.153.11 , and cannot call the services on other machines. The format of conditional routing rules is as follows:

[Consumer matching condition] => [provider matching condition]

If the consumer matching condition is empty, it means that the consumer is not restricted. If the provider matching condition is empty, it means that the service is disabled for some consumers

When the ConditionRouter class is initialized, it will first parse the routing rules configured by the user to obtain a series of conditions. Then the service is routed based on these conditions. This article will be explained in two parts, namely 3.1 expression analysis and 3.2 service routing. Below, we first analyze from the expression parsing process

3.1 Expression analysis

The conditional routing rule is a string. For Dubbo, it cannot directly understand the meaning of the string. It needs to be parsed into an internal format. The parsing process of the conditional expression starts with the construction method of ConditionRouter. The source code is as follows:

    public ConditionRouter(URL url) {
        this.url = url;
        this.priority = url.getParameter(PRIORITY_KEY, 0);
        this.force = url.getParameter(FORCE_KEY, false);
        this.enabled = url.getParameter(ENABLED_KEY, true);
        // 获取 url 中 rule 配置,并解析成匹配规则
        init(url.getParameterAndDecoded(RULE_KEY));
    }

    public void init(String rule) {
        try {
            if (rule == null || rule.trim().length() == 0) {
                throw new IllegalArgumentException("Illegal route rule!");
            }
            // rule 中的 consumer. 和 provider. 全部替换为 ""
            rule = rule.replace("consumer.", "").replace("provider.", "");
            // 定位 => 分隔符位置
            int i = rule.indexOf("=>");
            // => 左边的为 consumer 匹配条件
            String whenRule = i < 0 ? null : rule.substring(0, i).trim();
            // => 右边的为 provider 匹配条件
            String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
            // 解析 consumer 匹配规则
            Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
            // 解析 provider 匹配规则
            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.
            // 注意:应该在业务级别上确定 'when' 条件是否可以为空
            this.whenCondition = when;
            this.thenCondition = then;
        } catch (ParseException e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    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 对象中同时包含了匹配和不匹配条件集合
        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)) {
                if (condition.get(content) == null) {
                    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 内容存入到上一次匹配到的内容获取到的 MatchPair 的 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;
    }

You can debug the parsing method yourself. Let me show you the entire parsing process with an example. The parsing string is host = 2.2.2.2, 3.3.3.3 & host != 1.1.1.1 & method = hello. The results of the parsing are as follows:

    Bracket 1 Bracket 2
1. null host
2. = 2.2.2.2
3., 3.3.3.3
4. & host
5. != 1.1.1.1
6. & method
7. = hello

The thread enters the while loop:

The first loop: separator = null, content = "host". At this time, a MatchPair object is created and stored in the condition, condition = {"host": MatchPair@123}

The second cycle: separator = "=", content = "2.2.2.2", pair = MatchPair@123. At this time, put 2.2.2.2 into the matches collection of the MatchPair@123 object

The third cycle: separator = ",", content = "3.3.3.3", pair = MatchPair@123. At this time, put 3.3.3.3 into the matches collection of the MatchPair@123 object

Fourth cycle: separator = "&", content = "host", host already exists in the condition, so pair = MatchPair@123

The fifth cycle: separator = "!=", content = "1.1.1.1", pair = MatchPair@123. At this time, put 1.1.1.1 into the mismatches collection of the MatchPair@123 object

The sixth cycle: separator = "&", content = "method", condition.get("method") = null, at this time, create a new MatchPair object and put it in the condition. At this time condition = {"host": MatchPair@123, "method": MatchPair@ 456}

The seventh cycle: separator = "=", content = "hello", pair = MatchPair@456. At this time, put hello into the matches collection of the MatchPair@456 object

At the end of the loop, the content of condition is as follows:

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

I believe that everyone has a clear understanding of the parsing process through the above example, and then we analyze the dubbo service routing

3.2 Service routing

The entry of service routing is in the route method of ConditionRouter. The source code of the entire routing process is as follows:

    @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 {
            // 先对 consumer 条件进行匹配,如果不匹配的话,表明 consumer url 不符合匹配规则,无需进行后续匹配,直接返回 Invoker 列表即可
            // 比如下面的规则: host = 10.20.153.10 => host = 10.0.0.10
            // 这条路由规则希望 IP 为 10.20.153.10 的 consumer 调用 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) {
                // provider 匹配条件未配置,表明对指定的 consumer 禁用服务,也就是 consumer 在黑名单中
                logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
                return result;
            }
            // 对 provider 进行一一匹配
            for (Invoker<T> invoker : invokers) {
                // 若匹配成功,表明当前 Invoker 符合 provider 匹配规则,此时将 Invoker 添加到 result 列表中
                if (matchThen(invoker.getUrl(), url)) {
                    result.add(invoker);
                }
            }
            if (!result.isEmpty()) {
                // 返回匹配后的路由 invoker 后结果
                return result;
            } else if (force) {
                // 若 result 为空列表 && force = true,表示强制返回空列表
                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);
        }
        // 没有做任何路由处理,直接返回 invokers,此时 force = false
        return invokers;
    }


    boolean matchWhen(URL url, Invocation invocation) {
        // consumer 条件为 null 或空,均返回 true,比如:     => host != 172.22.3.91
        // 表示所有的 consumer 都不得调用 IP 为 172.22.3.91 的机器上的服务
        // 匹配条件,参数 whenCondition 是 consumer 匹配条件,参数 url 是 consumer url,参数 invocation 是 consumer 的 invocation
        return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
    }

    private boolean matchThen(URL url, URL param) {
        // provider 条件为 null 或空,表示禁用服务
        // 匹配条件,参数 thenCondition 是 provider 匹配条件,参数 url 是 provider url, param 是 consumer url
        return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
    }

    private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
        // 将 consumer 或者 provider url 转成 map
        Map<String, String> sample = url.toMap();
        boolean result = false;
        for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
            String key = matchPair.getKey();
            String sampleValue;
            //get real invoked method name from invocation
            // 如果 invocation 不为空 && key 为 mehtod 或者 methods,表示进行方法匹配
            if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
                // 从 invocation 中 获取实际调用的方法名
                sampleValue = invocation.getMethodName();
            } else if (ADDRESS_KEY.equals(key)) {
                // 从 consumer 或者 provider 的 url 中获取 address 值
                sampleValue = url.getAddress();
            } else if (HOST_KEY.equals(key)) {
                // 从 consumer 或者 provider 的 url 中获取主机 host 值
                sampleValue = url.getHost();
            } else {
                // 从 consumer 或者 provider 的 url 中获取指定字段值,比如 protocol、path 等
                sampleValue = sample.get(key);
                if (sampleValue == null) {
                    // 尝试通过 default.key 获取相应的值
                    sampleValue = sample.get(DEFAULT_KEY_PREFIX + key);
                }
            }
            if (sampleValue != null) {
                // 调用 MatchPair 的 isMatch 方法进行匹配
                if (!matchPair.getValue().isMatch(sampleValue, param)) {
                    // 只要有一个规则匹配失败,立即返回 false 结束方法
                    return false;
                } else {
                    result = true;
                }
            } else {
                //not pass the condition
                // sampleValue 为空,表明 consumer 或者 provider 的 url 中不包含相关字段。此时如果 MatchPair 的 matches 不为空,表示匹配失败,返回 false
                // 比如我们有这样一条匹配条件 loadbalance = random,假设 url 中并不包含 loadbalance 参数,此时 sampleValue = null。
                // 既然路由规则里限制了 loadbalance 必须为 random,但 sampleValue = null,明显不符合规则,因此返回 false
                if (!matchPair.getValue().matches.isEmpty()) {
                    return false;
                } else {
                    // MatchPair 的 matches 为空,表示匹配成功
                    result = true;
                }
            }
        }
        return result;
    }

    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) {
            // 情况1:matches不为空 && mismatches为空
            if (!matches.isEmpty() && mismatches.isEmpty()) {
                // 遍历 matches 集合,检测入参是否能被 matches 集合元素匹配到
                // 举个例子,如果 value = 10.20.153.11,matches = [10.20.153.*],
                // 此时 isMatchGlobPattern 方法返回 true,结果返回 true
                for (String match : matches) {
                    // 只要有一个条件匹配成功即返回匹配成功结果
                    if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                        return true;
                    }
                }
                // 所有匹配项都不匹配,返回false
                return false;
            }

            // 情况2:matches为空 && mismatches为不空
            if (!mismatches.isEmpty() && matches.isEmpty()) {
                // 遍历 matches 集合,检测入参是否能被 mismatches 集合元素匹配到
                // 举个例子,如果 value = 10.20.153.11,mismatches = [10.20.153.*],
                // 此时 isMatchGlobPattern 方法返回 true,结果返回 false
                for (String mismatch : mismatches) {
                    // 只要入参被 mismatches 集合中的任意一个元素匹配到,就返回 false
                    if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                        return false;
                    }
                }
                // mismatches 集合中所有元素都无法匹配到入参,此时返回 true
                return true;
            }

            // 情况3:matches不为空 && mismatches为不空
            if (!matches.isEmpty() && !mismatches.isEmpty()) {
                //when both mismatches and matches contain the same value, then using mismatches first
                // 当 mismatches 和 matches 都不为空的时候,此时优先使用 mismatches 集合元素对入参进行匹配
                // 只要 mismatches 集合中任意一个元素与入参匹配成功,就立即返回 false,结束方法
                for (String mismatch : mismatches) {
                    if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                        return false;
                    }
                }

                // mismatches 集合元素无法匹配到入参,此时再使用 matches 继续匹配
                for (String match : matches) {
                    // 只要 matches 集合中任意一个元素与入参 value 匹配成功,就立即返回 true,结束方法
                    if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                        return true;
                    }
                }

                // 全部匹配失败,返回false
                return false;
            }

            // 情况4:matches 和 mismatches 都为空,直接返回 false
            return false;
        }
    }

The comments in the source code of the whole process have been written very clearly, so I won't repeat them here. Here is a summary of the isMatch method of MatchPair:

  condition process
Situation One matches is not empty, mismatches is empty Traverse the elements of the matches collection and match the input parameters. As long as one element is successfully matched into the parameter, it returns true. If all are mismatched, false is returned.
Situation two matches is empty, mismatches is not empty Traverse the mismatches collection elements and match them with the input parameters. As long as one element is successfully matched into the parameter, it is immediately false. If all are mismatched, return true.
Situation three matches 非空,mismatches 非空 The mismatches collection element is preferred to match the input parameters. As long as any element matches the input parameter successfully, it will immediately return false to end the method logic. Otherwise, the set elements in matches are used for matching. As long as any element matches successfully, it will return true. If all mismatches, return false
Situation four matches is empty, mismatches is empty Return false directly

The isMatch method is finally matched through the isMatchGlobPattern method of UrlUtils, so let's take a look at the source code of the isMatchGlobPattern method:

    public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
        if (param != null && pattern.startsWith("$")) {
            // param 参数为 consumer 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)) {
            // 匹配规则为空 && value 为空,此时认为两者匹配成功
            return true;
        }
        if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
            // 匹配规则为空 或者 value 为空,此时认为两者匹配不成功
            return false;
        }

        // 定位 * 通配符位置
        int i = pattern.lastIndexOf('*');
        // doesn't find "*"
        if (i == -1) {
            // 不存在通配符 *,则直接比较两者是否相等
            return value.equals(pattern);
        }
        // "*" is at the end
        // 通配符 * 在匹配规则末尾,例如 10.0.21.*
        else if (i == pattern.length() - 1) {
            // 检测 value 是否以“不含通配符的匹配规则”开头,并返回结果。比如: pattern = 10.0.21.*,value = 10.0.21.12
            // pattern.substring(0, i) = 10.0.21. ,此时返回 true
            return value.startsWith(pattern.substring(0, i));
        }
        // "*" is at the beginning
        // 通配符 * 在匹配规则开头,例如 *.10.0.21
        else if (i == 0) {
            // 检测 value 是否以“不含通配符的匹配规则”结尾,并返回结果。比如: pattern = *.10.0.21,value = 12.10.0.21
            // pattern.substring(i + 1) = .10.0.21 ,此时返回 true
            return value.endsWith(pattern.substring(i + 1));
        }
        // "*" is in the middle
        // 通配符 * 在匹配规则中间,例如 10.0.*.12
        else {
            // prefix = 10.0.
            String prefix = pattern.substring(0, i);
            // suffix = .12
            String suffix = pattern.substring(i + 1);
            // 检测 value 是否以 prefix 为开头 && 是否以 suffix 结尾,并返回结果。比如: pattern = 10.0.*.12,value = 10.0.11.12
            // 此时 prefix = 10.0. ,suffix = .12,value 以 prefix 开头 && value 以 suffix 结尾,此时返回 true
            return value.startsWith(prefix) && value.endsWith(suffix);
        }
    }

In the whole process of rule matching, the code comments are analyzed and explained with specific examples. You can understand the details at once, and it will not take up your time and verbosity here.

The whole process of service routing here is analyzed.

Four, summary

This paper analyzes the expression analysis of conditional routing and the process of service routing in more detail. In the process of looking at the source code, you must use unit tests for more debugging. This will not only deepen your impression, but also help you understand the intent of the code well.

reference:

https://dubbo.apache.org/zh-cn/docs/source_code_guide/router.html

Guess you like

Origin blog.csdn.net/ywlmsm1224811/article/details/103141349