2023-08-16  阅读(4)
原文作者:Ressmix 原文地址:https://www.tpvlog.com/article/372

本章,我将对 dubbo-cluster 模块中的Router路由机制进行讲解。Router 的主要功能就是根据用户配置的路由规则以及请求携带的信息,过滤出符合条件的 Invoker 集合,供后续负载均衡逻辑使用。

在上一章介绍 RegistryDirectory 的实现时,我们看到了 RouterChain 这个 Router 链的存在,但是没有深入分析,下面我就对 RouterChain 进行分析。

一、RouterChain

1.1 核心字段

RouterChain的核心字段如下:

    // RouterChain.java
    
    public class RouterChain<T> {
    
        // 待过滤的 Invoker 集合
        private List<Invoker<T>> invokers = Collections.emptyList();
    
        // 真正要使用的 Router 集合
        // 不仅包括了上面 builtinRouters 集合中全部的 Router 对象,还包括通过 addRouters() 方法添加的 Router 对象
        private volatile List<Router> routers = Collections.emptyList();
    
        //  激活的内置 Router 集合
        private List<Router> builtinRouters = Collections.emptyList();
    
    }

在 RouterChain 的构造函数中,会在传入的 URL 参数中查找 router 参数值,并根据该值获取确定激活的 RouterFactory,之后通过 Dubbo SPI 机制加载这些激活的 RouterFactory 对象,由 RouterFactory 创建内置 Router 实例:

    // RouterChain.java
    
    private RouterChain(URL url) {
    
        // 通过ExtensionLoader加载激活的RouterFactory
        List<RouterFactory> extensionFactories = ExtensionLoader.getExtensionLoader(RouterFactory.class)
                .getActivateExtension(url, "router");
    
        // 遍历所有RouterFactory,调用其getRouter()方法创建相应的Router对象
        List<Router> routers = extensionFactories.stream()
                .map(factory -> factory.getRouter(url))
                .collect(Collectors.toList());
    
        // 初始化buildinRouters字段以及routers字段
        initWithRouters(routers); 
    }
    
    public void initWithRouters(List<Router> builtinRouters) {
        this.builtinRouters = builtinRouters;
        this.routers = new ArrayList<>(builtinRouters);
        // 这里会对routers集合进行排序
        this.sort(); 
    }

1.2 addRouter方法

完成内置 Router 的初始化之后,在 Directory 实现中还可以通过 addRouter() 方法添加新的 Router 实例到 routers 字段中:

    // RouterChain.java
    
    public void addRouters(List<Router> routers) {
        List<Router> newRouters = new ArrayList<>();
        // 添加builtinRouters集合
        newRouters.addAll(builtinRouters); 
        // 添加传入的Router集合
        newRouters.addAll(routers); 
        // 重新排序
        CollectionUtils.sort(newRouters); 
        this.routers = newRouters;
    }

1.3 route方法

RouterChain.route() 方法会遍历 routers 字段,逐个调用 Router 对象的 route() 方法,对 invokers 集合进行过滤,具体实现如下:

    // RouterChain.java
    
    public List<Invoker<T>> route(URL url, Invocation invocation) {
        List<Invoker<T>> finalInvokers = invokers;
    
        // 遍历全部的Router对象
        for (Router router : routers) { 
            finalInvokers = router.route(finalInvokers, url, invocation);
    
        }
        return finalInvokers;
    }

二、RouterFactory

了解了 RouterChain 的大致逻辑之后,我们知道 真正进行路由的是 routers 集合中的 Router 对象 。接下来我们再来看 RouterFactory 这个工厂接口, RouterFactory 接口是一个扩展接口 ,具体定义如下:

    // RouterFactory.java
    
    @SPI
    public interface RouterFactory {
        // 动态生成的适配器会根据protocol参数选择扩展实现
        @Adaptive("protocol") 
        Router getRouter(URL url);
    }

2.1 继承关系

RouterFactory 接口有很多实现类,如下图所示:

202308162142492611.png

下面我们就来深入介绍下每个 RouterFactory 实现类以及对应的 Router 实现对象。 Router 决定了一次 Dubbo 调用的目标服务,Router 接口的每个实现类代表了一个路由规则 ,当 Consumer 访问 Provider 时,Dubbo 根据路由规则筛选出合适的 Provider 列表,之后通过负载均衡算法再次进行筛选。Router 接口的继承关系如下图所示:

