Dubbo——路由机制(下)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Dubbo——路由机制(下)相关的知识,希望对你有一定的参考价值。

参考技术A 在 Dubbo——路由机制(上) ,介绍了 Router 接口的基本功能以及 RouterChain 加载多个 Router 的实现,之后介绍了 ConditionRouter 这个类对条件路由规则的处理逻辑以及 ScriptRouter 这个类对脚本路由规则的处理逻辑。本文继续介绍剩余的三个 Router 接口实现类。

FileRouterFactory 是 ScriptRouterFactory 的装饰器,其扩展名为 file,FileRouterFactory 在 ScriptRouterFactory 基础上增加了读取文件的能力。可以将 ScriptRouter 使用的路由规则保存到文件中,然后在 URL 中指定文件路径,FileRouterFactory 从中解析到该脚本文件的路径并进行读取,调用 ScriptRouterFactory 去创建相应的 ScriptRouter 对象。

下面来看 FileRouterFactory 对 getRouter() 方法的具体实现,其中完成了 file 协议的 URL 到 script 协议 URL 的转换,如下是一个转换示例,首先会将 file:// 协议转换成 script:// 协议,然后会添加 type 参数和 rule 参数,其中 type 参数值根据文件后缀名确定,该示例为 js,rule 参数值为文件内容。

可以再结合接下来这个示例分析 getRouter() 方法的具体实现:

TagRouterFactory 作为 RouterFactory 接口的扩展实现,其扩展名为 tag。但是需要注意的是,TagRouterFactory 与之前介绍的 ConditionRouterFactory、ScriptRouterFactory 的不同之处在于,它是通过继承 CacheableRouterFactory 这个抽象类,间接实现了 RouterFactory 接口。

CacheableRouterFactory 抽象类中维护了一个 ConcurrentMap 集合(routerMap 字段)用来缓存 Router,其中的 Key 是 ServiceKey。在 CacheableRouterFactory 的 getRouter() 方法中,会优先根据 URL 的 ServiceKey 查询 routerMap 集合,查询失败之后会调用 createRouter() 抽象方法来创建相应的 Router 对象。在 TagRouterFactory.createRouter() 方法中,创建的自然就是 TagRouter 对象了。

通过 TagRouter,可以将某一个或多个 Provider 划分到同一分组,约束流量只在指定分组中流转,这样就可以轻松达到流量隔离的目的,从而支持灰度发布等场景。

目前,Dubbo 提供了动态和静态两种方式给 Provider 打标签,其中动态方式就是通过服务治理平台动态下发标签,静态方式就是在 XML 等静态配置中打标签。Consumer 端可以在 RpcContext 的 attachment 中添加 request.tag 附加属性,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,我们只需要在起始调用时进行设置,就可以达到标签的持续传递。

了解了 Tag 的基本概念和功能之后,再简单介绍一个 Tag 的使用示例。

在实际的开发测试中,一个完整的请求会涉及非常多的 Provider,分属不同团队进行维护,这些团队每天都会处理不同的需求,并在其负责的 Provider 服务中进行修改,如果所有团队都使用一套测试环境,那么测试环境就会变得很不稳定。如下图所示,4 个 Provider 分属不同的团队管理,Provider 2 和 Provider 4 在测试环境测试,部署了有 Bug 的版本,这样就会导致整个测试环境无法正常处理请求,在这样一个不稳定的测试环境中排查 Bug 是非常困难的,因为可能排查到最后,发现是别人的 Bug。

为了解决上述问题,我们可以针对每个需求分别独立出一套测试环境,但是这个方案会占用大量机器,前期的搭建成本以及后续的维护成本也都非常高。

下面是一个通过 Tag 方式实现环境隔离的架构图,其中,需求 1 对 Provider 2 的请求会全部落到有需求 1 标签的 Provider 上,其他 Provider 使用稳定测试环境中的 Provider;需求 2 对 Provider 4 的请求会全部落到有需求 2 标签的 Provider 4 上,其他 Provider 使用稳定测试环境中的 Provider。

在一些特殊场景中,会有 Tag 降级的场景,比如找不到对应 Tag 的 Provider,会按照一定的规则进行降级。如果在 Provider 集群中不存在与请求 Tag 对应的 Provider 节点,则默认将降级请求 Tag 为空的 Provider;如果希望在找不到匹配 Tag 的 Provider 节点时抛出异常的话,我们需设置 request.tag.force = true。

如果请求中的 request.tag 未设置,只会匹配 Tag 为空的 Provider,也就是说即使集群中存在可用的服务,若 Tag 不匹配也就无法调用。一句话总结,携带 Tag 的请求可以降级访问到无 Tag 的 Provider,但不携带 Tag 的请求永远无法访问到带有 Tag 的 Provider。

