定义&应用场景
标签路由通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。
下图是标签路由的一个典型场景,应用A和应用B都要访问同一组dubbo服务,应用A请求dubbo接口时传递TagA,请求就会被路由到服务中红色框的providerA、providerB,因为providerA、providerB被打上了TagA;同理,应用B因为携带了TagB,请求会被路由到providerC、providerD,因为后者被打上了TagB。
降级约定:
1、consumer携带request.tag=tag1 时优先选择 标记了tag=tag1 的 provider。若集群中不存在与请求标记对应的服务,默认将降级请求 tag为空的provider;如果要改变这种默认行为,即找不到匹配tag1的provider返回异常,需设置request.tag.force=true。
2、comsumer侧request.tag未设置时,只会匹配tag为空的provider。即使集群中存在可用的服务,若tag不匹配也就无法调用,这与约定1不同,携带标签的请求可以降级访问到无标签的服务,但不携带标签/携带其他种类标签的请求永远无法访问到其他标签的服务。
标签路由使用方式
到处我们基本清楚了什么是标签路由,标签路由的作用是什么。那下面我们来看下怎么使用标签路由。
#####provider端标签设置
######静态标签
静态标签配置也有三种方式 :
1、dubbo xml配置文件中添加如下配置指定标签
<dubbo:provider tag=“tag1”/>
或者
<dubbo:service tag=“tag1”/>
2、通过注解指定标签
@org.apache.dubbo.config.annotation.Service(tag = “tag1”)
3、通过jvm参数或os环境变量指定。如下:
java -jar xxx-provider.jar -Ddubbo.provider.tag=tag1
动态标签
在系统后台下发如下的yaml配置:dubbo-tag-route-demo应用增加了两个标签组,tag1包含一个实例127.0.0.1:20880,tag2包含一个实例127.0.0.1:20881
force: false
runtime: true
enabled: true
key: dubbo-tag-route-demo
tags:
- name: tag1
addresses: ["127.0.0.1:20880"]
- name: tag2
addresses: ["127.0.0.1:20881"]
consumer传递标签
静态标签
consumer也可以通过dubbo xml配置指定标签:
<dubbo:reference id=“xxxxxxApi” interface=“com.XXXXXApi” tag=“tag1”/>
动态标签
在发起调用dubbo前,通过RpcContext.getContext().setAttachment(Constants.TAG_KEY,“TAG_A”)携带标签。请求标签的作用域为每一次 invocation,使用 attachment 来传递请求标签,注意保存在 attachment 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。
关于标签传递:因为ConsumerContextFilter在每次请求之后会清空Attachments,所以dubbo调用A携带的tagA,要传递到下次dubbo调用B,需要应用自己来做。具体的解法可以将标签放到theadlocal中,这样dubbo调用B可以从threadlocal中获取到tagA,然后传递下去。
还有一个场景也是需要注意的 ,如果dubbo调用是在一个单独的线程或线程池中发起的 ,标签的传递也要解决跨线程传递的问题,无侵入跨线程传递参数的解决方案有阿里开源的transmittable-thread-local。
标签路由原理
上面已经清楚的标签路由的使用方式和应用场景,那dubbo的标签路由具体是怎么实现的呢?
我们先来看看整体流程。从下图知,在发起dubbo调用之后,最终会走到RouterChain来做服务调用路由。如果TagRouter在RouterChain里,则会由TagRouter来做标签路由的逻辑。
从图中可以看到具体的路由逻辑在TagRouter,而TagRouter使用的路由规则来自配置中心的。在动态标签的场景,其他系统将标签路由规则写到配置中心,配置中心通过event将路由规则通知给TagRouter。
下面结合代码来看下,TagRouter是如何做路由的。
从下面代码看到,在没有动态标签路由规则时,会根据静态标签路由规则来路由,路由的逻辑就是按照上面提到的降级约定来做的。
存在动态标签路由规则时,会先按照动态标签路由规则来路由,如果动态路由规则路由到的adress为空,则会尝试使用静态标签路由规则来路由一次。
如果请求没有携带标签,则只能路由到没有打标签的adress,不能降级到打了标签的adress,同降级约定一致。
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
if (CollectionUtils.isEmpty(invokers)) {
return invokers;
}
// since the rule can be changed by config center, we should copy one to use.
final TagRouterRule tagRouterRuleCopy = tagRouterRule;
if (tagRouterRuleCopy == null || !tagRouterRuleCopy.isValid() || !tagRouterRuleCopy.isEnabled()) {
return filterUsingStaticTag(invokers, url, invocation);
}
List<Invoker<T>> result = invokers;
String tag = StringUtils.isEmpty(invocation.getAttachment(Constants.TAG_KEY)) ? url.getParameter(Constants.TAG_KEY) :
invocation.getAttachment(Constants.TAG_KEY);
// if we are requesting for a Provider with a specific tag
if (StringUtils.isNotEmpty(tag)) {
List<String> addresses = tagRouterRuleCopy.getTagnameToAddresses().get(tag);
// filter by dynamic tag group first
if (CollectionUtils.isNotEmpty(addresses)) {
result = filterInvoker(invokers, invoker -> addressMatches(invoker.getUrl(), addresses));
// if result is not null OR it's null but force=true, return result directly
if (CollectionUtils.isNotEmpty(result) || tagRouterRuleCopy.isForce()) {
return result;
}
} else {
// dynamic tag group doesn't have any item about the requested app OR it's null after filtered by
// dynamic tag group but force=false. check static tag
result = filterInvoker(invokers, invoker -> tag.equals(invoker.getUrl().getParameter(Constants.TAG_KEY)));
}
// If there's no tagged providers that can match the current tagged request. force.tag is set by default
// to false, which means it will invoke any providers without a tag unless it's explicitly disallowed.
if (CollectionUtils.isNotEmpty(result) || isForceUseTag(invocation)) {
return result;
}
// FAILOVER: return all Providers without any tags.
else {
List<Invoker<T>> tmp = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(),
tagRouterRuleCopy.getAddresses()));
return filterInvoker(tmp, invoker -> StringUtils.isEmpty(invoker.getUrl().getParameter(Constants.TAG_KEY)));
}
} else {
// List<String> addresses = tagRouterRule.filter(providerApp);
// return all addresses in dynamic tag group.
List<String> addresses = tagRouterRuleCopy.getAddresses();
if (CollectionUtils.isNotEmpty(addresses)) {
result = filterInvoker(invokers, invoker -> addressNotMatches(invoker.getUrl(), addresses));
// 1. all addresses are in dynamic tag group, return empty list.
if (CollectionUtils.isEmpty(result)) {
return result;
}
// 2. if there are some addresses that are not in any dynamic tag group, continue to filter using the
// static tag group.
}
return filterInvoker(result, invoker -> {
String localTag = invoker.getUrl().getParameter(Constants.TAG_KEY);
return StringUtils.isEmpty(localTag) || !tagRouterRuleCopy.getTagNames().contains(localTag);
});
}