农场主的黑科技.

Dubbo源码跟踪实录-集群容错:服务路由Router

字数统计: 2.2k阅读时长: 13 min
2019/04/21 Share

前言

首先,服务路由是干什么的?

刷新invoker的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。

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

Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter、脚本路由 ScriptRouter 和标签路由 TagRouter。文中主要分析最常用的条件路由 ConditionRouter.

推荐阅读:

https://dubbo.incubator.apache.org/zh-cn/

https://segmentfault.com/blog/dubboanalysis

路由规则

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

1
host = 10.20.153.10 => host = 10.20.153.11

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

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

如果服务消费者匹配条件为空,表示不对服务消费者进行限制。如果服务提供者匹配条件为空,表示对某些服务消费者禁用服务。

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

表达式解析

条件表达式的解析过程始于 ConditionRouter 的构造方法.该构造方法会在RegistryDirectory#notify中被调用,所以参数是从zk的routers节点下的url,下面开始分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public ConditionRouter(URL url) {//zk routers节点下的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(); //host = 10.20.153.10 (服务消费者匹配条件)
String thenRule = i < 0 ? rule.trim() : rule.substring(i + 2).trim();//host = 10.20.153.11 (服务提供者匹配条件)
// 解析服务消费者匹配规则
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;推荐阅读:

https://dubbo.incubator.apache.org/zh-cn/

https://segmentfault.com/blog/dubboanalysis
} catch (ParseException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}

假如我们这里的路由规则为

1
host = 10.20.153.10 => host = 10.20.153.11

首先会被分割为:

1
2
String whenRule = rule.substring(0, i).trim(); //host = 10.20.153.10 (服务消费者匹配条件)
String thenRule = rule.substring(i + 2).trim();//host = 10.20.153.11 (服务提供者匹配条件

通过parseRule()解析.结果的Map是Map<String, MatchPair>类型.而MatchPair是一个内部类,它的定义如下

1
2
3
4
private static final class MatchPair {
final Set<String> matches = new HashSet<String>();
final Set<String> mismatches = new HashSet<String>();
}

parseRule()的字符串解析代码这里就不分析了.最后我们得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
----- whenCondition ------
{
"host": {
"matches": ["10.20.153.10"],
"mismatches": []
}
}
----- thenCondition ------
{
"host": {
"matches": ["10.20.153.11"],
"mismatches": []
}
}

最后把whenCondition和thenCondition进行保存.

再看一个parseRule()的例子.如果参数String为

1
2
> host = 2.2.2.2 & host != 1.1.1.1 & method = hello
>

那么parseRule()返回的结果是

1
2
3
4
5
6
7
8
9
10
11
> {
> "host": {
> "matches": ["2.2.2.2"],
> "mismatches": ["1.1.1.1"]
> },
> "method": {
> "matches": ["hello"],
> "mismatches": []
> }
> }
>

ConditionRouterTest类里面有许多匹配的测试示例,可以参考.

服务路由

服务路由就是调用ConditionRouter的route()方法.这个方法在什么时候被调用呢.典型的例子就是在RegistryDirectory#toMethodInvokers,所有Invoker创建完成的时候进行调用.也就是说服务路由的过程就是通过之前加载的路由条件筛选出合适的invoker的过程.

匹配的细节就不看了.这里简单看route()的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
//url参数 = 消费者url
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. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
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. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey() + ", router: " + url.getParameterAndDecoded(Constants.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;
}

最后

在上一章为止一直在用官方的example demo来进行演示.而demo运行时AbstractDirectory的router变量是null.也就是router没有被生成,没有进行服务路由的环节.因此本章就没有进行太多的代码跟踪.

简单说一下router的创建时机,其实这部分应该放在文章的开头去讲的,不知为何我尝试创建路由规则的时候失败了(待会会讲),所以下面的内容只是根据源码的猜测,可能不准确:

AbstractDirectory的router变量的构造时机,也就是ConditionRouter的构造时机,一共有两次.一次是在AbstractDirectory的构造函数的参数中.还有一次是在RegistryDirectory的notify()里面.

而RegistryDirectory的notify()方法会从zk中获取router的URL.demo中获取到的URL是无效的(不存在),所以就没有构造成功.如果这里获取到有效的url,ConditionRouter就会被构造,并启动服务路由.

那么该如何创建路由呢?

官方文档说需要通过Dubbo-Admin写入路由规则.不知为何我在创建路由规则的页面保存不了..如果这里创建成功了,RegistryDirectory的notify()方法应该就会获取到有效的router的URL,并创建ConditionRouter,启动服务路由.

路由规则和匹配的逻辑文中都没有详细介绍,感觉以后要用的时候再查就行了.其实关于服务路由知道这一句话就够了:

服务路由的过程就是通过之前加载的路由条件筛选出合适的invoker的过程

CATALOG
  1. 1. 前言
  2. 2. 路由规则
  3. 3. 表达式解析
  4. 4. 服务路由
  5. 5. 最后