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 国际许可协议进行许可。

相关文章
相关标签/搜索