文章

4. Dubbo 服务路由源码分析

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 中,多个路由器组成一条路由链共同协作,前一个路由器的输出作为另一个路由器的输入,经过层层筛选后,最终生成有效的地址集合。

Router Chain

本文将重点分析标签路由和条件路由的源码实现,它们是 Dubbo 中最常用的两种路由机制。

2. 标签路由 (TagRouter)

标签路由通过给服务提供者实例打标签,并在消费者请求中指定标签来实现精准的流量分配。

tag-condition-compare

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 条件匹配实现

条件路由的匹配逻辑由 matchWhenmatchThen 方法实现:

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);
}

核心匹配逻辑在 doMatchisMatch 方法中:

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:若任意模式匹配,返回false
2. 再遍历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 服务路由机制提供了灵活强大的流量控制能力,通过源码分析,我们可以看出:

  1. 标签路由适合于明确的流量隔离场景,如环境隔离、灰度发布等
  2. 条件路由适合于更复杂的流量控制场景,支持多种参数匹配和灵活的规则组合

在实际应用中,建议:

  • 优先使用标签路由实现简单的流量隔离需求
  • 对于复杂的流量控制逻辑,使用条件路由
  • 合理设置 force 参数,避免因路由规则导致服务不可用
  • 结合监控系统观察路由规则生效情况

路由规则应该简单明确,避免过于复杂的规则组合,以确保系统的可维护性和稳定性。

本文由作者按照 CC BY 4.0 进行授权