引言

当一个服务存在多个 Provider 时,势必就需要考虑服务路由问题,本文中,我们就来介绍 Dubbo 服务路由的实现细节。

服务路由

服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。

条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。比如有这样一条规则:

host = 10.20.153.10 => host = 10.20.153.11

该条规则表示 IP 为 10.20.153.10 的服务消费者只可调用 IP 为 10.20.153.11 机器上的服务,不可调用其他机器上的服务。条件路由规则的格式如下:

[服务消费者匹配条件] => [服务提供者匹配条件]

如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。

条件路由规则是一条字符串,对于 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);
        // 将解析出的匹配规则分别赋值给 whenCondition 和 thenCondition 成员变量
        this.whenCondition = when;
        this.thenCondition = then;
    } catch (ParseException e) {
        throw new IllegalStateException(e.getMessage(), e);
    }
}

如上,ConditionRouter 构造方法先是对路由规则做预处理,然后调用 parseRule 方法分别对服务提供者和消费者规则进行解析,最后将解析结果赋值给 whenCondition 和 thenCondition 成员变量。

服务路由的入口方法是 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);  // 进行条件匹配
}

参考内容

[1]《深入理解Apache Dubbo与实战》
[2] dubbo 官方文档