202308162142499002.png

2.2 ConditionRouterFactory

首先来看 ConditionRouterFactory 实现,其扩展名为 condition,在其 getRouter() 方法中会创建 ConditionRouter 对象,如下所示:

    // ConditionRouterFactory.java
    
    public class ConditionRouterFactory implements RouterFactory {
        public static final String NAME = "condition";
    
        @Override
        public Router getRouter(URL url) {
            return new ConditionRouter(url);
        }
    }

ConditionRouter

ConditionRouter 是基于条件表达式的路由实现类 ,下面就是一条基于条件表达式的路由规则:

    host = 192.168.0.100 => host = 192.168.0.150

在上述规则中:

  • =>之前的为 Consumer 匹配的条件,该条件中的所有参数会与 Consumer 的 URL 进行对比,当 Consumer 满足匹配条件时,会对该 Consumer 的此次调用执行 => 后面的过滤规则;
  • => 之后为 Provider 地址列表的过滤条件,该条件中的所有参数会和 Provider 的 URL 进行对比,Consumer 最终只拿到过滤后的地址列表。

如果 Consumer 匹配条件为空,表示 => 之后的过滤条件对所有 Consumer 生效 ,例如:=> host != 192.168.0.150,含义是所有Consumer 都不能请求 192.168.0.150 这个 Provider 节点。

如果 Provider 过滤条件为空,表示禁止访问所有 Provider ,例如:host = 192.168.0.100 =>,含义是 192.168.0.100 这个 Consumer 不能访问任何 Provider 节点。

ConditionRouter 的核心字段有如下几个:

  • url(URL 类型):路由规则的 URL,可以从 rule 参数中获取具体的路由规则;
  • ROUTE_PATTERN(Pattern 类型):用于切分路由规则的正则表达式;
  • priority(int 类型):路由规则的优先级,用于排序,该字段值越大,优先级越高,默认值为 0;
  • force(boolean 类型):当路由结果为空时,是否强制执行。如果不强制执行,则路由结果为空的路由规则将会自动失效;如果强制执行,则直接返回空的路由结果;
  • whenCondition(Map 类型):Consumer 匹配的条件集合,通过解析条件表达式 rule 的 => 之前半部分,可以得到该集合中的内容;
  • thenCondition(Map 类型):Provider 匹配的条件集合,通过解析条件表达式 rule 的 => 之后半部分,可以得到该集合中的内容。

在 ConditionRouter 的构造方法中,会根据 URL 中携带的相应参数初始化 priority、force、enable 等字段,然后从 URL 的 rule 参数中获取路由规则进行解析,具体的解析逻辑是在 init() 方法中实现的,如下所示:

    // ConditionRouter.java
    
    public void init(String rule) {
    
        // 将路由规则中的"consumer."和"provider."字符串清理掉
        rule = rule.replace("consumer.", "").replace("provider.", "");
    
        // 按照"=>"字符串进行分割,得到whenRule和thenRule两部分
        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();
    
        // 解析whenRule和thenRule,得到whenCondition和thenCondition两个条件集合
        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;
    }

whenCondition 和 thenCondition 两个集合中,Key 是条件表达式中指定的参数名称(例如 host = 192.168.0.150 这个表达式中的 host)。ConditionRouter 支持三类参数:

  • 服务调用信息,例如,method、argument 等;
  • URL 本身的字段,例如,protocol、host、port 等;
  • URL 上的所有参数,例如,application 等。

Value 是 MatchPair 对象,包含两个 Set 类型的集合—— matches 和 mismatches。在 使用 MatchPair 进行过滤 的时候,会按照下面四条规则执行。

  1. 当 mismatches 集合为空的时候,会逐个遍历 matches 集合中的匹配条件,匹配成功任意一条即会返回 true。这里具体的匹配逻辑以及后续 mismatches 集合中条件的匹配逻辑,都是在 UrlUtils.isMatchGlobPattern() 方法中实现,其中完成了如下操作:如果匹配条件以 "$" 符号开头,则从 URL 中获取相应的参数值进行匹配;当遇到 "" 通配符的时候,会处理""通配符在匹配条件开头、中间以及末尾三种情况;
  2. 当 matches 集合为空的时候,会逐个遍历 mismatches 集合中的匹配条件,匹配成功任意一条即会返回 false;
  3. 当 matches 集合和 mismatches 集合同时不为空时,会优先匹配 mismatches 集合中的条件,成功匹配任意一条规则,就会返回 false;若 mismatches 中的条件全部匹配失败,才会开始匹配 matches 集合,成功匹配任意一条规则,就会返回 true;
  4. 当上述三个步骤都没有成功匹配时,直接返回 false。

