一、前沿
在上一节中我们分析了集群容错的第一部分 — 服务目录 Directory,服务目录在列举 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的 provider,接下来我们分析集群容错的第二部分 — 服务路由 Router
服务路由 Router 定义:Router 包含一条路由规则,路由规则决定了 consumer 的调用目标,即规定了 consumer 可调用哪些 provider
dubbo 中目前提供了三种服务路由实现,分别是:
ConditionRouter : 条件路由
ScriptRouter : 脚本路由
TagRouter : 标签路由
其中 ConditionRouter 是我们最常使用的,因此本文只分析 ConditionRouter 相关源码,至于 ScriptRouter 和 TagRouter 源码就不在这里分析了,感兴趣的可以自己分析一下源码
二、服务路由结构
Router 总结构如下图:
上图中可以看出 ConditionRouter 、 ScriptRouter、 TagRouter 全部继承了 AbstractRouter 抽象类,而 AbstractRouter 类实现了 Router 接口,该接口中定义了一个非常重要的方法 route,即路由 invoker 列表,过滤掉不符合规则的 invoker
三、Router 源码
条件路由规则由两个条件组成,分别用于对 consumer 和 provider 进行匹配。比如有这样一条规则:
host = 10.20.153.10 => host = 10.20.153.11
该条规则表示 IP 为 10.20.153.10 的 consumer 只可调用 IP 为 10.20.153.11 的 provider 的服务,不可调用其他机器上的服务。条件路由规则的格式如下:
[consumer匹配条件] => [provider匹配条件]
如果 consumer 匹配条件为空,表示不对 consumer 进行限制。如果 provider 匹配条件为空,表示对某些 consumer 禁用服务
ConditionRouter 类在初始化时会先对用户配置的路由规则进行解析,得到一系列的条件。然后再根据这些条件对服务进行路由。本文将分两部分进行讲解,即 3.1 表达式解析 和 3.2 服务路由 。下面,我们先从表达式解析过程分析
3.1 表达式解析
条件路由规则是一个字符串,对于 Dubbo 来说,它并不能直接理解字符串的意思,需要将其解析成内部格式才行,条件表达式的解析过程始于 ConditionRouter 的构造方法,源码如下:
public ConditionRouter(URL url) {
this.url = url;
this.priority = url.getParameter(PRIORITY_KEY, 0);
this.force = url.getParameter(FORCE_KEY, false);
this.enabled = url.getParameter(ENABLED_KEY, true);
// 获取 url 中 rule 配置,并解析成匹配规则
init(url.getParameterAndDecoded(RULE_KEY));
}
public void init(String rule) {
try {
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
// rule 中的 consumer. 和 provider. 全部替换为 ""
rule = rule.replace("consumer.", "").replace("provider.", "");
// 定位 => 分隔符位置
int i = rule.indexOf("=>");
// => 左边的为 consumer 匹配条件
String whenRule = i < 0 ? null : rule.substring(0, i).trim();
// => 右边的为 provider 匹配条件
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();
// 解析 consumer 匹配规则
Map<String, MatchPair> when = StringUtils.isBlank(whenRule) || "true".equals(whenRule) ? new HashMap<String, MatchPair>() : parseRule(whenRule);
// 解析 provider 匹配规则
Map<String, MatchPair> then = StringUtils.isBlank(thenRule) || "false".equals(thenRule) ? null : parseRule(thenRule);
// NOTE: It should be determined on the business level whether the `When condition` can be empty or not.
// 注意:应该在业务级别上确定 'when' 条件是否可以为空
this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
private static Map<String, MatchPair> parseRule(String rule)
throws ParseException {
// 定义 <匹配项,条件> 映射集合
Map<String, MatchPair> condition = new HashMap<String, MatchPair>();
if (StringUtils.isBlank(rule)) {
return condition;
}
// Key-Value pair, stores both match and mismatch conditions
// MatchPair 对象中同时包含了匹配和不匹配条件集合
MatchPair pair = null;
// Multiple values
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()) {
// Try to match one by one
// 获取匹配到的第一个括号内的值
String separator = matcher.group(1);
// 获取匹配到的第二个括号内的值
String content = matcher.group(2);
// Start part of the condition expression.
// 分隔符为空,表示匹配的是表达式的开始部分
if (StringUtils.isEmpty(separator)) {
// 创建 MatchPair 对象
pair = new MatchPair();
// 存储 <匹配项, MatchPair> 键值对,比如 <host, MatchPair>
condition.put(content, pair);
}
// The KV part of the condition expression
// 如果分隔符为 &,表明接下来也是一个条件
else if ("&".equals(separator)) {
if (condition.get(content) == null) {
pair = new MatchPair();
condition.put(content, pair);
} else {
pair = condition.get(content);
}
}
// The Value in the KV part.
// 分隔符为 =
else if ("=".equals(separator)) {
// = 符号右边没有内容匹配,抛出异常
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
// 匹配的条件
values = pair.matches;
// 将 content 存入到 MatchPair 的 matches 集合中
values.add(content);
}
// The Value in the KV part.
else if ("!=".equals(separator)) {
if (pair == null) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
// 不匹配的条件
values = pair.mismatches;
// 将 content 存入到 MatchPair 的 mismatches 集合中
values.add(content);
}
// The Value in the KV part, if Value have more than one items.
// 分隔符为 ,
else if (",".equals(separator)) {
// Should be separated by ','
if (values == null || values.isEmpty()) {
throw new ParseException("Illegal route rule \""
+ rule + "\", The error char '" + separator
+ "' at index " + matcher.start() + " before \""
+ content + "\".", matcher.start());
}
// 将 content 内容存入到上一次匹配到的内容获取到的 MatchPair 的 matches 或者 mismatches 集合
values.add(content);
} else {
throw new ParseException("Illegal route rule \"" + rule
+ "\", The error char '" + separator + "' at index "
+ matcher.start() + " before \"" + content + "\".", matcher.start());
}
}
return condition;
}
自己可以动手调试解析方法看看,下面我用例子给大家演示一下整个解析过程,解析字符串为 host = 2.2.2.2,3.3.3.3 & host != 1.1.1.1 & method = hello,解析结果如下:
括号一 括号二
1. null host
2. = 2.2.2.2
3. , 3.3.3.3
4. & host
5. != 1.1.1.1
6. & method
7. = 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 = "3.3.3.3",pair = MatchPair@123。此时将 3.3.3.3 放入到 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 = "hello",pair = MatchPair@456。此时将 hello 放入到 MatchPair@456 对象的 matches 集合中
循环结束,此时 condition 的内容如下:
{
"host": {
"matches": ["2.2.2.2","3.3.3.3"],
"mismatches": ["1.1.1.1"]
},
"method": {
"matches": ["hello"],
"mismatches": []
}
}
相信大家通过上面的例子对解析过程已经有了清晰的认识,接着我们分析 dubbo 服务路由
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 {
// 先对 consumer 条件进行匹配,如果不匹配的话,表明 consumer url 不符合匹配规则,无需进行后续匹配,直接返回 Invoker 列表即可
// 比如下面的规则: host = 10.20.153.10 => host = 10.0.0.10
// 这条路由规则希望 IP 为 10.20.153.10 的 consumer 调用 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) {
// provider 匹配条件未配置,表明对指定的 consumer 禁用服务,也就是 consumer 在黑名单中
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
// 对 provider 进行一一匹配
for (Invoker<T> invoker : invokers) {
// 若匹配成功,表明当前 Invoker 符合 provider 匹配规则,此时将 Invoker 添加到 result 列表中
if (matchThen(invoker.getUrl(), url)) {
result.add(invoker);
}
}
if (!result.isEmpty()) {
// 返回匹配后的路由 invoker 后结果
return result;
} else if (force) {
// 若 result 为空列表 && force = true,表示强制返回空列表
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);
}
// 没有做任何路由处理,直接返回 invokers,此时 force = false
return invokers;
}
boolean matchWhen(URL url, Invocation invocation) {
// consumer 条件为 null 或空,均返回 true,比如: => host != 172.22.3.91
// 表示所有的 consumer 都不得调用 IP 为 172.22.3.91 的机器上的服务
// 匹配条件,参数 whenCondition 是 consumer 匹配条件,参数 url 是 consumer url,参数 invocation 是 consumer 的 invocation
return CollectionUtils.isEmptyMap(whenCondition) || matchCondition(whenCondition, url, null, invocation);
}
private boolean matchThen(URL url, URL param) {
// provider 条件为 null 或空,表示禁用服务
// 匹配条件,参数 thenCondition 是 provider 匹配条件,参数 url 是 provider url, param 是 consumer url
return CollectionUtils.isNotEmptyMap(thenCondition) && matchCondition(thenCondition, url, param, null);
}
private boolean matchCondition(Map<String, MatchPair> condition, URL url, URL param, Invocation invocation) {
// 将 consumer 或者 provider url 转成 map
Map<String, String> sample = url.toMap();
boolean result = false;
for (Map.Entry<String, MatchPair> matchPair : condition.entrySet()) {
String key = matchPair.getKey();
String sampleValue;
//get real invoked method name from invocation
// 如果 invocation 不为空 && key 为 mehtod 或者 methods,表示进行方法匹配
if (invocation != null && (METHOD_KEY.equals(key) || METHODS_KEY.equals(key))) {
// 从 invocation 中 获取实际调用的方法名
sampleValue = invocation.getMethodName();
} else if (ADDRESS_KEY.equals(key)) {
// 从 consumer 或者 provider 的 url 中获取 address 值
sampleValue = url.getAddress();
} else if (HOST_KEY.equals(key)) {
// 从 consumer 或者 provider 的 url 中获取主机 host 值
sampleValue = url.getHost();
} else {
// 从 consumer 或者 provider 的 url 中获取指定字段值,比如 protocol、path 等
sampleValue = sample.get(key);
if (sampleValue == null) {
// 尝试通过 default.key 获取相应的值
sampleValue = sample.get(DEFAULT_KEY_PREFIX + key);
}
}
if (sampleValue != null) {
// 调用 MatchPair 的 isMatch 方法进行匹配
if (!matchPair.getValue().isMatch(sampleValue, param)) {
// 只要有一个规则匹配失败,立即返回 false 结束方法
return false;
} else {
result = true;
}
} else {
//not pass the condition
// sampleValue 为空,表明 consumer 或者 provider 的 url 中不包含相关字段。此时如果 MatchPair 的 matches 不为空,表示匹配失败,返回 false
// 比如我们有这样一条匹配条件 loadbalance = random,假设 url 中并不包含 loadbalance 参数,此时 sampleValue = null。
// 既然路由规则里限制了 loadbalance 必须为 random,但 sampleValue = null,明显不符合规则,因此返回 false
if (!matchPair.getValue().matches.isEmpty()) {
return false;
} else {
// MatchPair 的 matches 为空,表示匹配成功
result = true;
}
}
}
return result;
}
protected static final class MatchPair {
final Set<String> matches = new HashSet<String>();
final Set<String> mismatches = new HashSet<String>();
private boolean isMatch(String value, URL param) {
// 情况1:matches不为空 && mismatches为空
if (!matches.isEmpty() && mismatches.isEmpty()) {
// 遍历 matches 集合,检测入参是否能被 matches 集合元素匹配到
// 举个例子,如果 value = 10.20.153.11,matches = [10.20.153.*],
// 此时 isMatchGlobPattern 方法返回 true,结果返回 true
for (String match : matches) {
// 只要有一个条件匹配成功即返回匹配成功结果
if (UrlUtils.isMatchGlobPattern(match, value, param)) {
return true;
}
}
// 所有匹配项都不匹配,返回false
return false;
}
// 情况2:matches为空 && mismatches为不空
if (!mismatches.isEmpty() && matches.isEmpty()) {
// 遍历 matches 集合,检测入参是否能被 mismatches 集合元素匹配到
// 举个例子,如果 value = 10.20.153.11,mismatches = [10.20.153.*],
// 此时 isMatchGlobPattern 方法返回 true,结果返回 false
for (String mismatch : mismatches) {
// 只要入参被 mismatches 集合中的任意一个元素匹配到,就返回 false
if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
return false;
}
}
// mismatches 集合中所有元素都无法匹配到入参,此时返回 true
return true;
}
// 情况3:matches不为空 && mismatches为不空
if (!matches.isEmpty() && !mismatches.isEmpty()) {
//when both mismatches and matches contain the same value, then using mismatches first
// 当 mismatches 和 matches 都不为空的时候,此时优先使用 mismatches 集合元素对入参进行匹配
// 只要 mismatches 集合中任意一个元素与入参匹配成功,就立即返回 false,结束方法
for (String mismatch : mismatches) {
if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) {
return false;
}
}
// mismatches 集合元素无法匹配到入参,此时再使用 matches 继续匹配
for (String match : matches) {
// 只要 matches 集合中任意一个元素与入参 value 匹配成功,就立即返回 true,结束方法
if (UrlUtils.isMatchGlobPattern(match, value, param)) {
return true;
}
}
// 全部匹配失败,返回false
return false;
}
// 情况4:matches 和 mismatches 都为空,直接返回 false
return false;
}
}
整个过程源码中的注释已经写的很清楚明白,这里就不在赘述了,这里对 MatchPair 的 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 参数为 consumer 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 (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
// 匹配规则为空 && value 为空,此时认为两者匹配成功
return true;
}
if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
// 匹配规则为空 或者 value 为空,此时认为两者匹配不成功
return false;
}
// 定位 * 通配符位置
int i = pattern.lastIndexOf('*');
// doesn't find "*"
if (i == -1) {
// 不存在通配符 *,则直接比较两者是否相等
return value.equals(pattern);
}
// "*" is at the end
// 通配符 * 在匹配规则末尾,例如 10.0.21.*
else if (i == pattern.length() - 1) {
// 检测 value 是否以“不含通配符的匹配规则”开头,并返回结果。比如: pattern = 10.0.21.*,value = 10.0.21.12
// pattern.substring(0, i) = 10.0.21. ,此时返回 true
return value.startsWith(pattern.substring(0, i));
}
// "*" is at the beginning
// 通配符 * 在匹配规则开头,例如 *.10.0.21
else if (i == 0) {
// 检测 value 是否以“不含通配符的匹配规则”结尾,并返回结果。比如: pattern = *.10.0.21,value = 12.10.0.21
// pattern.substring(i + 1) = .10.0.21 ,此时返回 true
return value.endsWith(pattern.substring(i + 1));
}
// "*" is in the middle
// 通配符 * 在匹配规则中间,例如 10.0.*.12
else {
// prefix = 10.0.
String prefix = pattern.substring(0, i);
// suffix = .12
String suffix = pattern.substring(i + 1);
// 检测 value 是否以 prefix 为开头 && 是否以 suffix 结尾,并返回结果。比如: pattern = 10.0.*.12,value = 10.0.11.12
// 此时 prefix = 10.0. ,suffix = .12,value 以 prefix 开头 && value 以 suffix 结尾,此时返回 true
return value.startsWith(prefix) && value.endsWith(suffix);
}
}
规则匹配的整个过程中,代码注释以具体例子分析讲解,详细大家一下子就看懂了,这里也不占用大家的时间啰嗦了
到这里服务路由的整个流程就分析完了
四、总结
本文对条件路由的表达式解析和服务路由过程进行了较为详细的分析。在看源码的过程中大家一定要使用单元测试多进行调试,这样子不仅可以加深印象还可以帮助你很好的理解代码的意图
参考:
https://dubbo.apache.org/zh-cn/docs/source_code_guide/router.html