下面再来看 TagRouter 的具体实现。在 TagRouter 中持有一个 TagRouterRule 对象的引用,在 TagRouterRule 中维护了一个 Tag 集合,而在每个 Tag 对象中又都维护了一个 Tag 的名称,以及 Tag 绑定的网络地址集合,如下图所示:

另外,在 TagRouterRule 中还维护了 addressToTagnames、tagnameToAddresses 两个集合(都是 Map<String, List<String>> 类型),分别记录了 Tag 名称到各个 address 的映射以及 address 到 Tag 名称的映射。在 TagRouterRule 的 init() 方法中,会根据 tags 集合初始化这两个集合。

了解了 TagRouterRule 的基本构造之后,我们继续来看 TagRouter 构造 TagRouterRule 的过程。TagRouter 除了实现了 Router 接口之外,还实现了 ConfigurationListener 接口,如下图所示:

ConfigurationListener 用于监听配置的变化,其中就包括 TagRouterRule 配置的变更。当我们通过动态更新 TagRouterRule 配置的时候,就会触发 ConfigurationListener 接口的 process() 方法,TagRouter 对 process() 方法的实现如下:

我们可以看到,如果是删除配置的操作,则直接将 tagRouterRule 设置为 null,如果是修改或新增配置,则通过 TagRuleParser 解析传入的配置,得到对应的 TagRouterRule 对象。TagRuleParser 可以解析 yaml 格式的 TagRouterRule 配置,下面是一个配置示例:

经过 TagRuleParser 解析得到的 TagRouterRule 结构,如下所示:

除了上图展示的几个集合字段,TagRouterRule 还从 AbstractRouterRule 抽象类继承了一些控制字段,后面介绍的 ConditionRouterRule 也继承了 AbstractRouterRule。

AbstractRouterRule 中核心字段的具体含义大致可总结为如下:

我们可以看到,AbstractRouterRule 中的核心字段与前面的示例配置是一一对应的。

我们知道,Router 最终目的是要过滤符合条件的 Invoker 对象,下面我们一起来看 TagRouter 是如何使用 TagRouterRule 路由逻辑进行 Invoker 过滤的,大致步骤如下:

上述流程的具体实现是在 TagRouter.route() 方法中,如下所示:

除了之前介绍的 TagRouterFactory 继承了 CacheableRouterFactory 之外,ServiceRouterFactory 也继承 CachabelRouterFactory,具有了缓存的能力,具体继承关系如下图所示:

ServiceRouterFactory 创建的 Router 实现是 ServiceRouter,与 ServiceRouter 类似的是 AppRouter,两者都继承了 ListenableRouter 抽象类(虽然 ListenableRouter 是个抽象类,但是没有抽象方法留给子类实现),继承关系如下图所示:

ListenableRouter 在 ConditionRouter 基础上添加了动态配置的能力,ListenableRouter 的 process() 方法与 TagRouter 中的 process() 方法类似,对于 ConfigChangedEvent.DELETE 事件,直接清空 ListenableRouter 中维护的 ConditionRouterRule 和 ConditionRouter 集合的引用;对于 ADDED、UPDATED 事件,则通过 ConditionRuleParser 解析事件内容,得到相应的 ConditionRouterRule 对象和 ConditionRouter 集合。这里的 ConditionRuleParser 同样是以 yaml 文件的格式解析 ConditionRouterRule 的相关配置。ConditionRouterRule 中维护了一个 conditions 集合(List<String> 类型),记录了多个 Condition 路由规则,对应生成多个 ConditionRouter 对象。

整个解析 ConditionRouterRule 的过程,与前文介绍的解析 TagRouterRule 的流程类似。

在 ListenableRouter 的 route() 方法中,会遍历全部 ConditionRouter 过滤出符合全部路由条件的 Invoker 集合,具体实现如下:

ServiceRouter 和 AppRouter 都是简单地继承了 ListenableRouter 抽象类,且没有覆盖 ListenableRouter 的任何方法,两者只有以下两点区别。

本文我们是紧接 Dubbo——路由机制(上) 的内容,继续介绍了剩余 Router 接口实现的内容。

我们介绍了基于文件的 FileRouter 实现,其底层会依赖之前介绍的 ScriptRouter;接下来又讲解了基于 Tag 的测试环境隔离方案,以及如何基于 TagRouter 实现该方案,同时深入分析了 TagRouter 的核心实现;最后我们还介绍了 ListenableRouter 抽象类以及 ServerRouter 和 AppRouter 两个实现,它们是在条件路由的基础上添加了动态变更路由规则的能力,同时区分了服务级别和服务实例级别的配置。