上述流程具体实现在 MatchPair 的 isMatch() 方法中,比较简单,这里就不再展示。

了解了每个 MatchPair 的匹配流程之后,我们来看 parseRule() 方法是如何解析一条完整的条件表达式,生成对应 MatchPair 的 ,具体实现如下:

    // ConditionRouter.java
    
    private static Map<String, MatchPair> parseRule(String rule) throws ParseException {
    
        Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
    
        MatchPair pair = null;
    
        Set<String> values = null;
    
        // 首先,按照ROUTE_PATTERN指定的正则表达式匹配整个条件表达式
        final Matcher matcher = ROUTE_PATTERN.matcher(rule);
    
        while (matcher.find()) { // 遍历匹配的结果
            // 每个匹配结果有两部分(分组),第一部分是分隔符,第二部分是内容
            String separator = matcher.group(1); 
            String content = matcher.group(2);
    
            if (StringUtils.isEmpty(separator)) { // ---(1) 没有分隔符,content即为参数名称
                pair = new MatchPair();
                // 初始化MatchPair对象,并将其与对应的Key(即content)记录到condition集合中
                condition.put(content, pair); 
            }
    
            else if ("&".equals(separator)) { // ---(4)
                // &分隔符表示多个表达式,会创建多个MatchPair对象
                if (condition.get(content) == null) {
                    pair = new MatchPair();
                    condition.put(content, pair);
                } else {
                    pair = condition.get(content);
                }
            }else if ("=".equals(separator)) { // ---(2) 
                // =以及!=两个分隔符表示KV的分界线
                if (pair == null) {
                    throw new ParseException("..."");
                }
                values = pair.matches;
                values.add(content);
            }else if ("!=".equals(separator)) { // ---(5)
                if (pair == null) {
                    throw new ParseException("...");
                }
                values = pair.mismatches;
                values.add(content);
            }else if (",".equals(separator)) { // ---(3)
                // 逗号分隔符表示有多个Value值
                if (values == null || values.isEmpty()) {
                    throw new ParseException("...");
                }
                values.add(content);
            } else {
                throw new ParseException("...");
            }
        }
        return condition;
    }

介绍完 parseRule() 方法的实现之后,我们可以再通过下面这个条件表达式示例的解析流程,更深入地体会 parseRule() 方法的工作原理:

    host = 2.2.2.2,1.1.1.1,3.3.3.3 & method !=get => host = 1.2.3.4

经过 ROUTE_PATTERN 正则表达式的分组之后,我们得到如下分组:

202308162142505543.png

我们先来看 => 之前的 Consumer 匹配规则的处理。

  1. 分组 1 中,separator 为空字符串,content 为 host 字符串。此时会进入上面示例代码展示的 parseRule() 方法中(1)处的分支,创建 MatchPair 对象,并以 host 为 Key 记录到 condition 集合中。
  2. 分组 2 中,separator 为 "=" 空字符串,content 为 "2.2.2.2" 字符串。处理该分组时,会进入 parseRule() 方法中(2) 处的分支,在 MatchPair 的 matches 集合中添加 "2.2.2.2" 字符串。
  3. 分组 3 中,separator 为 "," 字符串,content 为 "3.3.3.3" 字符串。处理该分组时,会进入 parseRule() 方法中(3)处的分支,继续向 MatchPair 的 matches 集合中添加 "3.3.3.3" 字符串。
  4. 分组 4 中,separator 为 "&" 字符串,content 为 "method" 字符串。处理该分组时,会进入 parseRule() 方法中(4)处的分支,创建新的 MatchPair 对象,并以 method 为 Key 记录到 condition 集合中。
  5. 分组 5 中,separator 为 "!=" 字符串,content 为 "get" 字符串。处理该分组时,会进入 parseRule() 方法中(5)处的分支,向步骤 4 新建的 MatchPair 对象中的 mismatches 集合添加 "get" 字符串。

最后,我们得到的 whenCondition 集合如下图所示:

202308162142511164.png

同理,parseRule() 方法解析上述表达式 => 之后的规则得到的 thenCondition 集合,如下图所示:

202308162142517065.png

了解了 ConditionRouter 解析规则的流程以及 MatchPair 内部的匹配原则之后,ConditionRouter 中最后一个需要介绍的内容就是它的 route() 方法了。

ConditionRouter.route() 方法首先会尝试前面创建的 whenCondition 集合,判断此次发起调用的 Consumer 是否符合表达式中 => 之前的 Consumer 过滤条件,若不符合,直接返回整个 invokers 集合;若符合,则通过 thenCondition 集合对 invokers 集合进行过滤,得到符合 Provider 过滤条件的 Invoker 集合,然后返回给上层调用方。ConditionRouter.route() 方法的核心实现如下:

    // ConditionRouter.java
    
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
            throws RpcException {
    
        // ...通过enable字段判断当前ConditionRouter对象是否可用
    
        // ...当前invokers集合为空,则直接返回
    
        // 匹配发起请求的Consumer是否符合表达式中=>之前的过滤条件
        if (!matchWhen(url, invocation)) { 
            return invokers;
        }
    
        // 判断=>之后是否存在Provider过滤条件,若不存在则直接返回空集合,表示无Provider可用
        List<Invoker<T>> result = new ArrayList<Invoker<T>>();
        if (thenCondition == null) { 
            return result;
        }
    
        // 逐个判断Invoker是否符合表达式中=>之后的过滤条件
        for (Invoker<T> invoker : invokers) { 
            if (matchThen(invoker.getUrl(), url)) {
                // 记录符合条件的Invoker
                result.add(invoker); 
            }
        }
    
        if (!result.isEmpty()) {
            return result;
        } else if (force) { 
            // 在无Invoker符合条件时,根据force决定是返回空集合还是返回全部Invoker
            return result;
        }
    
        return invokers;
    }

2.3 ScriptRouterFactory

ScriptRouterFactory 的扩展名为 script,其 getRouter() 方法中会创建一个 ScriptRouter 对象并返回:

    // ScriptRouterFactory.java
    
    public class ScriptRouterFactory implements RouterFactory {
    
        public static final String NAME = "script";
    
        @Override
        public Router getRouter(URL url) {
            return new ScriptRouter(url);
        }
    }

ScriptRouter

ScriptRouter 支持 JDK 脚本引擎的所有脚本 ,例如,JavaScript、JRuby、Groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。下面我们就定义一个 route() 函数进行 host 过滤:

    function route(invokers, invocation, context){
        var result = new java.util.ArrayList(invokers.size()); 
    
        var targetHost = new java.util.ArrayList();
        targetHost.add("10.134.108.2"); 
    
        // 遍历Invoker集合
        for (var i = 0; i < invokers.length; i) {  
            // 判断Invoker的host是否符合条件
            if(targetHost.contains(invokers[i].getUrl().getHost())){
                result.add(invokers[i]);
            }
        }
        return result;
    }
    
    // 立即执行route()函数
    route(invokers, invocation, context)

我们可以将上面这段代码进行编码并作为 rule 参数的值添加到 URL 中,在这个 URL 传入 ScriptRouter 的构造函数时,即可被 ScriptRouter 解析。ScriptRouter 的 核心字段 有如下几个:

  • url(URL 类型):路由规则的 URL,可以从 rule 参数中获取具体的路由规则。
  • priority(int 类型):路由规则的优先级,用于排序,该字段值越大,优先级越高,默认值为 0。
  • ENGINES(ConcurrentHashMap 类型):这是一个 static 集合,其中的 Key 是脚本语言的名称,Value 是对应的 ScriptEngine 对象。这里会按照脚本语言的类型复用 ScriptEngine 对象。
  • engine(ScriptEngine 类型):当前 ScriptRouter 使用的 ScriptEngine 对象。
  • rule(String 类型):当前 ScriptRouter 使用的具体脚本内容。
  • function(CompiledScript 类型):根据 rule 这个具体脚本内容编译得到。

在 ScriptRouter 的构造函数中,首先会初始化 url 字段以及 priority 字段(用于排序),然后根据 URL 中的 type 参数初始化 engine、rule 和 function 三个核心字段 ,具体实现如下:

    // ScriptRouter.java
    
    public ScriptRouter(URL url) {
    
        this.url = url;
        this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);
    
        // 根据URL中的type参数值,从ENGINES集合中获取对应的ScriptEngine对象
        engine = getEngine(url);
    
        // 获取URL中的rule参数值,即为具体的脚本
        rule = getRule(url);
    
        Compilable compilable = (Compilable) engine;
    
        // 编译rule字段中的脚本,得到function字段
        function = compilable.compile(rule);
    }

