dubbo路由规则及源码分析

Dubbo高级应用-服务治理 目录 1、dubbo-admin-2.7.x版本安装部署 1.1 下载源码 1.2 部署访问 2、路由规则 2.1 Dubbo API配置 2.2 管理控制台配置 3、规则动态配置 3.1 应用粒度 3.2 服务粒度 4、服务降级 5、集群容错 5.1 集群容错模式 5.2 集群模式配置 6、黑白名单 6.1 应用粒度 6.2 服务粒度 7、权重调整 7.1 应用粒度 7.2 服务粒度 8、负载均衡 8.1 官方提供 8.2 自定义策略 9、优雅停机 9. 阅读详情

路由规则

路由规则 [1] 决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展 [2]。

写入路由规则

向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成

RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181"));
registry.register(URL.valueOf("route://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11")));

其中:

  • route:// 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。
  • 0.0.0.0 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。
  • com.foo.BarService 表示只对指定服务生效,必填。
  • group=foo 对指定服务的指定group生效,不填表示对未配置group的指定服务生效
  • version=1.0对指定服务的指定version生效,不填表示对未配置version的指定服务生效
  • category=routers 表示该数据为动态配置类型,必填。
  • dynamic=false 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。
  • enabled=true 覆盖规则是否生效,可不填,缺省生效。
  • force=false 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 false。
  • runtime=false 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 false。
  • priority=1 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。
  • rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11") 表示路由规则的内容,必填。

条件路由规则

基于条件表达式的路由规则,如:host = 10.20.153.10 => host = 10.20.153.11

规则:

  • => 之前的为消费者匹配条件,所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。
  • => 之后为提供者地址列表的过滤条件,所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。
  • 如果匹配条件为空,表示对所有消费方应用,如:=> host != 10.20.153.11
  • 如果过滤条件为空,表示禁止访问,如:host = 10.20.153.10 =>

表达式:

参数支持:

  • 服务调用信息,如:method, argument 等,暂不支持参数路由
  • URL 本身的字段,如:protocol, host, port 等
  • 以及 URL 上的所有参数,如:application, organization 等

条件支持:

  • 等号 = 表示"匹配",如:host = 10.20.153.10
  • 不等号 != 表示"不匹配",如:host != 10.20.153.10

值支持:

  • 以逗号 , 分隔多个值,如:host != 10.20.153.10,10.20.153.11
  • 以星号 * 结尾,表示通配,如:host != 10.20.*
  • 以美元符 $ 开头,表示引用消费者参数,如:host = $host

示例:

  1. 排除预发布机:

    => host != 172.22.3.91
    
  2. 白名单 [3]:

    host != 10.20.153.10,10.20.153.11 =>
    
  3. 黑名单:

    host = 10.20.153.10,10.20.153.11 =>
    
  4. 服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉:

    => host = 172.22.3.1*,172.22.3.2*
    
  5. 为重要应用提供额外的机器:

    application != kylin => host != 172.22.3.95,172.22.3.96
    
  6. 读写分离:

    method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96
    method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98
    
  7. 前后台分离:

    application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93
    application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96
    
  8. 隔离不同机房网段:

    host != 172.22.3.* => host != 172.22.3.*
    
  9. 提供者与消费者部署在同集群内,本机只访问本机的服务:

    => host = $host
    

用Filter实现黑白名单

https://blog.youkuaiyun.com/o9109003234/article/details/83869760

注:还有种方式实现黑白名单是在请求服务时把这边的路由规则加进去,比较蠢,不再赘述,也可参考dubbo开源源码的路由规则测试相关类中的模拟实现

脚本路由规则

脚本路由规则 [4] 支持 JDK 脚本引擎的所有脚本,比如:javascript, jruby, groovy 等,通过 type=javascript 参数设置脚本类型,缺省为 javascript。

"script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { ... } (invokers))")

基于脚本引擎的路由规则,如:

(function route(invokers) {
    var result = new java.util.ArrayList(invokers.size());
    for (i = 0; i < invokers.size(); i ++) {
        if ("10.20.153.10".equals(invokers.get(i).getUrl().getHost())) {
            result.add(invokers.get(i));
        }
    }
    return result;
} (invokers)); // 表示立即执行方法

