Dubbo源码解析-过滤器

898 阅读8分钟

1.过滤器概述

       过滤器的总体设计符合软件设计中的开闭原则,即通过扩展而不是修改原有设计的方式来实现新的功能需求。具体是由责任链的设计模式完成的。其中责任链模式如下图所示,当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以使每个加工组件化,减少耦合。也可以使用在当一个request过来的时候,需要找到合适的加工方式。当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工。

        那么可以看出基于责任链的设计具备了低耦合,简化对象之间的相互链接,更灵活地扩展等优点,但也会带来一定的性能损耗,而且还有可能因为使用设计不当导致死循环/逻辑错误等,总体来说,对于一个以请求/响应模型为主的RPC框架来说利大于弊,更有利于扩展。Dubbo过滤器框架对整个调用过程进行了拦截,提供了在调用前后插入自定义逻辑的途径。

2.过滤器总体结构

        Filter接口上有@SPI注解,属于扩展点接口,还有一个内部定义Listener接口定义,Filter主要关注调用,Listener关注响应后的信息回调处理。

@SPI
public interface Filter {

    Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException;

    interface Listener {

        void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation);

        void onError(Throwable t, Invoker<?> invoker, Invocation invocation);
    }
}

如下图所示,可以看到Dubbo框架自带了很多的Filter实现类,实现了各式各样的功能,很多Filter实现类上都使用了@Activate注解,在ExtensionLoader处理中会被默认激活使用, 当然Filter实现类也加上了条件比如服务提供端/消费端、带有指定参数的条件激活。可以更好地构造合适的过滤器执行链。

3.过滤器实现原理

        ServiceConfig#doExportUrlFor1Protocol ReferenceConfig#服务暴露和引用都是在Protocol层完成的,这里ProtocolFilterWrapper包装类实现了过滤器链的组装,在服务暴露和引用过程都是使用ProtocolFilterWrapper#buildInvokeChain方法实现的。

public class ProtocolFilterWrapper implements Protocol {
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        if (UrlUtils.isRegistry(invoker.getUrl())) {
            return protocol.export(invoker);
        }
        return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        if (UrlUtils.isRegistry(url)) {
            return protocol.refer(type, url);
        }
        return buildInvokerChain(protocol.refer(type, url), REFERENCE_FILTER_KEY, CommonConstants.CONSUMER);
    }

}

        过滤器的实现原理就是构造处理责任链,核心是当前节点持有下一个节点,当用户实现自定义过滤器类时,会将下一个节点作为构造参数传入,这里需要注意在当前节点的invoker方法中要调用传入的参数对象的Invoker#invoker方法,不然会下一个执行节点将会被忽略执行导致剩下的执行链路失效。从里到外构造匿名类的方式构造Invoker,需要进行倒序遍历才能将最外层的Invoker才可能是最后一个执行,如过滤器列表为<A,B,C>和Invoker,过滤器链路的构建顺序为C->Invoker, B->C->Invoker,A->B->C->Invoker,最终调用的顺序是正序的,所以就会变成A->B->C->Invoker。

private static <T> Invoker<T> buildInvokerChain(final Invoker<T> invoker, 
                                                String key, String group) {
        //保存真正的引用
        Invoker<T> last = invoker;
       //根据url上的group参数获取默认和所有的过滤器对象列表
        List<Filter> filters = ExtensionLoader.getExtensionLoader(Filter.class)
            .getActivateExtension(invoker.getUrl(), key, group);

        if (!filters.isEmpty()) {
            //倒序遍历
            for (int i = filters.size() - 1; i >= 0; i--) {
                final Filter filter = filters.get(i);
                //把last节点变为next节点,并放到Filter链的next中
                final Invoker<T> next = last;
                last = new Invoker<T>() {
                    @Override
                    public Result invoke(Invocation invocation) throws RpcException {
                        Result asyncResult;
                        try {
                            //设置过滤器链路的下一个执行节点,形成执行链路
                            asyncResult = filter.invoke(next, invocation);
                        } catch (Exception e) {
                            //这里会触发监听器的onResponse和onError方法,
                            //对响应结果和结果异常情况做处理
                            ...
                        } finally {
                            ...
                        }
                        //异步结果回调处理
                        return asyncResult.whenCompleteWithContext((r, t) -> {
                            //这里会触发监听器的onResponse和onError方法,
                            //对响应结果和结果异常情况做处理
                            ...
                        });
                    }
                    ...
                };
            }
        }
        return last;
    }

4. 主要过滤器实现原理

4.1 AccessLogFilter实现原理

      AccessLogFilter过滤器作用于服务提供端,实现了日志记录的功能,每请求一次都会将请求信息记录下来。这里使用了一个典型的生产者-消费者模型,即每次调用时将日志信息放入到缓冲区中,然后启动任务线程定期拿到缓冲区的日志信息并写入到文件中。