接下来看 ScriptRouter 对 route() 方法的实现,其中首先会创建调用 function 函数所需的入参,也就是 Bindings 对象,然后调用 function 函数得到过滤后的 Invoker 集合,最后通过 getRoutedInvokers() 方法整理 Invoker 集合得到最终的返回值:

    // ScriptRouter.java
    
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
    
        // 创建Bindings对象作为function函数的入参
        Bindings bindings = createBindings(invokers, invocation);
    
        if (function == null) {
            return invokers;
        }
    
        // 调用function函数,并在getRoutedInvokers()方法中整理得到的Invoker集合
        return getRoutedInvokers(function.eval(bindings));
    }
    
    private <T> Bindings createBindings(List<Invoker<T>> invokers, Invocation invocation) {
        Bindings bindings = engine.createBindings();
    
        // 与前面的javascript的示例脚本结合,我们可以看到这里在Bindings中为脚本中的route()函数提供了invokers、Invocation、context三个参数
        bindings.put("invokers", new ArrayList<>(invokers));
        bindings.put("invocation", invocation);
        bindings.put("context", RpcContext.getContext());
    
        return bindings;
    }

2.4 FileRouterFactory

FileRouterFactory 是 ScriptRouterFactory 的装饰器 ,其扩展名为 file,FileRouterFactory 在 ScriptRouterFactory 基础上 增加了读取文件的能力 。我们可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径,FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。

