当前位置: 首页 > 工具软件 > Dubbo Gateway > 使用案例 >

Dubbo架构篇 - 服务路由

陶和歌
2023-12-01

前言

服务目录在刷新Invoker列表的过程中,会通过Router进行服务路由,筛选出符合路由规则的服务提供者。

服务路由包含一条路由规则,路由规则决定了服务消费者可以调用哪些服务提供者。

Dubbo目前提供了三种服务路由实现 - 条件路由(ConditionRouter)、脚本路由(ScriptRouter)、标签路由(TagRouter)。


条件路由的使用

可以在服务治理控制台 Dubbo-Admin 写入路由规则

应用粒度

# app1的消费者只能消费所有端口为20880的服务实例
# app2的消费者只能消费所有端口为20881的服务实例

scope: application
key: governance-conditionrouter-consumer
enabled: true
force: true
runtine: true
conditions:
	- application=app1 => address=*:20880
	- application=app2 => address=*:20881

服务粒度

# DemoService的sayHello方法只能消费所有端口为20880的服务实例
# DemoService的sayHi方法只能消费所有端口为20881的服务实例

scope: service
key: org.apache.dubbo.samples.goverance.api.DemoService
enabled: true
force: true
runtime: true
conditions:
	- method=sayHello => address=*:20880
	- method=sayHi => address=*:20881

  • scope:表示路由规则的作用粒度,scope的取值会决定key的取值。必填。

    • application:应用粒度
    • service:服务粒度
  • key:明确规则体作用在哪个应用或者规则。必填

    • scope=application时,key取值为application名称。
    • scope=service时,key的取值为[{group}:]{service}[:{version}] 的组合。
  • conditions:定义具体的路由规则内容。必填

  • enabled:当前路由规则是否生效,缺省生效。

  • force:当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,缺省为 false。

  • runtime:是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设置为true,需要注意设置会影响调用的性能。缺省为false。

  • priority:路由规则的优先级,用于排序,优先级越大越靠前执行。缺省为0。

Conditions规则体

1、格式

  • => 之前的为消费者匹配条件,所有参数和消费者的URL进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。

  • => 之后的为提供者地址列表的过滤条件,所有参数和提供者的URL进行对比,消费者最终只拿到过滤后的地址列表。

  • 如果匹配条件为空,表示对所有消费者应用,比如:=> host != 10.20.153.11

  • 如果过滤条件为空,表示禁止访问,比如:host = 10.20.153.10 =>

2、表达式

参数支持

  • 服务调用信息,比如: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

3、示例

排除预发布机

=> host != 172.22.3.91

白名单(一个服务只能有一个白名单规则,否则两条规则交叉,都被筛选掉了)

register.ip != 10.20.153.10,10.20.153.11 =>

黑名单

register.ip = 10.20.153.10,10.20.153.11 =>

提供者与消费者部署在同集群内,本机只访问本机的服务:

=> host = $host

隔离不同机房网段

host != 172.22.3.* => host != 172.22.3.*

前后台分离

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

读写分离

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

为重要应用提供额外的机器

application != kylin => host != 172.22.3.95,172.22.3.96

服务寄宿在应用上,只暴露一部分的机器,防止整个集群挂掉

=> host = 172.22.3.1*,172.22.3.2*

源码分析


ConditionRouter#route

以下是条件路由的源码分析过程

@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 {
      	// 先对服务消费者条件进行匹配,如果匹配失败,表明服务消费者 url 不符合匹配规则,
        // 无需进行后续匹配,直接返回 Invoker 列表即可
        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. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
            return result;
        }
      	// 对服务提供者匹配规则
        for (Invoker<T> invoker : invokers) {
          	// 若匹配成功,表明当前 Invoker 符合服务提供者匹配规则。
            // 此时将 Invoker 添加到 result 列表中
            if (matchThen(invoker.getUrl(), url)) {
                result.add(invoker);
            }
        }
      	// 如果 result 列表不为空,直接返回
        if (!result.isEmpty()) {
            return result;
        // 如果 result 为空列表,且 force = true,表示强制返回空列表,
        // 否则路由结果为空的路由规则将自动失效
        } else if (force) {
            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);
    }
  	// 原样返回,此时 force = false,表示该条路由规则失效
    return invokers;
}

matchWhen/matchThen

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

private boolean matchThen(URL url, URL param) {
  	// 服务提供者条件为 null 或空,表示禁用服务
    return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
}