标签路由规则

标签路由规则 [5] ,当应用选择装配标签路由(TagRouter)之后,一次 dubbo 调用能够根据请求携带的 tag 标签智能地选择对应 tag 的服务提供者进行调用。

服务提供者

  1. 给应用装配标签路由器:
@Bean
public ApplicationConfig applicationConfig() {
    ApplicationConfig applicationConfig = new ApplicationConfig();
    applicationConfig.setName("provider-book");
    applicationConfig.setQosEnable(false);
    // instruct tag router
    Map<String,String> parameters = new HashMap<>();
    parameters.put(Constants.ROUTER_KEY, "tag");
    applicationConfig.setParameters(parameters);
    return applicationConfig;
}
  1. 给应用设置具体的标签:
@Bean
public ProviderConfig providerConfig(){
	ProviderConfig providerConfig = new ProviderConfig();
	providerConfig.setTag("red");
	return providerConfig;
}

应用未装配 tag 属性或服务提供者未设置 tag 属性,都将被认为是默认的应用,这些默认应用将会在调用无法匹配 provider 时当作降级方案。

服务消费者

RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"red");

请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。

目前仅仅支持 hardcoding 的方式设置 requestTag。注意到 RpcContext 是线程绑定的,优雅的使用 TagRouter 特性,建议通过 servlet 过滤器(在 web 环境下),或者定制的 SPI 过滤器设置 requestTag。

规则描述

  1. request.tag=red 时优先选择 tag=red 的 provider。若集群中不存在与请求标记对应的服务,可以降级请求 tag=null 的 provider,即默认 provider。

  2. request.tag=null 时,只会匹配 tag=null 的 provider。即使集群中存在可用的服务,若 tag 不匹配就无法调用,这与规则1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。


  1. 2.2.0 以上版本支持 ↩︎

  2. 路由规则扩展点:路由扩展 ↩︎

  3. 注意:一个服务只能有一条白名单规则,否则两条规则交叉,就都被筛选掉了 ↩︎

  4. 注意:脚本没有沙箱约束,可执行任意代码,存在后门风险 ↩︎

  5. 该特性在 2.7.0 以上版本支持 ↩︎

路由扩展

扩展说明

从多个服务提者方中选择一个进行调用。

扩展接口

  • org.apache.dubbo.rpc.cluster.RouterFactory
  • org.apache.dubbo.rpc.cluster.Router

已知扩展

  • org.apache.dubbo.rpc.cluster.router.ScriptRouterFactory
  • org.apache.dubbo.rpc.cluster.router.FileRouterFactory

扩展示例

Maven 项目结构:

src
 |-main
    |-java
        |-com
            |-xxx
                |-XxxRouterFactory.java (实现LoadBalance接口)
    |-resources
        |-META-INF
            |-dubbo
                |-org.apache.dubbo.rpc.cluster.RouterFactory (纯文本文件,内容为:xxx=com.xxx.XxxRouterFactory)

XxxRouterFactory.java:

package com.xxx;
import org.apache.dubbo.rpc.cluster.RouterFactory;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.RpcException;
public class XxxRouterFactory implements RouterFactory {
    public <T> List<Invoker<T>> select(List<Invoker<T>> invokers, Invocation invocation) throws RpcException {
        // ...
    }
}

META-INF/dubbo/org.apache.dubbo.rpc.cluster.RouterFactory:

xxx=com.xxx.XxxRouterFactory

1. 简介

服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。在详细分析服务路由的源码之前,先来介绍一下服务路由是什么。服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。其中条件路由是我们最常使用的,标签路由是一个新的实现,暂时还未发布,该实现预计会在 2.7.x 版本中发布。本篇文章将分析条件路由相关源码,脚本路由和标签路由这里就不分析了。

2. 源码分析

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

host = 10.20.153.10 => host = 10.20.153.11

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

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

如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。官方文档中对条件路由进行了比较详细的介绍,大家可以参考下,这里就不过多说明了。