下面我们来看 FileRouterFactory 对 getRouter() 方法的具体实现,其中完成了 file 协议的 URL 到 script 协议 URL 的转换,如下是一个转换示例,首先会将 file:// 协议转换成 script:// 协议,然后会添加 type 参数和 rule 参数,其中 type 参数值根据文件后缀名确定,该示例为 js,rule 参数值为文件内容。

202308162142521916.png

我们可以再结合接下来这个示例分析 getRouter() 方法的具体实现:

    // FileRouterFactory.java
    
    public class FileRouterFactory implements RouterFactory {
    
        public static final String NAME = "file";
        private RouterFactory routerFactory;
    
        public void setRouterFactory(RouterFactory routerFactory) {
            this.routerFactory = routerFactory;
        }
    
        @Override
        public Router getRouter(URL url) {
            try {
                // 默认使用script协议
                String protocol = url.getParameter(ROUTER_KEY, ScriptRouterFactory.NAME); 
                String type = null;
                String path = url.getPath();
    
                // 获取脚本文件的语言类型
                if (path != null) {
                    int i = path.lastIndexOf('.');
                    if (i > 0) {
                        type = path.substring(i + 1);
                    }
                }
                // 读取脚本文件中的内容
                String rule = IOUtils.read(new FileReader(new File(url.getAbsolutePath())));
    
                // 创建script协议的URL
                boolean runtime = url.getParameter(RUNTIME_KEY, false);
                URL script = URLBuilder.from(url)
                        .setProtocol(protocol)
                        .addParameter(TYPE_KEY, type)
                        .addParameter(RUNTIME_KEY, runtime)
                        .addParameterAndEncoded(RULE_KEY, rule)
                        .build();
                // 获取script对应的Router实现
                return routerFactory.getRouter(script);
            } catch (IOException e) {
                throw new IllegalStateException(e.getMessage(), e);
            }
        }
    }

2.5 TagRouterFactory

TagRouterFactory 作为 RouterFactory 接口的扩展实现 ,其扩展名为 tag。但是需要注意的是,TagRouterFactory 与 ConditionRouterFactory、ScriptRouterFactory 的不同之处在于:它是 通过继承 CacheableRouterFactory 这个抽象类,间接实现了 RouterFactory 接口

    // TagRouterFactory.java
    
    @Activate(order = 100)
    public class TagRouterFactory extends CacheableRouterFactory {
    
        public static final String NAME = "tag";
    
        @Override
        protected Router createRouter(URL url) {
            return new TagRouter(url);
        }
    }