ConditionRouter#matchCondition

第二个参数代表服务提供者URL,第三个参数代表服务消费者URL

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()) {
        String key = matchPair.getKey();
        String sampleValue;
      	// 1、如果invocation不为空,并且key等于method或者methods,则从invocation获取被调用的方法名
        if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
            sampleValue = invocation.getMethodName();
        // 2、如果key等于address,则从URL中获取address
        } else if (ADDRESS_KEY.equals(key)) {
            sampleValue = url.getAddress();
        // 3、如果key等于host,则从URL中获取host
        } else if (HOST_KEY.equals(key)) {
            sampleValue = url.getHost();
        // 4、从服务提供者/服务消费者URL中获取指定字段值
        } else {
            sampleValue = sample.get(key);
            if (sampleValue == null) {
                sampleValue = sample.get(key);
            }
        }
        if (sampleValue != null) {
          	// 调用MatchPair的isMatch方法进行匹配
            if (!matchPair.getValue().isMatch(sampleValue, param)) {
     						// 只要有一个规则匹配失败,直接返回false
                return false;
            } else {
                result = true;
            }
        } else {
          	// sampleValue 为空,表明服务提供者或消费者 url 中不包含相关字段。此时如果 
            // MatchPair 的 matches 不为空,表示匹配失败,返回 false
            if (!matchPair.getValue().matches.isEmpty()) {
                return false;
            } else {
                result = true;
            }
        }
    }
    return result;
}

MatchPair#isMatch

private boolean isMatch(String value, URL param) {
  	// 情况一、matches不为空、mismatches为空
    if (!matches.isEmpty() && mismatches.isEmpty()) {
      	// 遍历matches,只有有一个元素匹配入参,则返回true
        for (String match : matches) {
            if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                return true;
            }
        }
      	// 如果matches的所有元素都不匹配,则返回false
        return false;
    }
	// 情况二、matches为空、mismatches不为空
    if (!mismatches.isEmpty() && matches.isEmpty()) {
      	// 遍历mismatches,只要有一个元素匹配入参,则返回false
        for (String mismatch : mismatches) {
            if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                return false;
            }
        }
      	// 如果mismatches的所有元素都不匹配,则返回true
        return true;
    }
	// 情况三、matches不为空、mismatches不为空
    if (!matches.isEmpty() && !mismatches.isEmpty()) {
		// 遍历mismatches,只要有一个元素匹配入参,则返回false
        for (String mismatch : mismatches) {
            if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
                return false;
            }
        }
      	// 遍历matches,只要有一个元素匹配入参,则返回true
        for (String match : matches) {
            if (UrlUtils.isMatchGlobPattern(match, value, param)) {
                return true;
            }
        }
      	// 如果mismatches元素都不匹配,并且matches元素也都不匹配,则返回false
        return false;
    }
  	// 剩余情况直接返回false
    return false;
}

UrlUtils#isMatchGlobPattern

public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
    if (param != null && pattern.startsWith("$")) {
        // 引用服务消费者参数
        pattern = param.getRawParameter(pattern.substring(1));
    }
  	// 调用重载方法继续比较
    return isMatchGlobPattern(pattern, value);
}

public static boolean isMatchGlobPattern(String pattern, String value) {
    // 如果pattern是通配符“*”,返回true
  	if ("*".equals(pattern)) {
        return true;
    }
  	// 如果pattern、value都为空,返回true
    if (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
        return true;
    }
  	// 如果pattern、value有一个不为空,返回false
    if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
        return false;
    }

    int i = pattern.lastIndexOf('*');
    // 如果pattern没有使用*
    if (i == -1) {
      	// 比较pattern、value的内容是否相同并返回
        return value.equals(pattern);
    }
    // 如果pattern的末尾是*
    else if (i == pattern.length() - 1) {
      	// 判断value是否以pattern开头的
        return value.startsWith(pattern.substring(0, i));
    }
    // 如果pattern的开头是*
    else if (i == 0) {
   		// 判断value是否以pattern结尾的
        return value.endsWith(pattern.substring(i + 1));
    }
    // 如果pattern的中间是*
    else {
        String prefix = pattern.substring(0, i);
        String suffix = pattern.substring(i + 1);
      	// 判断value是否以pattern的*之前的内容开头
      	// 并且value是否以pattern的*之后的内容结尾
        return value.startsWith(prefix) && value.endsWith(suffix);
    }
}
 类似资料: