路由是通过互联网把信息从源地址传输到目的地址的过程,而决定路由目标地址的是路由规则。在Dubbo里,路由规则在发起一次RPC调用前起到过滤目标服务器地址的作用,过滤后的地址列表,将作为消费端最终发起RPC调用的备选地址。它能控制流量的走向,可用于服务治理,如流量隔离、灰度发布等。
关于Dubbo路由规则的详细介绍可以直接看官方文档路由规则一小节,这里就不多叙述了,本文主要介绍下静态标签的使用与扩展。
首先区分两个概念:路由和负载均衡,在Dubbo里
路由:在不同的服务间进行分发
负载均衡:在相同服务的不同实例间进行分发
下图是Dubbo从业务中发起调用到真正执行远程调用的流程示意图,可见路由发生在负载均衡之前。
对应的源码在org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker#invoke中
@Override
public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
// binding attachments into invocation.
Map<String, String> contextAttachments = RpcContext.getContext().getAttachments();
if (contextAttachments != null && contextAttachments.size() != 0) {
((RpcInvocation) invocation).addAttachments(contextAttachments);
}
//根据路由规则获取符合条件的服务
List<Invoker<T>> invokers = list(invocation);
//初始化负载均衡策略
LoadBalance loadbalance = initLoadBalance(invokers, invocation);
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
//根据负载均衡策略在路由结果的服务中选择一个服务进行调用
return doInvoke(invocation, invokers, loadbalance);
}
启动时通过订阅注册中心的服务把符合条件的服务放在RouterChain的invokers集合里
这里放一个静态标签路由过程的源码流程图,感兴趣的可以跟着源码走一走
测试时使用的Dubbo版本是2.7.3
dubbo:
scan:
base-packages: com.restkeeper
protocols:
dubbo:
name: dubbo
port: -1
serialization: kryo
registry:
address: spring-cloud://xx.xx.28.39
consumer:
timeout: 600000
provider:
# 统一版本号
version: 1.0.0
# 静态标签,在dubbo-admin上可以添加动态标签,动态标签优先级高于静态标签
tag: wyz
那么在consumer端必须在对应的dubbo配置中添加标签,否则无法消费
dubbo:
scan:
base-packages: com.restkeeper
protocols:
dubbo:
name: dubbo
port: -1
serialization: kryo
registry:
address: spring-cloud://xx.xx.28.39
consumer:
timeout: 600000
tag: wyz
provider:
# 统一版本号
version: 1.0.0
@Service(version = "1.0.0", protocol = "dubbo",tag = "wyz")
consumer端有两种调用方式
@Reference注解里添加,这种用法写的太死
@Reference(version = "1.0.0", check = false, tag = "wyz")
在服务调用前使用RpcContext传递标签,例
RpcContext.getContext().setAttachment(Constants.TAG_KEY,"wyz");
operatorUserService.login(loginName,loginPass);
显然,在每次调用RPC服务时使用RpcContext设置标签过于繁琐,官网上建议“通过 servlet 过滤器(在 web 环境下),或者定制的 SPI 过滤器设置 dubboTag”,利用servlet 过滤器有个缺点就是,如果在一次http调用中有多次RPC调用,那么除第一次外后续所有RPC调用在路由时都没有标签(每次RPC调用都会清除RpcContext的内容)。所以我们考虑利用Dubbo的SPI机制做扩展。
这里我们利用静态标签实现一个根据请求参数将请求分发到对应服务的功能。
先看Dubbo调用时的顺序,路由和负载选择发生在图中红色的部分
因此,我们可以在红色部分进行扩展,将标签信息提前放置进去。看上面调用链路图,我们只要仿照TagRouter,在TagRouter前面添加一个router专门用来添加标签信息即可。
我测试的dubbo版本是2.7.3,在这个版本的RouterFactory类上面有下面这么一段注释
See {@link CacheableRouterFactory} and
{@link RouterChain} for how to extend a
new Router or how the Router instances
are loaded
我们看类CacheableRouterFactory,它上面又有这么一段注释
If you want to provide a router implementation
based on design of v2.7.0, please extend from
this abstract class.For 2.6.x style router,
please implement and use RouterFactory directly.
这就很明显了,我们是2.7.3版本,所以直接继承CacheableRouterFactory就可以了。先创建一个用于打标签的router类,注意priority即可。
public class GrayRouter extends AbstractRouter {
public static final String NAME = "GRAY_ROUTER";
/**
* Router继承了Comparable,实现了compareTo方法基于priority进行升序排序
* RouterChain的构造函数里基于SPI加载了所有router后会对所有的router进行排序
* 所以,如果对router有顺序要求的话,必须设置priority的值,我们这需要比TagRouter
* 的priority小
*/
private static final int GRAY_ROUTER_PRIORITY = 0;
public GrayRouter() {
this.priority = GRAY_ROUTER_PRIORITY;
}
@Override
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
//只是单纯地给传递标签,并不过滤invokers,TenantContext就是一个ThreadLocal,值是web拦截器拦截时保存的请求参数
invocation.getAttachments().put(Constants.TAG_KEY, (String) TenantContext.get(Constants.TAG_KEY));
return invokers;
}
}
创建CacheableRouterFactory的子类
//这里的order只能控制RouterFactory的加载顺序,并不能控制router的排序
@Activate(order = 0)
public class GrayRouterFactory extends CacheableRouterFactory {
@Override
protected Router createRouter(URL url) {
return new GrayRouter();
}
}
然后利用Dubbo里设置好的SPI机制,在Resource路径的META-INFO/dubbo(或者dubbo/internal路径下)下创建名为org.apache.dubbo.rpc.cluster.RouterFactory的文件,文件内容是
gray=com.restkeeper.dubbo.GrayRouterFactory
然后我们就可以测试了,这里我们把标签信息放到请求头上,然后在web拦截器里保存一下
测试接口,请求头带标签
请求头不带标签,因为我没有专门起一个不带标签的服务,所以路由降级的时候直接报找不到服务
在GrayRouter里,我们把标签信息放到了ThreadLocal里,所以只要是一次http请求,在一个线程里,无论多少次RPC调用都能设置上标签。
此外,如果需要将标签信息在服务链路中传递,那么可以扩展Filter,设置一个ConsumerFilter在服务调用时把标签信息保存到invocation的attachment里,设置一个和ProviderFilter在服务被调用时保存到ThreadLocal里,扩展的流程可参考调用拦截扩展,这里就不写了,注意一个点就行,在dubbo的ContextFilter里,它会unloading一些特定的参数,像我们上面的Constants.TAG_KEY都会被清掉,所以标签的名字要注意别跟会unloading的参数名一样,或者干脆设置扩展的filter执行顺序在ContextFilter后面。
最后说一下,路由和version与group的区别,version与group有点像静态的路由,如果服务的version和group不匹配,那么它就不会出现在RouterChain的invokers集合里,而路由,由于降级策略,即使有不匹配标签的服务也会出现在invokers里,而且路由规则是在每次RPC调用时都会执行的。
参考:
1.Dubbo路由功能实现灰度发布及源码分析
2.深入理解RPC框架原理与实现 华钟明著