微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

Dubbo过滤器

文章目录

一、过滤器概述

dubbo 中的过滤器和 Web 应用中的过滤器的概念是一样的 , 提供了在服务调用前后插入自定义逻辑的途径 。 过滤器是整个 dubbo 框架中非常重要的组成部分 , dubbo 中有很多功能都是基于过滤器扩展而来的 。 过滤器提供了服务提供者和消费者调用过程的拦截 , 即每次执行 RPC 调用的时候 , 对应的过滤器都会生效 。 虽然过滤器的功能强大 , 但由于每次调用时都会执行 , 因此在使用的时候需要注意它对性能的影响。

过滤器配置示例如下

<!-- 消费方调用过程拦截 -->
<dubbo:reference filter="xxx , yyy" />
< ! -- 消费方调用过程拦截器 , 将拦截所有 reference -->
<dubbo:consumer filter="xxx,yyy"/>
<!-- 服务提供方调用过程拦截 -->
<dubbo:service filter="xxx , yyy" />
<!-- 服务提供方调用过程拦截器 , 将拦截所有 service -->
<dubbo:provider filter="xxx , yyy"/>

以上就是常见的配置方式 , 下面我们来了解一下配置上的一些 “ 潜规则 ” :

(1) 过滤器顺序 。
用户自定义的过滤器的顺序认会在框架内置过滤器之后 , 我们可以使用 filter=“xxx,default” 这种配置方式让自定义的过滤器顺序靠前 。
我们在配置 filter= ” xxx,yyy ” 时 , 写在前面的 xxx 会比 yyy 的顺序要靠前 。
(2) 剔除过滤器 。 对于一些认的过滤器或自动激活的过滤器 , 有些方法不想使用这些过滤器 , 则可以使用加过滤器名称来过滤 , 如 filter="-xxFilter" 会让 xxFilter 不生效 。 如果不想使用所有认启用的过滤器 , 则可以配置 filter="-defaulf 来进行剔除 。
(3) 过滤器的叠加 。 如果服务提供者 、 消费者端都配置了过滤器 , 则两边的过滤器不会互相覆盖 , 而是互相叠加 , 都会生效 。 如果需要覆盖 , 则可以在消费方使用气 ” 的方式剔除对应的过滤器 。

二、原理

1.初始化的实现原理

服务的暴露与引用会使用 Protocol 层 , 而 ProtocolFilterWrapper包装类则实现了过滤器链的组装 。 在服务的暴露与引用过程中 , 会使用 ProtocolFilterWrapper#buildInvokerChain 方法组装整个过滤器链。

2.AccessLogFilter

AccessLogFilter 是一个日志过滤器 , 如果想记录服务每一次的请求日志 , 则可以开启这个过滤器 。 虽然 AccessLogFilter 有旧 Activate 注解 , 认会被激活 , 但还是需要手动配置来开启日志的打印 。
AccessLogFilter 的实现步骤比较简短 , 主要分为构造方法和 invoke 方法两大逻辑 。 在AccessLogFilter 的构造方法中会加锁并初始化一个定时线程池 ScheduledThreadPool 该线程池只有在指定了输出的 log 文件时才会用到 , ScheduledThreadPool 中的线程会定时把队列中的日志数据写入文件 。 在构造方法中主要是初始化线程池,而打印日志的逻辑主要在 invoke 方法中 ,其逻辑如下 :
(1) 获取参数 。 获取上下文 、 接口名 、 版本 、 分组信息等参数 , 用于日志的构建 。
(2) 构建日志字符串 。 根据步骤 ( 1 ) 中的数据开始组装日志 , 最终会得到一个日志字符串 , 类似于[2019-01-15 20:13:58] 192.168.1.17:20881 -> 192.168.1.17:20882 - com.test.demo.Demo.Service testFunction ( java . lang.String ) [null]
(3) 日志打印 。 如果用户配置了使用应用本身的日志组件 , 则直接通过封装的LoggerFactory 打印日志 ; 如果用户配置了日志要输出自定义文件中 , 则会把日志加入一个 ConcurrentMap<String, ConcurrentHashSet> 中暂存 , key 是自定义的 accesslog 值( 如 accesslogicustom-access.log ” ) , value 就是对应的日志集合 。 后续等待定时线程不断遍历整个 Map, 把日志写入对应的文件

2.ExecuteLimitFilter

ExecuteLimitFilter 用于限制每个服务中每个方法的最大并发数 , 有接口级别和方法级别的配置方式 。

如果不设置 , 则认不做限制 ; 如果设置了小于等于 0 的数值 , 那么也会不做任何限制 。其实现原理是 : 在框架中使用一个 ConcurrentMap 缓存了并发数的计数器 。 为每个请求 URL生成一个 Identity String, 并以此为 key ; 再以每个 IdentityString 生成一个 RpcStatus 对象 , 并以此为 value 。 RpcStatus 对象用于记录对应的并发数 。 在过滤器中 , 会以 try-catch-finally 的形式调用过滤器链的下一个节点 。 因此 , 在开始调用之前 , 会通过 URL 获得 RpcStatus 对象 , 把对象中的并发数计数器原子+1 , 在 finally 中再将原子- 1 。 只要在计数器 +1 的时候 , 发现当前计数比设置的最大并发数大时 , 就会抛出异常 , 提示己经超过最大并发数 , 请求就会被终止并直接返回 。

3.Context Filter

