4. Dubbo 服务路由源码分析
Dubbo 服务路由机制详解
参考链接:dubbo2.6源码分析, dubbo官方文档-路由机制
1. 服务路由概述
在上一篇文章中,我们分析了集群容错的第一部分 — 服务目录 Directory。服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由,筛选出符合路由规则的服务提供者。本文将深入分析 Dubbo 服务路由的实现原理和源码。
1.1 什么是服务路由
服务路由是 Dubbo 中的流量控制机制,它通过一系列路由规则决定服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者。路由规则可以基于多种条件(如地域、环境、版本等)对服务提供者进行筛选,从而实现精准的流量控制。
1.2 Dubbo3 路由实现
Dubbo 3 提供了多种路由实现,主要包括:
- 标签路由 (TagRouter): 基于标签将服务提供者分组,实现流量隔离
- 条件路由 (ConditionRouter): 基于参数条件匹配规则筛选服务提供者
- 脚本路由 (ScriptRouter): 通过脚本语言定义复杂路由逻辑
- Istio路由 (IstioRouter): 与 Istio 服务网格集成的路由实现
- 自定义路由 (CustomizedRouter): 用户可扩展的自定义路由实现
在 Dubbo 中,多个路由器组成一条路由链共同协作,前一个路由器的输出作为另一个路由器的输入,经过层层筛选后,最终生成有效的地址集合。
本文将重点分析标签路由和条件路由的源码实现,它们是 Dubbo 中最常用的两种路由机制。
2. 标签路由 (TagRouter)
标签路由通过给服务提供者实例打标签,并在消费者请求中指定标签来实现精准的流量分配。
2.1 标签路由工作模式
标签路由支持两种打标方式:静态标签和动态标签。
2.1.1 静态标签
静态标签需要在服务提供者实例启动前确定,并通过特定参数 tag 指定。
在 Dubbo 实例启动前,指定当前实例的标签,如部署在杭州区域的实例,指定 tag=gray。
Provider 配置示例:
1
2
3
4
5
6
7
8
<!-- 方式1: 在提供者级别设置标签 -->
<dubbo:provider tag="gray"/>
<!-- 方式2: 在服务级别设置标签 -->
<dubbo:service tag="gray"/>
<!-- 方式3: 通过JVM参数设置标签 -->
<!-- java -jar xxx-provider.jar -Ddubbo.provider.tag=gray -->
Consumer 使用示例:
1
2
// 在发起调用前设置标签,确保请求被路由到带有相同标签的提供者
RpcContext.getContext().setAttachment(Constants.TAG_KEY, "gray");
2.1.2 动态标签
动态标签可以匹配多个属性,根据指定条件将提供者实例动态划分到不同流量分组。
Provider 配置示例:
1
2
3
4
5
6
7
8
9
10
configVersion: v3.0
force: true
enabled: true
key: shop-detail
tags:
- name: gray
match:
- key: env
value:
exact: gray
Consumer 使用示例:
1
2
// 与静态标签使用方式相同
RpcContext.getContext().setAttachment(Constants.TAG_KEY, "gray");
2.2 标签路由源码分析
2.2.1 初始化过程
1
2
3
4
public TagStateRouter(URL url) {
// 调用父类构造方法,完成基础路由器初始化
super(url);
}
2.2.2 路由实现
标签路由的核心逻辑在 doRoute 方法中:
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Override
public BitList<Invoker<T>> doRoute(
BitList<Invoker<T>> invokers,
URL url,
Invocation invocation,
boolean needToPrintMessage,
Holder<RouterSnapshotNode<T>> nodeHolder,
Holder<String> messageHolder)
throws RpcException {
// 1. 处理边界情况: Invoker列表为空或路由规则无效
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
final TagRouterRule tagRouterRuleCopy = tagRouterRule;
if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
return filterUsingStaticTag(invokers, url, invocation);
}
// 2. 获取请求标签
String tag = StringUtils.isEmpty(invocation.getAttachment(TAG_KEY))
? url.getParameter(TAG_KEY)
: invocation.getAttachment(TAG_KEY);
// 3. 处理带标签的请求
if (StringUtils.isNotEmpty(tag)) {
// 3.1 尝试动态标签匹配
Map<String, Set<String>> tagnameToAddresses = tagRouterRuleCopy.getTagnameToAddresses();
Set<String> addresses = selectAddressByTagLevel(tagnameToAddresses, tag, isForceUseTag(invocation));
if (addresses != null) {
// 按地址过滤提供者
BitList<Invoker<T>> result = filterInvoker(invokers,
invoker -> addressMatches(invoker.getUrl(), addresses));
// 有匹配结果或强制使用标签时,直接返回
if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
return result;
}
}
// 3.2 尝试静态标签匹配
BitList<Invoker<T>> result = filterInvoker(
invokers, invoker -> tag.equals(invoker.getUrl().getParameter(TAG_KEY)));
// 静态标签有匹配或强制使用标签时,返回结果
if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
return result;
}
// 3.3 标签匹配失败,执行故障转移:返回所有无标签提供者
BitList<Invoker<T>> tmp = filterInvoker(
invokers, invoker -> addressNotMatches(invoker.getUrl(), tagRouterRuleCopy.getAddresses()));
return filterInvoker(
tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
}
// 4. 处理无标签请求
else {
// 4.1 排除所有带标签的提供者
Set<String> addresses = tagRouterRuleCopy.getAddresses();
if (CollectionUtils.isNotEmpty(addresses)) {
BitList<Invoker<T>> result = filterInvoker(
invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
if (CollectionUtils.isEmpty(result)) {
return result;
}
}
// 4.2 返回无标签的提供者
return filterInvoker(invokers, invoker ->
StringUtils.isEmpty(invoker.getUrl().getParameter(TAG_KEY)));
}
}
该方法根据请求标签和路由规则,动态筛选匹配的服务提供者(Invoker),实现带标签请求的精准路由和无标签请求的安全隔离。标签路由的核心过滤逻辑由 filterInvoker 方法实现:
1
2
3
4
5
6
7
8
9
10
11
private <T> BitList<Invoker<T>> filterInvoker(BitList<Invoker<T>> invokers, Predicate<Invoker<T>> predicate) {
// 优化: 若所有元素都满足条件,直接返回原列表
if (invokers.stream().allMatch(predicate)) {
return invokers;
}
// 创建新列表并过滤
BitList<Invoker<T>> newInvokers = invokers.clone();
newInvokers.removeIf(invoker -> !predicate.test(invoker));
return newInvokers;
}
3. 条件路由 (ConditionRouter)
条件路由基于一系列参数条件对服务提供者进行筛选,支持更灵活的匹配规则。
3.1 条件路由与标签路由的区别
- 标签路由:一旦给实例打上标签,该实例就会从通用流量集合中移除,不同标签之间没有交集。
- 条件路由:所有实例被视为同等,每次路由都基于全量地址执行筛选,不存在分组隔离。
3.2 条件路由规则语法
条件路由规则主体 conditions 包含两部分:
- 匹配条件 (=> 之前):指定哪些消费者请求需要应用此规则
- 过滤条件 (=> 之后):指定符合条件的提供者地址子集
规则示例:
1
2
3
4
5
6
configVersion: v3.0
enabled: true
force: false
key: org.apache.dubbo.samples.CommentService
conditions:
- '=> region = $region' # 将请求路由到与消费者相同region的提供者
3.3 条件路由源码分析
3.3.1 初始化与规则解析
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
47
48
49
50
51
public ConditionStateRouter(URL url) {
// 调用父类 AbstractStateRouter(URL url) 构造方法,完成基础路由器的初始化,包括模块模型、规则仓库、路由器 URL 及是否快速失败等属性的设置
super(url);
this.setUrl(url);
// 设置 force 属性,决定路由结果为空时是否强制执行
this.setForce(url.getParameter(FORCE_KEY, false));
// 加载所有激活的 ConditionMatcherFactory 扩展,用于后续条件匹配器的创建
matcherFactories =
moduleModel.getExtensionLoader(ConditionMatcherFactory.class).getActivateExtensions();
// 设置 enabled 属性,决定路由器是否启用
this.enabled = url.getParameter(ENABLED_KEY, true);
if (enabled) {
// 从 URL 获取并解码路由规则(rule),并调用 init 方法进行规则解析和条件初始化
init(url.getParameterAndDecoded(RULE_KEY));
}
}
public void init(String rule) {
try {
// 1. 参数校验
if (rule == null || rule.trim().length() == 0) {
throw new IllegalArgumentException("Illegal route rule!");
}
// 2. 预处理规则字符串
rule = rule.replace("consumer.", "").replace("provider.", "");
// 3. 分割规则为when条件和then条件
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();
// 4. 解析when条件(消费者匹配条件)
Map<String, ConditionMatcher> when =
StringUtils.isBlank(whenRule) || "true".equals(whenRule)
? new HashMap<>() // 无条件匹配所有消费者
: parseRule(whenRule);
// 5. 解析then条件(提供者过滤条件)
Map<String, ConditionMatcher> then =
StringUtils.isBlank(thenRule) || "false".equals(thenRule)
? null // 禁止所有提供者
: parseRule(thenRule);
// 6. 保存解析结果
this.whenCondition = when;
this.thenCondition = then;
} catch (ParseException e) {
throw new IllegalStateException("Failed to parse route rule: " + rule, e);
}
}
3.3.2 规则解析详解
条件路由规则解析的核心在 parseRule 方法:
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private Map<String, ConditionMatcher> parseRule(String rule) throws ParseException {
Map<String, ConditionMatcher> condition = new HashMap<>();
if (StringUtils.isBlank(rule)) {
return condition;
}
// 当前处理的条件匹配器
ConditionMatcher matcherPair = null;
// 当前处理的多值集合
Set<String> values = null;
// 使用正则表达式解析规则字符串
// ROUTE_PATTERN = ([&!=,]*)\s*([^&!=,\s]+)
final Matcher matcher = ROUTE_PATTERN.matcher(rule);
while (matcher.find()) {
String separator = matcher.group(1); // 分隔符(&, =, !=, ,)
String content = matcher.group(2); // 内容(键或值)
// 处理规则开始部分(无分隔符)
if (StringUtils.isEmpty(separator)) {
matcherPair = this.getMatcher(content);
condition.put(content, matcherPair);
}
// 处理新条件(&分隔符)
else if ("&".equals(separator)) {
if (condition.get(content) == null) {
matcherPair = this.getMatcher(content);
condition.put(content, matcherPair);
} else {
matcherPair = condition.get(content);
}
}
// 处理等于条件(=分隔符)
else if ("=".equals(separator)) {
if (matcherPair == null) {
throw new ParseException("Expected key before '=' in rule: " + rule);
}
values = matcherPair.getMatches();
values.add(content);
}
// 处理不等于条件(!=分隔符)
else if ("!=".equals(separator)) {
if (matcherPair == null) {
// 格式错误:值定义前未定义键
throw new ParseException("/*...*/");
}
values = matcherPair.getMismatches();
values.add(content);
}
// 处理多值列表(,分隔符)
else if (",".equals(separator)) {
if (values == null || values.isEmpty()) {
// 格式错误:多值分隔符前没有已定义的值
throw new ParseException("/*...*/");
}
values.add(content);
} else {
// 不支持的分隔符
throw new ParseException("/*...*/");
}
}
return condition;
}
以上就是路由规则的解析逻辑,该逻辑由正则表达式和一个 while 循环以及数个条件分支组成。下面通过一个示例对解析逻辑进行演绎。示例为 host = 2.2. 2.2 & host != 1.1.1.1 & method = hello。正则解析结果如下:
1
2
3
4
5
6
7
括号一 括号二
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 集合 中。
1
2
3
4
5
6
7
8
9
10
{
"host": {
"matches": ["2.2.2.2"],
"mismatches": ["1.1.1.1"]
},
"method": {
"matches": ["hello"],
"mismatches": []
}
}
3.3.3 路由执行流程
条件路由的核心执行逻辑在 doRoute 方法中:
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
47
48
@Override
protected BitList<Invoker<T>> doRoute(
BitList<Invoker<T>> invokers,
URL url,
Invocation invocation,
boolean needToPrintMessage,
Holder<RouterSnapshotNode<T>> nodeHolder,
Holder<String> messageHolder)
throws RpcException {
// 1. 检查路由是否启用
if (!enabled) {
return invokers;
}
// 2. 检查上游Invoker列表是否为空
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
try {
// 3. 检查消费者是否匹配when条件
if (!matchWhen(url, invocation)) {
return invokers; // 不匹配则跳过路由规则
}
// 4. 检查then条件是否存在
if (thenCondition == null) {
return BitList.emptyList(); // then条件为空表示禁用服务
}
// 5. 应用then条件筛选提供者
BitList<Invoker<T>> result = invokers.clone();
result.removeIf(invoker -> !matchThen(invoker.getUrl(), url));
// 6. 处理筛选结果
if (!result.isEmpty()) {
return result; // 有匹配结果则返回
} else if (this.isForce()) {
return result; // 强制路由则返回空列表
}
} catch (Throwable t) {
logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers, t);
}
// 7. 默认返回原始列表
return invokers;
}
3.3.4 条件匹配实现
条件路由的匹配逻辑由 matchWhen 和 matchThen 方法实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
boolean matchWhen(URL url, Invocation invocation) {
// 1. when条件为空表示匹配所有消费者
if (CollectionUtils.isEmptyMap(whenCondition)) {
return true;
}
// 2. 执行具体匹配逻辑
return doMatch(url, null, invocation, whenCondition, true);
}
private boolean matchThen(URL url, URL param) {
// 1. then条件为空表示不匹配任何提供者
if (CollectionUtils.isEmptyMap(thenCondition)) {
return false;
}
// 2. 执行具体匹配逻辑
return doMatch(url, param, null, thenCondition, false);
}
核心匹配逻辑在 doMatch 和 isMatch 方法中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private boolean doMatch(URL url, URL param, Invocation invocation,
Map<String, ConditionMatcher> conditions, boolean isWhenCondition) {
// 将URL转换为参数Map
Map<String, String> sample = url.toOriginalMap();
// 遍历所有条件项
for (Map.Entry<String, ConditionMatcher> entry : conditions.entrySet()) {
ConditionMatcher matcher = entry.getValue();
// 任一条件不匹配则返回false
if (!matcher.isMatch(sample, param, invocation, isWhenCondition)) {
return false;
}
}
// 所有条件均匹配
return true;
}
条件匹配器的 isMatch 方法实现了具体的匹配逻辑:
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
47
public boolean isMatch(Map<String, String> sample, URL param,
Invocation invocation, boolean isWhenCondition) {
// 获取待匹配的值
String value = getValue(sample, param, invocation);
if (value == null) {
return false;
}
// 情况1: 只有匹配条件
if (!matches.isEmpty() && mismatches.isEmpty()) {
for (String match : matches) {
if (doPatternMatch(match, value, param, invocation, isWhenCondition)) {
return true; // 任一匹配即可
}
}
return false;
}
// 情况2: 只有不匹配条件
else if (!mismatches.isEmpty() && matches.isEmpty()) {
for (String mismatch : mismatches) {
if (doPatternMatch(mismatch, value, param, invocation, isWhenCondition)) {
return false; // 任一不匹配则失败
}
}
return true;
}
// 情况3: 同时存在匹配和不匹配条件
else if (!matches.isEmpty() && !mismatches.isEmpty()) {
// 优先处理不匹配条件
for (String mismatch : mismatches) {
if (doPatternMatch(mismatch, value, param, invocation, isWhenCondition)) {
return false;
}
}
// 再处理匹配条件
for (String match : matches) {
if (doPatternMatch(match, value, param, invocation, isWhenCondition)) {
return true;
}
}
return false;
}
// 情况4: 无有效条件
else {
return false;
}
}
isMatch 方法逻辑比较清晰,由三个条件分支组成,用于处理四种情况。这里对四种情况下的匹配逻辑进行简单的总结,如下:
| 情况分类 | 条件特征 | 匹配逻辑 | 返回值规则 |
|---|---|---|---|
| 情况1:仅匹配条件 | matches非空且mismatches为空 | 遍历所有matches中的模式,只要有一个模式与目标值匹配,则返回true | 全部不匹配时返回false |
| 情况2:仅不匹配条件 | mismatches非空且matches为空 | 遍历所有mismatches中的模式,只要有一个模式与目标值匹配,则返回false | 全部不匹配时返回true |
| 情况3:同时存在匹配和不匹配条件 | matches非空且mismatches非空 | 1. 先遍历mismatches:若任意模式匹配,返回false2. 再遍历 matches:若任意模式匹配,返回true | 两者均不匹配时返回false |
| 情况4:无有效条件 | matches为空且mismatches为空 | 无有效条件可匹配 | 直接返回false |
3.3.5 模式匹配实现
条件路由支持多种模式匹配,以通配符匹配为例:
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
public static boolean isMatchGlobPattern(String pattern, String value, URL param) {
// 处理动态模式:从URL参数中获取实际模式
if (param != null && pattern.startsWith("$")) {
pattern = param.getRawParameter(pattern.substring(1));
}
return isMatchGlobPattern(pattern, value);
}
public static boolean isMatchGlobPattern(String pattern, String value) {
// 全匹配模式: *匹配任意值
if ("*".equals(pattern)) {
return true;
}
// 空值处理
if (StringUtils.isEmpty(pattern) && StringUtils.isEmpty(value)) {
return true;
}
if (StringUtils.isEmpty(pattern) || StringUtils.isEmpty(value)) {
return false;
}
// 查找通配符位置
int i = pattern.lastIndexOf('*');
// 无通配符: 精确匹配
if (i == -1) {
return value.equals(pattern);
}
// 通配符在末尾: 前缀匹配
else if (i == pattern.length() - 1) {
return value.startsWith(pattern.substring(0, i));
}
// 通配符在开头: 后缀匹配
else if (i == 0) {
return value.endsWith(pattern.substring(i + 1));
}
// 通配符在中间: 前后缀组合匹配
else {
String prefix = pattern.substring(0, i);
String suffix = pattern.substring(i + 1);
return value.startsWith(prefix) && value.endsWith(suffix);
}
}
4. 路由机制应用场景
Dubbo 路由机制可应用于多种流量管控场景:
4.1 环境隔离
通过标签路由可实现开发、测试、预发布和生产环境的流量隔离:
1
2
3
4
5
6
7
8
9
10
11
12
13
configVersion: v3.0
key: demo-provider
tags:
- name: dev
match:
- key: env
value:
exact: dev
- name: test
match:
- key: env
value:
exact: test
4.2 灰度发布
使用标签路由将部分流量导向新版本服务:
1
2
3
4
5
6
7
8
configVersion: v3.0
key: demo-provider
tags:
- name: gray
match:
- key: version
value:
exact: 2.0.0
4.3 同区域优先
通过条件路由实现就近访问:
1
2
3
4
configVersion: v3.0
key: demo-provider
conditions:
- '=> zone = $zone'
4.4 流量限制
结合条件路由实现黑名单/白名单:
1
2
3
4
configVersion: v3.0
key: demo-provider
conditions:
- 'host = 10.20.153.10 => host != 10.20.153.11'
5. 总结与最佳实践
Dubbo 服务路由机制提供了灵活强大的流量控制能力,通过源码分析,我们可以看出:
- 标签路由适合于明确的流量隔离场景,如环境隔离、灰度发布等
- 条件路由适合于更复杂的流量控制场景,支持多种参数匹配和灵活的规则组合
在实际应用中,建议:
- 优先使用标签路由实现简单的流量隔离需求
- 对于复杂的流量控制逻辑,使用条件路由
- 合理设置
force参数,避免因路由规则导致服务不可用 - 结合监控系统观察路由规则生效情况
路由规则应该简单明确,避免过于复杂的规则组合,以确保系统的可维护性和稳定性。