条件路由实现类 ConditionRouter 在进行工作前,需要先对用户配置的路由规则进行解析,得到一系列的条件。然后再根据这些条件对服务进行路由。本章将分两节进行说明,2.1节介绍表达式解析过程。2.2 节介绍服务路由的过程。下面,我们先从表达式解析过程看起。

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);
        // 将解析出的匹配规则分别赋值给 whenCondition 和 thenCondition 成员变量
        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;
            }
        }    
        // 全部失配,则返回 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 ((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. 总结

本篇文章对条件路由的表达式解析和服务路由过程进行了较为细致的分析。总的来说,条件路由的代码还是有一些复杂的,需要静下心来看。在阅读条件路由代码的过程中,要多调试。 关于脚本路由规则代码写的比较简单,参考https://segmentfault.com/a/1190000018141200#articleHeader7 以及 https://github.com/CrazyHZM/dubbo/tree/analyze-2.6.x/dubbo-cluster/src/main/java/com/alibaba/dubbo/rpc/cluster/router 2.7.X后增加了标签路由机制,有兴趣可以研读相关源码。同事生产环境需注意dubbo版本问题。

dubbo自定义负载均衡策略及相关源码解析 背景 最近在研究即时消息推送系统(IM系统)的架构,自己用netty开发了基础网络通信部分,用来维护TCP连接,在客户端上线时将用户标识userId和路由信息存在redis中,通过dubbo向上层模块提供消息推送接口。 在上层消息转发路由模块设计时,遇到了一个问题,dubbo接口调用时要根据路由信息中的ip指定对应的provider服务器。研究了下资料发现可以通过dubbo SPI自定义负载均衡策... 阅读详情

相关推荐

告别‘打架’的检测头:手把手教你用PyTorch实现YOLOv11解耦头(附完整代码)

本文详细解析了YOLOv11解耦检测头的创新设计与PyTorch实现,通过解耦分类和回归任务,显著提升目标检测性能。文章包含完整的代码实现、架构设计解析及工程调优技巧,帮助开发者深入理解并应用这一先进技术。

weixin_29031057的博客 301

Dubbo 源码分析 – 集群容错之 Router

本篇文章对条件路由的表达式解析和服务路由过程进行了较为细致的分析。总的来说,条件路由的代码还是有一些复杂的,需要静下心来看。在阅读条件路由代码的过程中,要多调试。一般的框架都会有单元测试,Dubbo 也不例外,因此大家可以直接通过 ConditionRouterTest 对条件路由进行调试,无需自己手写测试用例。

二师兄撒欢之地 515

MatLab Desired port was :31515解决方案

前言:使用的MatLabR2022b今天突然出现了错误,在程序中打不开文件。后尝试了下面的方法,可以解决。后面还有另一种方法,上面方法如果行不通,大家可以尝试一下。搜索栏输入:firewall.cpl。找到相关项,右键属性,设置为允许。

qq_42212501的博客 1338

Dubbo 黑白名单机制详解

黑名单:指的是不允许访问的 IP 地址、服务或用户列表。黑名单中的条目将被拒绝访问系统或服务。白名单:指的是允许访问的 IP 地址、服务或用户列表。白名单中的条目将被允许访问系统或服务。在服务治理中,黑白名单机制可以用于控制哪些客户端能够调用服务,防止非法或不受信任的客户端进行访问。Dubbo 的黑白名单机制为服务提供了强有力的访问控制手段。通过合理配置黑白名单,企业可以有效地提升系统的安全性和可控性。在使用过程中,务必根据实际需求进行精细化管理,并定期审查和更新配置。

FireFox1997 1036

Altium AD20差分对走线设置方法

AD20的PCB差分对走线设置 1、原理图添加差分对 先在原理图中,讲要设置差分对的网络对添加网络标签。 名称前面的内容要相同,后缀用 _P 和 _N 加以区分。 最后使用 放置差分对,将两个网络标记。 2、编译,更改到PCB 编译,更新到PCB,确认差分对网络的出现。 验证更改,执行更改。将改动更新到PCB中。 PCB中,可通过 右下角 Panels - 调出 PCB 显示。来对已添加的差分对进行查看。 这时原本的走线有一点点小问题,需要删掉原来走线,设置差分对规则,并重新布线。

Mark_md的博客 3万+

