• 【Dubbo】消费端服务并发控制


    一. 概述

    版本:2.7.8

    解决问题

    • 对服务方法或服务接口中所有服务方法并发调用请求进行限制

    调用时机

    • 消费端通过 org.apache.dubbo.rpc.filter.ActiveLimitFilter 进行限制
    • 消费端在使用 DubboInvoker 的 invoke() 方法真正向远端发起RPC请求前,先调用 ActiveLimitFilter 的 invoke() 方法限制

    二. 源码分析

    RpcStatus类

    源码说明:

    1. 该类表示提供端服务接口(包括接口中所有服务方法)、消费端服务接口(包括接口中所有服务方法)的当前调用次数、总数、失败数、调用间隔等状态信息
    2. 代码中有详细注释,重点关注beginCount方法、endCount方法
    3. SERVICE_STATISTICS/METHOD_STATISTICS是静态变量,相当于缓存
    
    package org.apache.dubbo.rpc;
    
    public class RpcStatus {
    
        /**
         * key 为服务接口(url.toIdentityString()),value 为该服务接口中所有方法的 RpcStatus 状态
         * 静态变量:所有服务接口共用(缓存)
         */
        private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<>();
    
        /**
         * key 为服务接口(url.toIdentityString()),value 为 ConcurrentMap<String,RpcStatus> 对象,其中key为具体的服务方法,value为服务方法的 RpcStatus 状态
         * 静态变量:所有服务接口共用(缓存)
         */
        private static final ConcurrentMap<String, ConcurrentMap<String, RpcStatus>> METHOD_STATISTICS = new ConcurrentHashMap<>();
    
        private final ConcurrentMap<String, Object> values = new ConcurrentHashMap<>();
    
        // 服务方法正在执行中的数量
        private final AtomicInteger active = new AtomicInteger();
        
        // 服务方法调用的总数量
        private final AtomicLong total = new AtomicLong();
        
        // 服务方法调用失败的数量
        private final AtomicInteger failed = new AtomicInteger();
        
        private final AtomicLong totalElapsed = new AtomicLong();
        private final AtomicLong failedElapsed = new AtomicLong();
        private final AtomicLong maxElapsed = new AtomicLong();
        private final AtomicLong failedMaxElapsed = new AtomicLong();
        private final AtomicLong succeededMaxElapsed = new AtomicLong();
    
        /**
         * 根据 URL 返回服务接口的 RpcStatus 状态
         */
        public static RpcStatus getStatus(URL url) {
            String uri = url.toIdentityString();
            return SERVICE_STATISTICS.computeIfAbsent(uri, key -> new RpcStatus());
        }
        
        /**
         * 根据 URL 返回接口中服务方法的 RpcStatus 状态
         */
        public static RpcStatus getStatus(URL url, String methodName) {
    
            String uri = url.toIdentityString();
            ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.computeIfAbsent(uri, k -> new ConcurrentHashMap<>());
            return map.computeIfAbsent(methodName, k -> new RpcStatus());
        }
        
        /**
         * 服务方法执行前判断是否满足并发控制要求
         *
         * @param url
         * @param methodName 服务方法
         * @param max        并发控制的数量
         * @return false:并发数已达到,挂起当前线程,等待唤醒
         * @return true: 并发数未达到,可以执行
         */
        public static boolean beginCount(URL url, String methodName, int max) {
    
            max = (max <= 0) ? Integer.MAX_VALUE : max;
            RpcStatus appStatus = getStatus(url);
            RpcStatus methodStatus = getStatus(url, methodName);
    
            if (methodStatus.active.get() == Integer.MAX_VALUE) {
                return false;
            }
            for (int i; ; ) {
    
                // 服务方法正在调用中的数量
                i = methodStatus.active.get();
    
                // 超过配置的并发数,返回false
                if (i + 1 > max) {
                    return false;
                }
    
                // 并发数未达到,返回true
                if (methodStatus.active.compareAndSet(i, i + 1)) {
                    break;
                }
            }
    
            // 接口所有服务方法正在执行中的数量加1
            appStatus.active.incrementAndGet();
            return true;
        }
    
        /**
         * 服务方法执行后减少相关参数值(包括并发控制次数)
         *
         * @param url
         * @param methodName
         * @param elapsed
         * @param succeeded
         */
        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 (status.maxElapsed.get() < elapsed) {
                status.maxElapsed.set(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);
                }
            }
        }
    }
    

    ActiveLimitFilter 过滤器

    源码说明:

    1. 是消费端并且URL有指定key(actives) Filter才生效(@Activate(group = CONSUMER, value = ACTIVES_KEY))
    2. invoke:调用 RpcStatus.beginCount 判断是否满足并发控制条件
    3. RpcStatus.beginCount返回false:则让当前线程挂起,之后会在timeout时间后被唤醒,并抛出RpcException异常。

      也可能在其它消费端正常执行完或异常后调用 notifyFinish() 方法唤醒。

      如果超过timeout时间还没被唤醒,则当前线程会自动被唤醒,然后抛出RpcException异常。
    4. onResponse/onError:方法执行完成或异常调用RpcStatus.endCount减去相应数量
    5. onResponse/onError:会调用notifyFinish通知挂起的消费者
    
    package org.apache.dubbo.rpc.filter;
    
    @Activate(group = CONSUMER, value = ACTIVES_KEY)
    public class ActiveLimitFilter implements Filter, Filter.Listener {
    
        private static final String ACTIVELIMIT_FILTER_START_TIME = "activelimit_filter_start_time";
    
        @Override
        public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
    
            URL url = invoker.getUrl();
            String methodName = invocation.getMethodName();
    
            // 并发调用次数
            int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
            final RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());
    
            // 判断是不是超过并发限制
            // RpcStatus 根据 URL 和调用方法名获取对应方法的RPC状态对象
            // RpcStatus.beginCount 返回false :则让当前线程挂起,之后会在 timeout 时间后被唤醒,并抛出 RpcException 异常。
            // 也可能在其它消费端正常执行完或异常后调用 notifyFinish() 方法唤醒。
            if (!RpcStatus.beginCount(url, methodName, max)) {
    
                long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), TIMEOUT_KEY, 0);
                long start = System.currentTimeMillis();
                long remain = timeout;
    
                // 超过并发限制则阻塞当前线程timeout时间,并重试
                synchronized (rpcStatus) {
    
                    while (!RpcStatus.beginCount(url, methodName, max)) {
    
                        try {
    
                            rpcStatus.wait(remain);
    
                        } catch (InterruptedException e) {
                            // ignore
                        }
                        long elapsed = System.currentTimeMillis() - start;
                        remain = timeout - elapsed;
                        if (remain <= 0) {
                            throw new RpcException();
                        }
                    }
                }
            }
    
            invocation.put(ACTIVELIMIT_FILTER_START_TIME, System.currentTimeMillis());
    
            return invoker.invoke(invocation);
        }
    
         /**
         * 服务方法正常返回
         * @param appResponse
         * @param invoker
         * @param invocation
         */
        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
            String methodName = invocation.getMethodName();
            URL url = invoker.getUrl();
            int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
    
            RpcStatus.endCount(url, methodName, getElapsed(invocation), true);
            notifyFinish(RpcStatus.getStatus(url, methodName), max);
        }
    
        /**
         * 服务方法异常
         * @param t
         * @param invoker
         * @param invocation
         */
        @Override
        public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {
            String methodName = invocation.getMethodName();
            URL url = invoker.getUrl();
            int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
    
            if (t instanceof RpcException) {
                RpcException rpcException = (RpcException) t;
                if (rpcException.isLimitExceed()) {
                    return;
                }
            }
            RpcStatus.endCount(url, methodName, getElapsed(invocation), false);
            notifyFinish(RpcStatus.getStatus(url, methodName), max);
        }
        
        private long getElapsed(Invocation invocation) {
            Object beginTime = invocation.get(ACTIVELIMIT_FILTER_START_TIME);
            return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
        }
    
        /**
         * 服务方法正常返回或异常后:会调用 notifyAll 通知挂起的消费者
         * @param rpcStatus
         * @param max
         */
        private void notifyFinish(final RpcStatus rpcStatus, int max) {
            if (max > 0) {
                synchronized (rpcStatus) {
                    rpcStatus.notifyAll();
                }
            }
        }
    }
    
    

    三. 使用

    public static void main(String[] args) {
    
            // 1.创建服务引用对象实例
            ReferenceConfig<GreetingService> referenceConfig = new ReferenceConfig<>();
    
            // 2.设置应用程序信息
            referenceConfig.setApplication(new ApplicationConfig("dubbo-consumer"));
    
            // 3.设置服务注册中心
            referenceConfig.setRegistry(new RegistryConfig("ZKAddress"));
    
            // 4.设置服务接口和超时时间
            referenceConfig.setInterface(GreetingService.class);
            referenceConfig.setTimeout(5000);
    
            // 5. 服务接口所有方法
            referenceConfig.setActives(10);
    
            // 6. 指定服务方法
    		final List<MethodConfig> methodList = new ArrayList<MethodConfig>();
    
    		MethodConfig methodConfig = new MethodConfig();
    		methodConfig.setActives(10);
    		methodConfig.setName("sayHello");
    		methodList.add(methodConfig);
    		referenceConfig.setMethods(methodList);
    		
            // 7.设置服务分组与版本
            referenceConfig.setVersion("1.0.0");
            referenceConfig.setGroup("dubbo");
    
            // 8.引用服务
            GreetingService greetingService = referenceConfig.get();
            
        }
    
  • 相关阅读:
    一道简单的数组遍历题,加上四个条件后感觉无从下手
    五分钟小知识:如何用算法高效寻找素数?
    如何高效对有序数组/链表去重?
    中级控件——文本输入——文本变化监听器
    中级控件——对话框——提醒对话框AlertDialog
    数据存储——四种存储方式——存储卡的文件操作——私有存储空间与公共存储空间
    中级控件——文本输入——焦点变更监听器
    中级控件——对话框——日期对话框DatePickerDialog
    中级控件——对话框——时间对话框TimePickerDialog
    数据存储——四种存储方式——共享参数SharedPreferences——保存路径是:/data/data/应用包名/shared_prefs/文件名.xml——实现密码登录功能——此章节是前面的内容整合
  • 原文地址:https://www.cnblogs.com/gossip/p/14367693.html
Copyright © 2020-2023  润新知