面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?

Dubbo 路由机制是在服务间的调用时,通过将服务提供者按照设定的路由规则来决定调用哪一个具体的服务。


# 路由服务结构


Dubbo 实现路由都是通过实现 RouterFactory 接口。当前版本 dubbo-2.7.5 实现该接口类如下:


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


路由实现工厂类是在 router 包下


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


由于 RouterFactory 是 SPI 接口,同时在获取路由 RouterFactory#getRouter 方法上有 @Adaptive("protocol") 注解,所以在获取路由的时候会动态调用需要的工厂类。


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


可以看到 getRouter 方法返回的是一个 Router 接口,该接口信息如下


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


其中 Router#route 是服务路由的入口,对于不同类型的路由工厂,有特定的 Router 实现类。


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


以上就是通过解析 URL,获取到具体的 Router,通过调用 Router#router 过滤出符合当前路由规则的 invokers。


# 服务路由实现


上面展示了路由实现类,这几个实现类型中,ConditionRouter 条件路由是最为常用的类型,由于文章篇幅有限,本文就不对全部的路由类型逐一分析,只对条件路由进行具体分析,只要弄懂这一个类型,其它类型的解析就能容易掌握。


条件路由参数规则


在分析条件路由前,先了解条件路由的参数配置,官方文档如下:


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


条件路由规则内容如下:


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


条件路由实现分析


分析路由实现,主要分析工厂类的 xxxRouterFactory#getRouter 和 xxxRouter#route 方法。


ConditionRouterFactory#getRouter


ConditionRouterFactory 中通过创建 ConditionRouter 对象来初始化解析相关参数配置。


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


在 ConditionRouter 构造函数中,从 URL 里获取 rule 的字符串格式的规则,解析规则在 ConditionRouter#init 初始化方法中。

 
   
   
 