细胞膜电学特性的分数阶微积分建模

分数阶微积分在细胞膜电学特性建模中的应用主要体现在对细胞膜的电容行为和神经元放电活动的更精确描述上。

冰河世纪20的博客 619

为dubbo接口增加IP白名单

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CFXXVRoq-1661553422394)(http://omh46px9n.bkt.clouddn.com/17-11-4/32927644.jpg)]在/resources目录下,新建META-INF/dubbo目录,并新建一个名为com.alibaba.dubbo.rpc.Filter的文本文件。在dubbo中,通过扩展Filter接口,可以实现IP白名单的功能。然后我们实现dubbo的Filter接口。...

web13293720476的博客 796

【Frida Android】实战篇4:SSL Pinning 证书绑定绕过 Hook 教程(一)

在前两章中,我们学习了中间人抓包与 Hook 抓包的基础方法,这些方法在理想环境下能够有效获取网络请求数据。但在实际的企业级应用中,出于安全防护需求,很多 APP 会启用证书绑定(SSL Pinning)机制。这种机制会让客户端对服务器证书的校验更为严格,导致常规的抓包方法(即使将 BurpSuite/Charles 证书安装到系统信任区)也无法生效。因此,从本章开始,我们将系统学习 SSL Pinning 机制的绕过技术。SSL Pinning(证书绑定)是客户端校验服务器身份的进阶安全机制。

水滴石穿 1609

Zookeeper 查看 Dubbo 服务列表

黑白名单异常是 Dubbo 服务常见的问题。 异常示例: com.alibaba.dubbo.rpc.RpcException: Forbid consumer 10.1.2.3 access service demo.service.DemoService from registry 10.1.2.3:2181 use dubbo version 2.8.4, Please check...

一个被IT搞的 7257

K230目标识别数字提问:模型只能识别近距离的目标,稍远一点就无法正确检测...如何解决?

🏆 本文收录于《全栈Bug调优(实战版)》专栏,致力于分享我在项目实战过程中遇到的各类Bug及其原因,并提供切实有效的解决方案。无论你是初学者还是经验丰富的开发者,本文将为你指引出一条更高效的Bug修复之路,助你早日登顶,迈向财富自由的梦想🚀!同时,欢迎大家关注、收藏、订阅本专栏,更多精彩内容正在持续更新中。让我们一起进步,Up!Up!Up!    备注: 部分问题/难题源自互联网,经过精心筛选和整理,结合数位十多年大厂实战经验资深大佬经验总结所得,数条可行方案供所需之人参考。

**My Coding Family** 1897

PyTorch深度学习实战(36)——Pix2Pix详解与实现

Pix2Pix 是基于生成对抗网络的图像转换框架,能够将输入图像转换为与之对应的输出图像,能够广泛用于图像到图像转换的任务,如风格转换、图像修复、语义标签到图像的转换等。Pix2Pix 的核心思想是通过对抗训练将输入图像和目标输出图像进行配对,使生成网络可以学习到输入图像到输出图像的映射关系。在本节中,将学习使用 Pix2Pix 根据给定轮廓生成图像。

盼小辉丶的博客 1万+

dubbo源码分析路由规则Route-15

在集中式环境中服务的机器台只有一台,这样对于服务不仅存在服务单点故障问题而且还存在流量问题。为了解决这个问题,就引入的分布式与集群概念。 分布式:一个业务分拆多个子业务,部署在不同的服务器上 集群:同一个业务,部署在多个服务器上 1、 dubbo 服务治理 当请求来临时,如何从多个服务器中,选择一个有效、合适的服务器,这个集群所需要面对一问题。所以在集群里面就引申出负载均衡(LoadBal...

Jack博客 924

电池管理系统(BMS)软件:CATL BMS二次开发_(1).CATL BMS软件概述

CATL BMS软件的保护机制是确保电池组安全和可靠运行的关键部分。通过高精度的传感器和智能算法,BMS可以实时监测电池组的状态,及时采取保护措施。二次开发中,可以根据特定的应用需求,增加新的传感器、优化算法、扩展通信接口等,进一步提高系统的性能和可靠性。维护和扩展则是确保系统长期稳定运行的重要手段。希望以上内容对您理解CATL BMS软件的保护机制有所帮助。如果您有任何进一步的问题或需要更详细的解释,欢迎随时提问。

zhubeibei168的博客 1457

Dubbo(十五)源码解析 之 服务路由

服务路由源码解析 1. 什么是服务路由 服务路由包含一条(或若干条)路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。Dubbo 目前提供了三种服务路由实现,分别为条件路由ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。其中条件路由是我们最常使用的。 下面将以条件路由为例来讲解服务路由的用法。 2. 路由规则的设置 路由规则是在 Dubbo 管控平台 Dubbo-Admin 中的。 (1) 启动管控平台 A、启动管控台

语雀同名:犬豪 yuque.com/qhao 1253

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

dubbo源码分析系列——dubbo-cluster模块源码分析

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_33971205的博客 234

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

dubbo源码解析(四十)集群——router

集群——router 目标:介绍dubbo中集群的路由,介绍dubbo-cluster下router包的源码。 前言 路由规则 决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展 。 源码分析 (一)ConditionRouterFactory public class ConditionRoute...

weixin_34250709的博客 68

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Dubbo架构篇 - 服务路由

本文解析Dubbo的服务路由中的条件路由

The Secret 1202

Dubbo 服务治理细节

为什么80%的码农都做不了架构师?>>> ...

weixin_34062469的博客 159

dubbo中消费者路由选择生产者的过程解析

最近帮忙同事解决了一个很奇怪的问题,首先我们启动一个某个服务的生产者服务,service版本号是1.0,然后我们启动了一个消费者来调用这个生产者reference版本号也设置为1.0,但是在启动消费者程序之后发现怎么也消费不到生产者服务,报错为: com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method xxx in the s...

m0_37633471的博客 1344

springboot菜鸟教程,Dubbo路由机制概述

引领完成Docker的安装、部署、管理和扩展,让其经历从测试到生产的整个开发生命周期,深入了解Docker适用于什么场景。并且这本Docker的学习权威指南介绍了其组件的基础知识,然后用Docker构建容器和服务来完成各种任务:利用Docker为新项目建立测试环境,演示如何使用持续集成的工作流集成Docker,如何构建应用程序服务和平台,如何使用Docker的API,如何扩展Docker。

2401_84010784的博客 533

Dubbo路由目录分析

温馨提示 个人觉得,看源码最好的方式就是看源码中写的测试代码 测试源码分析 org.apache.dubbo.rpc.cluster.directory.StaticDirectoryTest#testStaticDirectory public void testStaticDirectory() { // 第一步: 路由服务Router Router rout...

qq_30561643的博客 468

九、面向对象底层逻辑-Dubbo路由Router接口

本文深入探讨了Dubbo框架中Router接口的设计原理、核心实现及其在生产环境中的应用。Router接口作为Dubbo服务路由决策的核心抽象,支持多种流量管控策略,如条件路由、标签路由和权重路由等,以实现精准的请求路由。文章详细解析了Router接口的核心方法、内置路由策略对比、路由决策机制以及配置与扩展实践,并提供了生产级最佳实践和典型问题的解决方案。通过源码级实现分析,进一步揭示了路由链构建逻辑和条件路由解析器的内部工作机制。本文为开发者在复杂分布式服务架构中实现高效、灵活的路由策略提供了全面的指导。

u013883798的博客 1114

Dubbo进阶(十四):Router的实现

首先我们依然再看一遍Dubbo调用的流程 Router是什么 Router是一种约定的规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。比如服务A配置了调用的服务的IP是192.168.1.1,那么路由会过滤到除192.168.1.1之外的所有的服务,只会返回192.168.1.1。 通常服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。 在Dubbo 2.7.x的版本中,路由包含了条件路由、脚本路由、文件路由

程铭程铭你快成名的博客 1万+
上一篇: Dubbo-2.6.5 之前版本一定要注意的地方
下一篇: 原因为 1) 无法找到文档; 2) 无法读取文档; 3) 文档的根元素不是
Knight_Ren
博客等级 码龄10年 22粉丝 61原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Knight_Ren

资源不易,打赏随意

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值