服务目录在刷新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的取值。必填。
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、表达式
参数支持
条件支持
=
表示匹配,比如 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*
以下是条件路由的源码分析过程
@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);
}
}