CacheableRouterFactory 抽象类中维护了一个 ConcurrentMap 集合,用来缓存 Router,其中的 Key 是 ServiceKey。在 CacheableRouterFactory 的 getRouter() 方法中,会优先根据 URL 的 ServiceKey 查询 routerMap 集合,查询失败之后会调用 createRouter() 抽象方法来创建相应的 Router 对象。在 TagRouterFactory.createRouter() 方法中,创建的自然就是 TagRouter 对象了。

    // CacheableRouterFactory.java
    
    public abstract class CacheableRouterFactory implements RouterFactory {
        private ConcurrentMap<String, Router> routerMap = new ConcurrentHashMap<>();
    
        @Override
        public Router getRouter(URL url) {
            return routerMap.computeIfAbsent(url.getServiceKey(), k -> createRouter(url));
        }
    
        protected abstract Router createRouter(URL url);
    }

TagRouter

通过 TagRouter,我们可以将某一个或多个 Provider 划分到同一分组,约束流量只在指定分组中流转,这样就可以轻松达到流量隔离的目的,从而支持灰度发布等场景。

目前,Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意 保存在 attachment 中的值将会在一次完整的远程调用中持续传递 ,我们只需要在起始调用时进行设置,就可以达到标签的持续传递。

了解了 Tag 的基本概念和功能之后,我再简单介绍一个 Tag 的使用示例。

在实际的开发测试中,一个完整的请求会涉及非常多的 Provider,分属不同团队进行维护,这些团队每天都会处理不同的需求,并在其负责的 Provider 服务中进行修改,如果所有团队都使用一套测试环境,那么测试环境就会变得很不稳定。如下图所示,4 个 Provider 分属不同的团队管理,Provider 2 和 Provider 4 在测试环境测试,部署了有 Bug 的版本,这样就会导致整个测试环境无法正常处理请求,在这样一个不稳定的测试环境中排查 Bug 是非常困难的,因为可能排查到最后,发现是别人的 Bug。

202308162142527967.png

为了解决上述问题,我们可以针对每个需求分别独立出一套测试环境,但是这个方案会占用大量机器,前期的搭建成本以及后续的维护成本也都非常高。

下面是一个通过 Tag 方式实现环境隔离的架构图,其中,需求 1 对 Provider 2 的请求会全部落到有需求 1 标签的 Provider 上,其他 Provider 使用稳定测试环境中的 Provider;需求 2 对 Provider 4 的请求会全部落到有需求 2 标签的 Provider 4 上,其他 Provider 使用稳定测试环境中的 Provider。

202308162142533458.png

在一些特殊场景中,会有 Tag 降级的场景,比如找不到对应 Tag 的 Provider,会按照一定的规则进行降级。如果在 Provider 集群中不存在与请求 Tag 对应的 Provider 节点,则默认将降级请求 Tag 为空的 Provider;如果希望在找不到匹配 Tag 的 Provider 节点时抛出异常的话,我们需设置 request.tag.force = true。

如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider,也就是说即使集群中存在可用的服务,若 Tag 不匹配也就无法调用。一句话总结, 携带 Tag 的请求可以降级访问到无 Tag 的 Provider,但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider


下面我们再来看 TagRouter 的具体实现。在 TagRouter 中持有一个 TagRouterRule 对象的引用,在 TagRouterRule 中维护了一个 Tag 集合,而在每个 Tag 对象中又都维护了一个 Tag 的名称,以及 Tag 绑定的网络地址集合,如下图所示:

202308162142539369.png