@Activate(group = PROVIDER, value = ACCESS_LOG_KEY)
public class AccessLogFilter implements Filter {

    public AccessLogFilter() {
        //初始化定时任务线程池,每隔5秒将缓冲区的日志信息写入文件中
        LOG_SCHEDULED.scheduleWithFixedDelay(this::writeLogToFile, 
            LOG_OUTPUT_INTERVAL, LOG_OUTPUT_INTERVAL, TimeUnit.MILLISECONDS);
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
        try {
            //获取url上的accessLog参数,即日志文件
            String accessLogKey = invoker.getUrl().getParameter(ACCESS_LOG_KEY);
            if (ConfigUtils.isNotEmpty(accessLogKey)) {
                AccessLogData logData = buildAccessLogData(invoker, inv);
                log(accessLogKey, logData);
            }
        } catch (Throwable t) {
            ...
        }
        return invoker.invoke(inv);
    }

    private void log(String accessLog, AccessLogData accessLogData) {
        Set<AccessLogData> logSet = LOG_ENTRIES.computeIfAbsent(accessLog, 
                k -> new ConcurrentHashSet<>());
         //如果未超过了缓冲区限制大小,则放入缓冲区
        if (logSet.size() < LOG_MAX_BUFFER) {
            logSet.add(accessLogData);
        } else {
            //超过缓冲区限制大小后,则直接写入文件
            writeLogSetToFile(accessLog, logSet);
            logSet.add(accessLogData);
        }
    }
}

4.2 TimeoutFilter实现原理

        Timeout过滤器作用于服务提供端,会记录每个Invoker的调用时间,如果超过了接口设置的timeout值,则会打印一条打印告警日志。这里主要依赖RpcContext类完成相关功能,这个类对象信息记录了一次调用的相关耗时统计。

@Activate(group = CommonConstants.PROVIDER)
public class TimeoutFilter implements Filter, Filter.Listener {
    ....
    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        Object obj = RpcContext.getContext().get(TIME_COUNTDOWN_KEY);
        if (obj != null) {
            TimeoutCountDown countDown = (TimeoutCountDown) obj;
            if (countDown.isExpired()) {
                ((AppResponse) appResponse).clear();
               ....
            }
        }
    }
}

4.3 TpsLimitFilter实现原理

        TpsLimitFilter主要用于服务提供者的限流,主要依赖TpsLimiter的相关实现完成的。TpsLimiter的限流是基于令牌桶的思路去实现的,即在一个时间段内只分配N个令牌,每个请求会消耗掉一个令牌,消耗完位为止,后面的请求再过来就被拒绝。限流对象的纬度支持分组、版本和接口级别,通过interface+group+version作为唯一标识来判断是否超过最大值。具体逻辑是通过DefaultTPSLimiter#isAllowable方法来判断的,最后会调用StatItem#isAllowable方法,这个方法里面先按照规则获取到服务的唯一标识,每一个服务接口对应StatItem对象,并使用ConcurrentMap来存储。

public class DefaultTPSLimiter implements TPSLimiter {

    private final ConcurrentMap<String, StatItem> stats = 
                new ConcurrentHashMap<String, StatItem>();

    @Override
    public boolean isAllowable(URL url, Invocation invocation) {        
        ...
        if (rate > 0) {
            //通过服务唯一标识获取StatItem对象
            StatItem statItem = stats.get(serviceKey);
            //如果为空则初始化StateItem并放入容器中
            if (statItem == null) {
                stats.putIfAbsent(serviceKey, 
                        new StatItem(serviceKey, rate, interval));
                statItem = stats.get(serviceKey);
            } else {
                //如果不为空则更新容器中对应的StateItem对象
                if (statItem.getRate() != rate 
                    || statItem.getInterval() != interval) {
                    stats.put(serviceKey, 
                        new StatItem(serviceKey, rate, interval));
                    statItem = stats.get(serviceKey);
                }
            }
            //调用isAllowable方法
            return statItem.isAllowable();
        } else {
            //如果没有rate参数则将移除容器中StateItem对象
            StatItem statItem = stats.get(serviceKey);
            if (statItem != null) {
                stats.remove(serviceKey);
            }
        }
        return true;
    }

}


@Activate(group = CommonConstants.PROVIDER, value = TPS_LIMIT_RATE_KEY)
public class TpsLimitFilter implements Filter {

    private final TPSLimiter tpsLimiter = new DefaultTPSLimiter();

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        if (!tpsLimiter.isAllowable(invoker.getUrl(), invocation)) {
           ...  
        }
        return invoker.invoke(invocation);
    }

}

