代码改变世界

dubbo rpc filter实现剖析(一)

2018-09-05 00:04  chen.simon  阅读(1364)  评论(0编辑  收藏  举报

2.6.3版本,之前读的是2.4.9版本
本篇主要阐述dubbo rpc的filter的实现,包括作用,用法,原理,与Spring Cloud在这些能力的对比。

共提供了多少个?是哪些?发布时默认装配了哪些给他自身的扩展点机制?

从类与接口关系分析的结果文档中可以看到共20个:
241 Filter
--241.1 CacheFilter
--241.2 MonitorFilter
--241.3 AccessLogFilter
--241.4 ActiveLimitFilter
--241.5 ClassLoaderFilter
--241.6 CompatibleFilter
--241.7 ConsumerContextFilter
--241.8 ContextFilter
--241.9 DeprecatedFilter
--241.10 EchoFilter
--241.11 ExceptionFilter
--241.12 ExecuteLimitFilter
--241.13 GenericFilter
--241.14 GenericImplFilter
--241.15 TimeoutFilter
--241.16 TokenFilter
--241.17 TpsLimitFilter
--241.18 FutureFilter
--241.19 TraceFilter
--241.20 ValidationFilter

从发布的jar中的META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Filter中发现除了TpsLimitFilter之外,其余的都装上了。
cache=com.alibaba.dubbo.cache.filter.CacheFilter
validation=com.alibaba.dubbo.validation.filter.ValidationFilter
echo=com.alibaba.dubbo.rpc.filter.EchoFilter
generic=com.alibaba.dubbo.rpc.filter.GenericFilter
genericimpl=com.alibaba.dubbo.rpc.filter.GenericImplFilter
token=com.alibaba.dubbo.rpc.filter.TokenFilter
accesslog=com.alibaba.dubbo.rpc.filter.AccessLogFilter
activelimit=com.alibaba.dubbo.rpc.filter.ActiveLimitFilter
classloader=com.alibaba.dubbo.rpc.filter.ClassLoaderFilter
context=com.alibaba.dubbo.rpc.filter.ContextFilter
consumercontext=com.alibaba.dubbo.rpc.filter.ConsumerContextFilter
exception=com.alibaba.dubbo.rpc.filter.ExceptionFilter
executelimit=com.alibaba.dubbo.rpc.filter.ExecuteLimitFilter
deprecated=com.alibaba.dubbo.rpc.filter.DeprecatedFilter
compatible=com.alibaba.dubbo.rpc.filter.CompatibleFilter
timeout=com.alibaba.dubbo.rpc.filter.TimeoutFilter
trace=com.alibaba.dubbo.rpc.protocol.dubbo.filter.TraceFilter
future=com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter
monitor=com.alibaba.dubbo.monitor.support.MonitorFilter

这些filter都有什么作用?如何使用?实现原理是什么?Spring Cloud是否也提供了这些能力?有什么差异?

CacheFilter

作用

缓存调用结果,比如配置在consumer端, 比如我们通过id查某个用户的信息,对于特定的一个id,在consumer端第一次调用时会给provider端发请求,后面再调用时,直接用consumer端缓存的结果返回,你不再发请求给provider端。

使用方式

consumer侧的配置:

<dubbo:reference id="userService" interface="org.simonme.dubbo.demo.provider.service.UserService" filter="cache">
<dubbo:parameter key="cache" value="lru" />
</dubbo:reference>

能支持的全部cache定义在META-INF/dubbo/internal/com.alibaba.dubbo.cache.CacheFactory 当然你也可以遵循dubbo扩展点机制进行扩展。
默认提供三种:
threadlocal=com.alibaba.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory
lru=com.alibaba.dubbo.cache.support.lru.LruCacheFactory
jcache=com.alibaba.dubbo.cache.support.jcache.JCacheFactory

实现原理

缓存的key是你远程方法调用时传递的所有参数按规则组装成字符串作为key。
具体规则就是: 基本类型 直接拼接,复合类型转成json字符串后再拼接。

实现原理也不是很复杂,根据invoker的url找到其对应的cache对象,再跟据上述缓存的key找到缓存的结果。
有个不是太要紧的小问题,因为是根据invoker的url找到其对应的cache对象的,又因为invoker的url中含有remote.timestamp参数,所以你如果启用了consumer侧的缓存,consumer一直在服务状态,此时provider服务做了重启,那么consumer侧的缓存失效,会重新调用provider端。

ThreadLocalCache 是ThreadLocal配合HashMap实现
LRUCache 是继承自LinkedHashMap,同时结合ReentrantLock实现的线程安全的lru cache(最近最少使用),LinkedHashMap自带lru性质,通过构造参数控制,默认是fifo。
jache是封装的JCache API (JSR 107)。

与Spring Cloud对比

Spring Cloud提供了consumer侧的缓存能力,Hystrix组件支持用requestCache.enabled配置是否启用缓存,也支持用cacheKeyMethod注解指定getkey方法。

ValidationFilter

作用

在consumer和provider端提供了校验能力

使用方式

假设你要对consumer端进行校验,在配置文件中配置如下:

<dubbo:reference id="userService" interface="org.simonme.dubbo.demo.provider.service.UserService" filter="validation">
    <dubbo:parameter key="validation" value="JValidator" />
    
    <!-- 配置一个实现了javax.validation.spi.ValidationProvider<T>接口校验器 -->
    <dubbo:parameter key="jvalidation" value="org.hibernate.validator.HibernateValidator" />
</dubbo:reference>

filter要是配置多个的话,用逗号拼接,但是逗号前后不能有空格。
此处使用了hibernate的validator ,在你需要校验的接口方法上加校验注解即可,示例如下:

public User queryUser(@Range(min=0,message="用户id值不能小于0")int id);

当consumer端调用时传递了校验不通过的参数时,会收到ConstraintViolationException的异常。

实现原理

dubbo对接了javax.validation.Validation,hibernate等都有对其对接的实现,按需使用即可。也就是说dubbo自己不做具体校验的事情。

与Spring Cloud对比

spring在很早就支持validator。

EchoFilter

作用

在provider端提供回声服务的服务端的实现。

使用方式

这个filter略有特殊,无需在provider端的dubbo:service标签的filter中去配置,只要你在consumer做了echo回声调用,他都会产生作用,调试的时候也能看到能走到EchoFilter中。

consumer端的示例代码:

public class HelloClientTest
{
    @Autowired
    private HelloService helloService;

    @SuppressWarnings("static-access")
    @Test
    public void testSayHello()
    {
        System.out.println(((EchoService)helloService).$echo("aaaa"));
    }
}

就是把你的service类强转成EchoService,至于为什么能强转,可以参见之前写的文章 reference bean发起调用

实现原理

直接看EchoFilter代码,很简单,不再多说。

与Spring Cloud对比

Spring Cloud貌似没有这个能力。

GenericFilter

作用

先讨论一个问题,rpc,最简单的场景是consumer端调用provider端的一个服务,这个服务双方都遵循一个接口实现,按最简单的dubbo的demo玩法,是需要consumer和provider两段都要有这个接口声明的(包括接口参数的类型的相关类),比如:
org.simonme.dubbo.demo.provider.service.UserService.queryUser(int) 这是一个查询用户的服务接口,但是,如果consumer端没有这个服务的接口声明及其相关联的bean类,也就是如果仅仅在provider端能找到这个接口类,在consumer工程里压根没有这个类,那是否还能进行调用?
dubbo是可以的。 就是通过在proivder侧用GenericFilter达成目的。

使用方式

需要在consumer端声明这个bean的时候,加上generic="true"配置即可。

<dubbo:reference id="userService" interface="org.simonme.dubbo.demo.provider.service.UserService" generic="true">
</dubbo:reference>

generic还支持nativejava和bean两个可选的选项。nativejava对应byte[]类型的参数,bean对应com.alibaba.dubbo.common.beanutil.JavaBeanDescriptor类型的参数。

此外在使用这个服务时,形式略有不同:

    @Autowired
    private GenericService userService;
    
    @SuppressWarnings("static-access")
    @Test
    public void testSayHello()
    {
        Object result = userService.$invoke("queryUser", new String[] { "int" },  new Object[]{100});
        System.out.println(result);
        System.out.println(result.getClass());
    }

GenericService 表示你这个未知的服务类型,用$invoke这个特殊方法发起的你的服务方法调用。调用完之后,dubbo会把结果以hashmap的形式返回给你。比如,我这里是个User的bean,User有id和name字段,那么返回的map中就与id和value两个key,其对应的值就是bean的这两个字段的字段值。
为了便于理解,贴一下UserService的代码:

public interface UserService
{
    public User queryUser(@Range(min=0,message="用户id值不能小于0")int id);
}

上面这个写法,解释了怎么在consumer端没有org.simonme.dubbo.demo.provider.service.UserService接口声明的时候,调用他的queryUser方法。provider端无需改动,和正常写rpc服务一样配置即可。
用法也可以参见官方文档,使用泛化调用

实现原理

直接看GenericFilter代码,通过方法名(必须是$invoke),参数个数等判断是否命中,很简单,不再多说。

与Spring Cloud对比

Spring Cloud可以算有这个能力,因为Spring Cloud是http json的,http json天然不需要调用端有接口声明。

GenericImplFilter

作用

与 GenericFilter  类似,是一个相对的东西,他是用在provider端没有服务接口声明类时,使用的。使用方法可以参见官方文档实现泛化调用。不再多说。

TokenFilter

作用

官方文档说法是:

通过令牌验证在注册中心控制权限,以决定要不要下发令牌给消费者,可以防止消费者绕过注册中心访问提供者,另外通过注册中心可灵活改变授权方式,而不需修改或升级提供者。

使用方式

provider端:在provider上配置token值。 TokenFilter会在provider侧校验。

<dubbo:service interface="org.simonme.dubbo.demo.provider.service.UserService" ref="m00001.app001.xx.userService" timeout="600000" token="123456">

consumer端可以用编程的方式获取后塞,

RpcContext.getContext().setAttachment("token", "a37b6115-c171-43cd-b65c-38b636ee96cc");

或者通过配置parameter,

<dubbo:parameter key="token" value="123456" />

或者啥都不要处理,默认consumer会从provider服务url中解析到。provider的url中会含有token字段。

实现原理

如果 token 配置的是true, 那么在provider export服务时,ServiceConfig会生成UUID,这个其实不是由注册中心生成的。
当然token也支持配置固定密码。
比对过程:

if (!ConfigUtils.isEmpty(token)) {
    if (ConfigUtils.isDefault(token)) {// 是 true或者default字段串就是表示默认
        map.put(Constants.TOKEN_KEY, UUID.randomUUID().toString());
    } else {
        map.put(Constants.TOKEN_KEY, token);
    }
}

consumer端如果没有通过上述代码的方式或者parameter配置的方式传送token,那么consumer端会在调用时,先将从注册中心拿到的provider端的url中部分参数转换成attachment给consumer端用,这个部分参数就包括token。具体代码在DubboInvoker中,如下:

public DubboInvoker(Class<T> serviceType, URL url, ExchangeClient[] clients, Set<Invoker<?>> invokers) {
    super(serviceType, url, new String[]{Constants.INTERFACE_KEY, Constants.GROUP_KEY, Constants.TOKEN_KEY, Constants.TIMEOUT_KEY});// 此处会调用父类方法进行需要的参数从url转到attachment中
    this.clients = clients;
    // get version.
    this.version = url.getParameter(Constants.VERSION_KEY, "0.0.0");
    this.invokers = invokers;
}

当provider端使用注册中心,consumer试图不带token进行直接消费时,会被拒绝。 当consumer端也是连注册中心时,哪怕不送显式送token(实际上dubbo会自动送)也可以正常调用。但是如果consumer端用了注册中心,且显式送了token,那么就要送对。否则报错。

与Spring Cloud对比

Spring Cloud的注册中心eureka也是支持密码验证的。

AccessLogFilter

作用

用在provider端打印rpc请求日志,支持打到指定文件,支持异步。

使用方式

在provider侧配置名为accesslog的filter,若需要指定路径,则将accesslog参数设置成具体路径即可,默认需要将其配置成true。

<dubbo:service interface="org.simonme.dubbo.demo.provider.service.UserService" ref="m00001.app001.xx.userService" filter="accesslog" 
    timeout="600000" token="123456">
    <dubbo:parameter key="accesslog" value="true" />

</dubbo:service>

实现原理

参见AccessLogFilter 代码,比较简单。

与Spring Cloud对比

zuul网关也支持。
zuul.debug.request=true #如果设置了这个,默认所有的请求都会debug
zuul.include-debug-header: true #打印头
未设置zuul.debug.request=true,可以用zuul_host:zuul_port/路径?debug=true debug你的指定请求

ActiveLimitFilter

作用

在consumer端实现并发数控制,能支持到方法级。

使用方式

在consumer侧配置

<dubbo:reference interface="com.foo.BarService">
    <dubbo:method name="sayHello" actives="10" />
</dubbo:service>

实现原理

ConcurrentHashMap 配合 AtomicInteger AtomicLong完成。相关代码参见 ActiveLimitFilter 与 RpcStatus。

与Spring Cloud对比

Spring Cloud也支持。

ExecuteLimitFilter 与之类似,只是用在了provider端。

ClassLoaderFilter

作用

保持 调用下层invoker前后的ClassLoader一致

ContextFilter

作用

在provider端,对一些dubbo自己使用的保留key进行过滤,防止别人误传。

实现原理

参见代码即可,比较简单。

与Spring Cloud对比

N/A