ontextFilter 主要记录每个请求的调用上下文 。 每个调用都有可能产生很多中间临时信息 ,我们不可能要求在每个接口上都加一个上下文的参数 , 然后一路往下传 。 通常做法都是放在ThreadLocal 中 , 作为一个全局参数 , 当前线程中的任何一个地方都可以直接读/写上下文信息 。

ContextFilter 就是统一在过滤器中处理请求的上下文信息 , 它为每个请求维护一个 RpcContext 对象 , 该对象中维护两个 InternalThreadLocal (它是优化过的 ThreadLocal, 具体可以搜索 Netty 的 InternalThreadLocal 做了什么优化) , 分别记录 local 和 server 的上下文 。 每次收到或发起 RPC 调用的时候 , 上下文信息都会发生改变 。 例如 : A 调用 B, B 调用 C 。 当 A 调用 B 且 B 还未调用 C 时 , RpcContext 中保存 A 调用 B 的上下文 ; 当 B 开始调用 C 的时候 ,RpcContext 中保存 B 调用 C 的上下文 。 发起调用时候的上下文是由 ConsumerContextFilter 实现的 , 这个是消费者端的过滤器。

ContextFilter 的主要逻辑如下 :

(1) 清除异步属性 。 防止异步属性传到过滤器链的下一个环节 。
(2) 设置当前请求的上下文 , 如 Invoker 信息 、 地址信息 、 端口信息等 。 如果前面的过滤器已经对上下文设置了一些附件信息 ( attachments 是一个 Map, 里面可以保存各种 key-value数据) , 则和 Invoker 的附件信息合并 。
(3) 调用过滤器链的下一个节点 。
(4) 清除上下文信息 。 对于异步调用的场景 , 即使是同一个线程 , 处理不同的请求也会创建一个新的 RpcContext 对象 。 因此调用完成后 , 需要清理对应的上下文信息 。

4.ExceptionFilter

光看 ExceptionFilter 很容易会认为它和我们平时写业务时统一处理异常的过滤器一样 。 它的关注点不在于捕获异常 , 而是为了找到那些返回的自定义异常 , 但异常类可能不存在于消费者端 , 从而防止消费者端序列化失败 。 对于所有没有声明 Unchecked 的方法抛出的异常 ,ExceptionFilter 会把未引入的异常包装到 RuntimeException 中 , 并把异常原因字符串化后返回 因此 , ExceptionFilter 的逻辑都在 onResponse 方法中 。ExceptionFilter 过滤器会打印出 ERROR 级别的错误日志 , 但并不会处理泛化调用 , 即 Invoker的接口是 GenericService

下面我们来看一下 onResponse 方法中是如何处理异常的 :

(1) 判断是否是泛化调用 。 如果是泛化调用则直接不处理了 。
(2) 直接抛出一些异常 。 如果异常是 Java 自带异常 , 并且是必须显式使用 try-catch 来捕获的异常 , 则直接抛出 。 如果异常在 Invoker 的签名中出现 , 则直接抛出异常 , 并打印 error log。如果异常类和接口类在同一个 jar 包中 , 则直接抛出异常 ; 如果是 JDK 中的异常 , 则直接抛出 ;如果是 dubb 。 中定义的异常 , 则直接抛出 。
(3) 处理在步骤 (2) 中无法处理的异常 。 把异常转换为字符串 , 并包装成一个 RuntimeException放入 RpcResult 中返回 。

5.TimeoutFilter

TimeoutFilter 主要是日志类型的过滤器 , 它会记录每个 Invoker 的调用时间 , 如果超过了接口设置的 timeout 值 , 则会打印一条警告日志 , 并不会干扰业务的正常运行 。

6.TokenFilter

dubbo 中 , 如果某些服务提供者不想让消费者绕过注册中心直连自己 , 则可以使用令牌验证 。 总体的工作原理是 , 服务提供者在发布自己的服务时会生成令牌 , 与服务一起注册注册中心 。 消费者必须通过注册中心才能获取有令牌的服务提供者的 URL 。 TokenFilter 是在服务提供者端生效的过滤器 , 它的工作就是对请求的令牌做校验 。

7.TpsFilter

TpsLimitFilter 主要用于服务提供者端的限流 。 我们会发现在 org. apache, dubbo. rpc . Filter这个 SPI 配置文件中 , 并没有 TpsLimitFilter 的配置 , 因此如果需要使用 , 则用户要自己添加对应的配置 。 TpsLimitFilter 的限流是基于令牌的 , 即一个时间段内只分配 N 个令牌 , 每个请求过来都会消耗一个令牌 , 耗完即止 , 后面再来的请求都会被拒绝 。 限流对象的维度支持分组 、 版本和接口级别 , 认通过 interface + group + version 作为唯一标识来判断是否超过最大值 。

具体的实现逻辑主要关注 DefaultTPSLimiter#isAllowable, 会用这个方法判断是否触发限流 , 如果不触发就直接通过了 。 isAllowable 方法的逻辑如下 :

(1) 获取 URL 中的参数 , 包含每次发放的令牌数 、 令牌刷新时间间隔
(2) 如果设置了每次发放的令牌数则开始限流校验 。 DefaultTPSLimiter 内部用一个
ConcurrentMap 缓存每个接 口的令牌数 , key 是 interface + group + version, value 是一个 Statitem对象 , 它包装了令牌刷新时间间隔 、 每次发放的令牌数等属性 。 首先判断上次发放令牌的时间点到现在是否超过时间间隔了 , 如果超过了就重新发放令牌 , 之前没用完的不会叠加 , 而是直接覆盖 。 然后 , 通过 CAS 的方式 -1 令牌 , 减掉后令牌数如果小于 0 则会触发限流 。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