Dubbo filter扩展

filter其实是一种责任链模式,每个filter只负责完成自己职责的部分,解除耦合,这种设计模式很利于扩展。
大家可能对Dubbo的filter不太熟悉,但是应该都写过Servlet的filter,让我们先来回顾一下Servlet的Filter:

public interface Filter {

    /** 
    * Called by the web container to indicate to a filter that it is being placed into
    * service. The servlet container calls the init method exactly once after instantiating the
    * filter. The init method must complete successfully before the filter is asked to do any
    * filtering work. <br><br>

        * The web container cannot place the filter into service if the init method either<br>
        * 1.Throws a ServletException <br>
        * 2.Does not return within a time period defined by the web container 
    */
    public void init(FilterConfig filterConfig) throws ServletException;
    
    
    /**
    * The <code>doFilter</code> method of the Filter is called by the container
    * each time a request/response pair is passed through the chain due
    * to a client request for a resource at the end of the chain. The FilterChain passed in to this
    * method allows the Filter to pass on the request and response to the next entity in the
    * chain.<p>
    * A typical implementation of this method would follow the following pattern:- <br>
    * 1. Examine the request<br>
    * 2. Optionally wrap the request object with a custom implementation to
    * filter content or headers for input filtering <br>
    * 3. Optionally wrap the response object with a custom implementation to
    * filter content or headers for output filtering <br>
    * 4. a) <strong>Either</strong> invoke the next entity in the chain using the FilterChain object (<code>chain.doFilter()</code>), <br>   
    ** 4. b) <strong>or</strong> not pass on the request/response pair to the next entity in the filter chain to block the request processing<br>
    ** 5. Directly set headers on the response after invocation of the next entity in the filter chain.
    **/
    public void doFilter ( ServletRequest request, ServletResponse response, FilterChain chain ) throws IOException, ServletException;

    /**
    * Called by the web container to indicate to a filter that it is being taken out of service. This 
    * method is only called once all threads within the filter's doFilter method have exited or after
    * a timeout period has passed. After the web container calls this method, it will not call the
    * doFilter method again on this instance of the filter. <br><br>
    * 
        * This method gives the filter an opportunity to clean up any resources that are being held (for
    * example, memory, file handles, threads) and make sure that any persistent state is synchronized
    * with the filter's current state in memory.
    */

    public void destroy();

}

我们重写doFilter,在chain.doFilter(request, response) 前后做一些切面的工作,比如防XSS攻击、CROS跨域请求处理、记录相关日志等,调用逻辑可以用下图来概括:


Servlet filter.png

类似于Servlet中的filter,Dubbo也可以通过扩展filter来增强功能,Dubbo服务提供方和服务消费方均支持调用过程拦截,并且Dubbo 自身的大多功能均基于此扩展点实现,下面例举部分filter:
EchoFilter -> 用于provider的回声测试,检测服务是否正常
ContextFilter -> 用于provider接收RpcContext的参数
ConsumerContextFilter -> 用于consumer传递RpcContext的参数
ExecuteLimitFilter -> 用于provider的限流
ActiveLimitFilter -> 用于consumer的限流
ExceptionFilter -> 用于provider对异常进行封装
GenericFilter -> 用于provider的泛化调用,可用于集成通用服务测试框架或为其他语言调用服务提供Restful接口的支持
AccessLogFilter -> 用于provider 的access log记录
ClassLoaderFilter -> 用于provider切换当前的ClassLoader
MonitorFilter -> 用于dubbo monitor模块对consumer和provider进行监控

Dubbo filter的调用逻辑可以用下图来概括:


Dubbo filter.png

那么Dubbo是怎么将多个filter串起来的呢?
答案就位于ProtocolFilterWrapper这个类的buildInvokerChain方法。

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol){
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }

    public int getDefaultPort() {
        return protocol.getDefaultPort();
    }

    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (Constants.REGISTRY_PROTOCOL.equals(invoker.getUrl().getProtocol())) {
            return protocol.export(invoker);
        }
        return protocol.export(buildInvokerChain(invoker, Constants.SERVICE_FILTER_KEY, Constants.PROVIDER));
    }

    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (Constants.REGISTRY_PROTOCOL.equals(url.getProtocol())) {
            return protocol.refer(type, url);
        }
        return buildInvokerChain(protocol.refer(type, url), Constants.REFERENCE_FILTER_KEY, Constants.CONSUMER);
    }

    public void destroy() {
        protocol.destroy();
    }

    private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, String key, String group) {
        Invoker<T> last = invoker;
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);
        if (filters.size() > 0) {
            for (int i = filters.size() - 1; i >= 0; i --) {
                final Filter filter = filters.get(i);
                final Invoker<T> next = last;
                last = new Invoker<T>() {

                    public Class<T> getInterface() {
                        return invoker.getInterface();
                    }

                    public URL getUrl() {
                        return invoker.getUrl();
                    }

                    public boolean isAvailable() {
                        return invoker.isAvailable();
                    }

                    public Result invoke(Invocation invocation) throws RpcException {
                        return filter.invoke(next, invocation);
                    }

                    public void destroy() {
                        invoker.destroy();
                    }

                    @Override
                    public String toString() {
                        return invoker.toString();
                    }
                };
            }
        }
        return last;
    }
    
}