另外,在 TagRouterRule 中还维护了 addressToTagnames、tagnameToAddresses 两个集合(都是 Map `> 类型),分别记录了 Tag 名称到各个 address 的映射以及 address 到 Tag 名称的映射。在 TagRouterRule 的 init() 方法中,会根据 tags 集合初始化这两个集合。

了解了 TagRouterRule 的基本构造之后,我们继续来看 TagRouter 构造 TagRouterRule 的过程。TagRouter 除了实现了 Router 接口之外,还实现了 ConfigurationListener 接口,如下图所示:

2023081621425445010.png

ConfigurationListener 用于监听配置的变化,其中就包括 TagRouterRule 配置的变更 。当我们通过动态更新 TagRouterRule 配置的时候,就会触发 ConfigurationListener 接口的 process() 方法,TagRouter 对 process() 方法的实现如下:

    // TagRouter.java
    
    public synchronized void process(ConfigChangedEvent event) {
    
        // DELETED事件会直接清空tagRouterRule
        if (event.getChangeType().equals(ConfigChangeType.DELETED)) {
            this.tagRouterRule = null;
        } 
        // 其他事件会解析最新的路由规则,并记录到tagRouterRule字段中
        else { 
            this.tagRouterRule = TagRuleParser.parse(event.getContent());
        }
    }

我们可以看到,如果是删除配置的操作,则直接将 tagRouterRule 设置为 null,如果是修改或新增配置,则通过 TagRuleParser 解析传入的配置,得到对应的 TagRouterRule 对象。TagRuleParser 可以解析 yaml 格式的 TagRouterRule 配置,下面是一个配置示例:

    force: false
    runtime: true
    enabled: false
    priority: 1
    key: demo-provider
    tags:
      - name: tag1
        addresses: null
      - name: tag2
        addresses: ["30.5.120.37:20880"]
      - name: tag3
        addresses: []

经过 TagRuleParser 解析得到的 TagRouterRule 结构,如下所示:

2023081621425504911.png

除了上图展示的几个集合字段,TagRouterRule 还从 AbstractRouterRule 抽象类继承了一些控制字段,后面介绍的 ConditionRouterRule 也继承了 AbstractRouterRule。

2023081621425576412.png

AbstractRouterRule 中核心字段的具体含义大致可总结为如下。

  • key(string 类型)、scope(string 类型):key 明确规则体作用在哪个服务或应用。scope 为 service 时,key 由 [{group}:]{service}[:{version}] 构成;scope 为 application 时,key 为 application 的名称。
  • rawRule(string 类型):记录了路由规则解析前的原始字符串配置。
  • runtime(boolean 类型):表示是否在每次调用时执行该路由规则。如果设置为 false,则会在 Provider 列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。
  • force(boolean 类型):当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效。该字段默认值为 false。
  • valid(boolean 类型):用于标识解析生成当前 RouterRule 对象的配置是否合法。
  • enabled(boolean 类型):标识当前路由规则是否生效。
  • priority(int 类型):用于表示当前 RouterRule 的优先级。
  • dynamic(boolean 类型):表示该路由规则是否为持久数据,当注册方退出时,路由规则是否依然存在。

我们可以看到,AbstractRouterRule 中的核心字段与前面的示例配置是一一对应的。

我们知道,Router 最终目的是要过滤符合条件的 Invoker 对象,下面我们一起来看 TagRouter 是如何使用 TagRouterRule 路由逻辑进行 Invoker 过滤的,大致步骤如下。

  1. 如果 invokers 为空,直接返回空集合。

  2. 检查关联的 tagRouterRule 对象是否可用,如果不可用,则会直接调用 filterUsingStaticTag() 方法进行过滤,并返回过滤结果。在 filterUsingStaticTag() 方法中,会比较请求携带的 tag 值与 Provider URL 中的 tag 参数值。

  3. 获取此次调用的 tag 信息,这里会尝试从 Invocation 以及 URL 的参数中获取。

  4. 如果

    此次请求指定了 tag 信息

    ,则首先会获取 tag 关联的 address 集合。

    1. 如果 address 集合不为空,则根据该 address 集合中的地址,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。
    2. 如果 address 集合为空,则会将请求携带的 tag 值与 Provider URL 中的 tag 参数值进行比较,匹配出符合条件的 Invoker 集合。如果存在符合条件的 Invoker,则直接将过滤得到的 Invoker 集合返回;如果不存在,就会根据 force 配置决定是否返回空 Invoker 集合。
    3. 如果 force 配置为 false,且符合条件的 Invoker 集合为空,则返回所有不包含任何 tag 的 Provider 列表。
  5. 如果 此次请求未携带 tag 信息 ,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中,最后,将 Provider URL 中的 tag 值与 TagRouterRule 中的 tag 名称进行比较,得到最终的 Invoker 集合。

上述流程的具体实现是在 TagRouter.route() 方法中,如下所示:

    // TagRouter.java
    
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
    
        //... 如果invokers为空,直接返回空集合(略)
        final TagRouterRule tagRouterRuleCopy = tagRouterRule;
        if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
            return filterUsingStaticTag(invokers, url, invocation);
        }
    
        // 检查关联的tagRouterRule对象是否可用,如果不可用,则会直接调用filterUsingStaticTag() 方法进行过滤
        List<Invoker<T>> result = invokers;
    
        // 获取此次调用的tag信息,尝试从Invocation以及URL中获取
        String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY)) ? url.getParameter(TAG_KEY) :
                invocation.getAttachment(TAG_KEY);
    
        if (StringUtils.isNotEmpty(tag)) { // 此次请求一个特殊的tag
            // 获取tag关联的address集合
            List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
            if (CollectionUtils.isNotEmpty(addresses)) {
                // 根据上面的address集合匹配符合条件的Invoker
                result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
    
                // 如果存在符合条件的Invoker,则直接将过滤得到的Invoker集合返回
                // 如果不存在符合条件的Invoker,根据force配置决定是否返回空Invoker集合
                if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
                    return result;
                }
            } else {
                // 如果 address 集合为空,则会将请求携带的 tag 与 Provider URL 中的 tag 参数值进行比较,匹配出符合条件的 Invoker 集合。
                result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
            }
    
            if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
                return result; // 存在符合条件的Invoker或是force配置为true
            }else { // 如果 force 配置为 false,且符合条件的 Invoker 集合为空,则返回所有不包含任何 tag 的 Provider 列表。
                List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
                        tagRouterRuleCopy.getAddresses()));
                return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
            }
        } else {
            // 如果此次请求未携带 tag 信息,则会先获取 TagRouterRule 规则中全部 tag 关联的 address 集合。
            List<String> addresses = tagRouterRuleCopy.getAddresses();
            if (CollectionUtils.isNotEmpty(addresses)) {
                // 如果 address 集合不为空,则过滤出不在 address 集合中的 Invoker 并添加到结果集合中。
                result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
                if (CollectionUtils.isEmpty(result)) {
                    return result;
                }
            }
    
            // 如果不存在符合条件的 Invoker 或是 address 集合为空,则会将请求携带的 tag 与 Provider URL 中的 tag 参数值进行比较,得到最终的 Invoker 集合。
            return filterInvoker(result, invoker -> {
                String localTag = invoker.getUrl().getParameter(TAG_KEY);
                return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
            });
        }
    }

2.6 ServiceRouterFactory

除了前文介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外, ServiceRouterFactory 也继承 CachabelRouterFactory,具有了缓存的能力 ,具体继承关系如下图所示:

2023081621425628713.png

    // ServiceRouterFactory.java
    
    @Activate(order = 300)
    public class ServiceRouterFactory extends CacheableRouterFactory {
    
        public static final String NAME = "service";
    
        @Override
        protected Router createRouter(URL url) {
            return new ServiceRouter(url);
        }
    }

ServiceRouterFactory 创建的是 ServiceRouter,与 ServiceRouter 类似的是 AppRouter, 两者都继承了 ListenableRouter 抽象类 (虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示:

2023081621425685514.png

ListenableRouter

ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力 ,ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似:

  • 对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;
  • 对于 ADDEDUPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。

这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合(List<String> 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。

整个解析 ConditionRouterRule 的过程,与前文介绍的解析 TagRouterRule 的流程类似,这里不再赘述。

在 ListenableRouter 的 route() 方法中,会遍历全部 ConditionRouter 过滤出符合全部路由条件的 Invoker 集合,具体实现如下:

    // ListenableRouter.java
    
    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        if (CollectionUtils.isEmpty(invokers) || conditionRouters.size() == 0) {
            return invokers; // 检查边界条件,直接返回invokers集合
        } 
    
        for (Router router : conditionRouters) { // 路由规则进行过滤
            invokers = router.route(invokers, url, invocation);
        }
        return invokers;
    }

ServiceRouter 和 AppRouter 都是简单地继承了 ListenableRouter 抽象类,且没有覆盖 ListenableRouter 的任何方法,两者只有以下两点区别。

  • 一个是 priority 字段值不同 。ServiceRouter 为 140,AppRouter 为 150,也就是说 ServiceRouter 要先于 AppRouter 执行。
  • 另一个是获取 ConditionRouterRule 配置的 Key 不同 。ServiceRouter 使用的 RuleKey 是由 {interface}:[version]:[group] 三部分构成,获取的是一个服务对应的 ConditionRouterRule。AppRouter 使用的 RuleKey 是 URL 中的 application 参数值,获取的是一个服务实例对应的 ConditionRouterRule。

三、总结

本章,我重点介绍了 Router 接口的相关内容。首先我介绍了 RouterChain 的核心实现以及构建过程,然后讲解了 RouterFactory 接口和 Router 接口中核心方法的功能。最后,我对RouterFactory的所有默认实现进行了分析。


Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文