public void init(String rule) { try { if (rule == null || rule.trim().length() == 0) { throw new IllegalArgumentException("Illegal route rule!"); } // 去掉 consumer. 和 provider. 的标识 rule = rule.replace("consumer.", "").replace("provider.", ""); // 获取 消费者匹配条件 和 提供者地址匹配条件 的分隔符 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(); // 解析消费者路由规则 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); // NOTE: It should be determined on the business level whether the `When condition` can be empty or not. this.whenCondition = when; this.thenCondition = then; } catch (ParseException e) { throw new IllegalStateException(e.getMessage(), e); }}


以路由规则字符串中的 =>为分隔符,将消费者匹配条件和提供者匹配条件分割,解析两个路由规则后,赋值给当前对象的变量。


调用 parseRule 方法来解析消费者和服务者路由规则。

 
   
   
 
// 正则验证路由规则protected static final Pattern ROUTE_PATTERN = Pattern.compile("([&!=,]*)\s*([^&!=,\s]+)");

private static Map<String, MatchPair> parseRule(String rule) throws ParseException { /** * 条件变量和条件变量值的映射关系 * 比如 host => 127.0.0.1 则保存着 host 和 127.0.0.1 的映射关系 */ Map<String, MatchPair> condition = new HashMap<String, MatchPair>(); if (StringUtils.isBlank(rule)) { return condition; } // Key-Value pair, stores both match and mismatch conditions MatchPair pair = null; // Multiple values Set<String> values = null; final Matcher matcher = ROUTE_PATTERN.matcher(rule); while (matcher.find()) { // 获取正则前部分匹配(第一个括号)的内容 String separator = matcher.group(1); // 获取正则后部分匹配(第二个括号)的内容 String content = matcher.group(2); // 如果获取前部分为空,则表示规则开始位置,则当前 content 必为条件变量 if (StringUtils.isEmpty(separator)) { pair = new MatchPair(); condition.put(content, pair); } // 如果分隔符是 &,则 content 为条件变量 else if ("&".equals(separator)) { // 当前 content 是条件变量,用来做映射集合的 key 的,如果没有则添加一个元素 if (condition.get(content) == null) { pair = new MatchPair(); condition.put(content, pair); } else { pair = condition.get(content); } } // 如果当前分割符是 = ,则当前 content 为条件变量值 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()); } // 由于 pair 还没有被重新初始化,所以还是上一个条件变量的对象,所以可以将当前条件变量值在引用对象上赋值 values = pair.matches; values.add(content); } // 如果当前分割符是 = ,则当前 content 也是条件变量值 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; values.add(content); } // 如果当前分割符为 ',',则当前 content 也为条件变量值 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()); } // 直接向条件变量值集合中添加数据 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;}

上面就是解析条件路由规则的过程,条件变量的值都保存在 MatchPair 中的 matches、mismatches 属性中, =和 ,的条件变量值放在可以匹配的 matches 中, !=的条件变量值放在不可匹配路由规则的 mismatches 中。赋值过程中,代码还是比较优雅。


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


实际上 matches、mismatches 就是保存的是条件变量值。


ConditionRouter#route


Router#route的作用就是匹配出符合路由规则的 Invoker 集合。

 
   
   
 
// 在初始化中进行被复制的变量// 消费者条件匹配规则protected Map<String, MatchPair> whenCondition;// 提供者条件匹配规则protected Map<String, MatchPair> thenCondition;

public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException { if (!enabled) { return invokers; } // 验证 invokers 是否为空 if (CollectionUtils.isEmpty(invokers)) { return invokers; } try { // 校验消费者是否有规则匹配,如果没有则返回传入的 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; } // 遍历传入的 invokers,匹配提供者是否有规则匹配 for (Invoker<T> invoker : invokers) { if (matchThen(invoker.getUrl(), url)) { result.add(invoker); } } // 如果 result 不为空,或当前对象 force=true 则返回 result 的 Invoker 列表 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(RULE_KEY)); return result; } } catch (Throwable t) { logger.error("Failed to execute condition router rule: " + getUrl() + ", invokers: " + invokers + ", cause: " + t.getMessage(), t); } return invokers;}

上面代码可以看到,只要消费者没有匹配的规则或提供者没有匹配的规则及 force=false 时,不会返回传入的参数的 Invoker。


匹配消费者路由规则和提供者路由规则方法是 matchWhen 和 matchThen


面试官:你读过Dubbo的源码,能给我说一下它的路由机制是如何实现的吗?


这两个匹配方法都是调用同一个方法 matchCondition 实现的。将消费者或提供者 URL 转为 Map,然后与 whenCondition 或 thenCondition 进行匹配。


匹配过程中,如果 key (即 sampleValue 值)存在对应的值,则通过 MatchPair#isMatch 方法再进行匹配。

 
   
   
 
private boolean isMatch(String value, URL param) { // 存在可匹配的规则,不存在不可匹配的规则 if (!matches.isEmpty() && mismatches.isEmpty()) { // 不可匹配的规则列表为空时,只要可匹配的规则匹配上,直接返回 true for (String match : matches) { if (UrlUtils.isMatchGlobPattern(match, value, param)) { return true; } } return false; } // 存在不可匹配的规则,不存在可匹配的规则 if (!mismatches.isEmpty() && matches.isEmpty()) { // 不可匹配的规则列表中存在,则返回false for (String mismatch : mismatches) { if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { return false; } } return true; } // 存在可匹配的规则,也存在不可匹配的规则 if (!matches.isEmpty() && !mismatches.isEmpty()) { // 都不为空时,不可匹配的规则列表中存在,则返回 false for (String mismatch : mismatches) { if (UrlUtils.isMatchGlobPattern(mismatch, value, param)) { return false; } } for (String match : matches) { if (UrlUtils.isMatchGlobPattern(match, value, param)) { return true; } } return false; } // 最后剩下的是 可匹配规则和不可匹配规则都为空时 return false;}


匹配过程再调用 UrlUtils#isMatchGlobPattern 实现

 
   
   
 
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('*'); // 如果value中没有 "*" 通配符,则整个字符串值匹配 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); }}

就这样完成全部的条件路由规则匹配,虽然看似代码较为繁杂,但是理清规则、思路,一步一步还是较好解析,前提是要熟悉相关参数的用法及形式,不然代码较难理解。


# 最后


单纯从逻辑上,如果能够掌握条件路由的实现,去研究其它方式的路由实现,相信不会有太大问题。只是例如像脚本路由的实现,你得先会使用脚本执行引擎为前提,不然就不理解它的代码。最后,在 dubbo-admin 上可以设置路由,大家可以尝试各种使用规则,通过实操才能更好掌握和理解路由机制的实现。



 
   
   
 

 往期推荐 

以上是关于Dubbo——路由机制(下)的主要内容,如果未能解决你的问题,请参考以下文章

Dubbo路由机制概述

dubbo基本知识整理

dubbo系列七dubbo tag路由扩展

Dubbo在集群模式下的容错机制和负载均衡策略

Dubbo中集群Cluster,负载均衡,容错,路由解析

Dubbo3终极特性「流量治理体系」一文教你如何通过Dubbo-Admin实现动态进行流量隔离机制