在StatItem#isAllowable方法中实现了令牌桶算法,这里有个最后更新时间的变量,如果当前的请求时间戳大于最后更新时间变量和时间间隔的总和即超过了时间窗口后,则重新生成令牌数并更新最后更新时间,如果总的令牌数小于0即被时间窗口内的其他请求消耗完令牌则返回请求失败,如果没有则令牌数自减,并返回请求成功。

class StatItem {
    public boolean isAllowable() {
        long now = System.currentTimeMillis();
        //当前请求的时间戳大于最后更新时间+时间间隔
        if (now > lastResetTime + interval) {
            //重新生成令牌数
            token = buildLongAdder(rate);
            //更新最后时间
            lastResetTime = now;
        }
        //如果令牌数为0则返回失败
        if (token.sum() < 0) {
            return false;
        }
        //令牌数减1
        token.decrement();
        return true;
    }
}

       令牌桶算法还是比较好理解的,如下图所示,如果tps限制为4时,可以分为三种情况区别理解,初始化时在时间窗口内只有1个请求,则是请求成功的;如果下一个请求时间戳超过时间窗口则更新最后时间并重新生成令牌时间戳,但该时间段内请求数只有3并不会消耗完令牌数,则这3个请求都是成功的;其他情况下,在时间窗口内有多个请求到来,马上消耗完了4个令牌数,后面的请求则拿不到令牌,则请求失败。

4.4 ExcuteLimitFilter实现原理

         ExecuteLimitFilter过滤器主要作用于服务提供端,通过RpcStatus类进行服务器线程并发处理的限制。先使用RpcStatus#beginCount方法判断是否超过提供端的最大并发处理数,然后再进行invoke调用,在得到请求恢复后则调用endCount进行回复结果的统计。

@Activate(group = CommonConstants.PROVIDER, value = EXECUTES_KEY)
public class ExecuteLimitFilter implements Filter, Filter.Listener {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) {

        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        int max = url.getMethodParameter(methodName, EXECUTES_KEY, 0);
        //调用beginCount进行方法级的活跃调用判断
        if (!RpcStatus.beginCount(url, methodName, max)) {
            ...
        }
        ...
        try {
            return invoker.invoke(invocation);
        } catch (Throwable t) {
            ...
        }
    }

    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
        //收到
        RpcStatus.endCount(invoker.getUrl(), invocation.getMethodName(), getElapsed(invocation), true);
    }
}

        在RpcStatus类中有多个统计变量定义,如方法正被调用数/历史调用成功次数/历史调用失败次数/历史调用异常次数的统计变量等等,使用的是线程安全容器,在beginCount方法中,会使用自旋方式对方法调用次数进行自增操作,如果超过最大并发处理个数则退出返回失败,如果比最大并发数小则自增后返回成功,最后在endCound中把方法调用后返回的状态结果进行统计。

public class RpcStatus {
 
    public static boolean beginCount(URL url, String methodName, int max) {
        ...
        //自旋方式
        for (int i; ; ) {
            i = methodStatus.active.get();
            //判断超出最大值则返回失败
            if (i + 1 > max) {
                return false;
            }
            //cas自增,操作成功后则跳出
            if (methodStatus.active.compareAndSet(i, i + 1)) {
                break;
            }
        }
        //进行
        appStatus.active.incrementAndGet();
        return true;
    }

    //调用后进行最后的统计
    public static void endCount(URL url, String methodName, 
                                long elapsed, boolean succeeded) {
        endCount(getStatus(url), elapsed, succeeded);
        endCount(getStatus(url, methodName), elapsed, succeeded);
    }
 
   private static void endCount(RpcStatus status, long elapsed, boolean succeeded) {
        //正在调用统计个数自减
        status.active.decrementAndGet();
        //历史总调用次数自增
        status.total.incrementAndGet();
        status.totalElapsed.addAndGet(elapsed);

        //如果成功,则更新历史成功次数
        if (succeeded) {
            if (status.succeededMaxElapsed.get() < elapsed) {
                status.succeededMaxElapsed.set(elapsed);
            }
        } else {
            //如果失败,则更新历史失败次数
            status.failed.incrementAndGet();
            status.failedElapsed.addAndGet(elapsed);
            if (status.failedMaxElapsed.get() < elapsed) {
                status.failedMaxElapsed.set(elapsed);
            }
        }
    }

5.总结

     本篇章介绍了Dubbo框架中过滤器的主体结构和实现原理,还讲解了部分主要的过滤器的作用以及归属,框架在ProtocolFilterWrapper中为每个Invoker包上多层的过滤器,最终形成一个过滤器链。

参考文献

www.jianshu.com/p/1828a3bf1…

juejin.cn/post/684490… 

blog.csdn.net/hengyunabc/… arthas查看dubbo问题

xilidou.com/2018/11/27/… logAddr原理

cloud.tencent.com/developer/a… Dubbo的TPSLimiter实现原理