看明白这里首先要理解Dubbo的SPI扩展点机制,List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class).getActivateExtension(invoker.getUrl(), key, group);这一行是获取Filter接口的所有被标注为@Activate的扩展点,然后基于回调让前一个filter调用后一个filter从而串成一个调用链,调用的先后顺序是由每个filter定义的order属性决定的(不声明默认为0),order值越小则调用优先级越高。

了解了Dubbo filter的作用和原理,那让我们来看看如何扩展:
Maven 项目结构:

src
  |-main
    |-java
      |-com
        |-xxx
           |-XxxFilter.java (实现Filter接口)

Dubbo自身的过滤器配置都放在resources/META-INF/dubbo/internal下,我们扩展的过滤器一版放在resources/META-INF/dubbo/下:
     |-resources
      |-META-INF
        |-dubbo
          |-com.alibaba.dubbo.rpc.Filter (纯文本文件,内容为:xxx=com.xxx.XxxFilter)

XxxFilter.java:

package com.xxx;
 
import com.alibaba.dubbo.rpc.Filter;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Invocation;
import com.alibaba.dubbo.rpc.Result;
import com.alibaba.dubbo.rpc.RpcException;
 
public class XxxFilter implements Filter {
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        // before filter ...
        Result result = invoker.invoke(invocation);
        // after filter ...
        return result;
    }
}

META-INF/dubbo/com.alibaba.dubbo.rpc.Filter:

xxx=com.xxx.XxxFilter

我司生产环境中利用Dubbo filter扩展来记录服务调用日志和服务调用链追踪。

1.服务调用日志记录:
服务调用日志记录分为provider日志和consumer日志两部分,provider日志记录的是当前工程作为provider的服务提供日志,consumer日志记录的是当前工程作为consumer的服务消费日志,以下是部分consumer日志内容:

2018-06-07 18:15:22,971 [main] INFO  consumerMonitor -  [DUBBO] consumer[null,192.168.15.215:9015,2018-06-07 18:15:22.960] -> provider[nil,172.16.11.120:20942,2018-06-07 18:15:22.971] - com.bj58.qf.service.GoodsSkuService findSkuItemById(java.lang.Long) elapse:11ms  SUCCESS [2291038187773442], dubbo version: 2.5.3, current host: 192.168.15.215
2018-06-07 18:15:22,982 [main] INFO  consumerMonitor -  [DUBBO] consumer[null,192.168.15.215:9015,2018-06-07 18:15:22.971] -> provider[nil,172.16.11.120:20942,2018-06-07 18:15:22.982] - com.bj58.qf.service.GoodsSkuService findSkuItemById(java.lang.Long) elapse:11ms  SUCCESS [2291414190906882], dubbo version: 2.5.3, current host: 192.168.15.215
2018-06-07 18:15:22,995 [main] INFO  consumerMonitor -  [DUBBO] consumer[null,192.168.15.215:9015,2018-06-07 18:15:22.983] -> provider[nil,172.16.11.120:20942,2018-06-07 18:15:22.995] - com.bj58.qf.service.ShopGoodsService queryStorageBySkuId(java.lang.Long) elapse:12ms  SUCCESS [2290217867189761], dubbo version: 2.5.3, current host: 192.168.15.215

日志会记录每一次调用的consumer的ip、端口、调用时间、provider的ip、端口、接口请求的时间、调用的方法、调用耗时、调用结果(成功或失败,失败则打印异常)、方法入参(可选)、返回值(可选)等。
由于每次调用的入参和返回值的内容比较多,所以方法入参和返回值是否打印都是可以配置的,filter会根据当前配置的日志等级去打印。

2.调用链追踪:
Dubbo Filter结合brave + zipkin实现RPC调用链追踪和梳理项目间的依赖关系,filter中用brave向zipkin服务器异步发送http请求(也可以用kafka),zipkin服务器对数据进行分析汇总,很容易分析出性能的瓶颈在哪,效果如下:


image.png
image.png

如果有人对这些具体实现感兴趣,笔者会开放源码。

最后提醒一点,filter的实现中不要写太耗时的方法,会影响性能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • 0 准备 安装注册中心:Zookeeper、Dubbox自带的dubbo-registry-simple;安装Du...
    七寸知架构阅读 13,966评论 0 88
  • Dubbo是什么 Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方式来架构,使用这种方式...
    Coselding阅读 17,167评论 3 196
  • 默默继续写着吧,不为别的,只为自我的一种兴趣和寄托,其实,全为兴趣也是假的,更确切地说,我是为了现实中的名和利...
    蓝墨水的上游阅读 174评论 0 0
  • 18岁的时光 是四季里的阳春 18岁的丫头 是三月里的桃花 蝴蝶飞舞 白鹤徜徉 你, 是我最好的作品 18年来 我...
    旅京妈妈阅读 1,038评论 3 13