源码分析Dubbo服务提供者、服务消费者并发度控制机制

   本文将详细分析< dubbo:service executes=""/>与< dubbo:reference actives = “”/>的实现机制,深入探讨Dubbo自身的保护机制。
1、源码分析ExecuteLimitFilter
@Activate(group = Constants.PROVIDER, value = Constants.EXECUTES_KEY )

  • 过滤器作用
       服务调用方并发度控制。
  • 使用场景
       对Dubbo服务提供者实现的一种保护机制,控制每个服务的最大并发度。
  • 阻断条件
       当服务调用超过允许的并发度后,直接抛出RpcException异常。
       接下来源码分析ExecuteLimitFilter的实现细节。
       ExecuteLimitFilter#invoke
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        Semaphore executesLimit = null;
        boolean acquireResult = false;
        int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);      // @1
        if (max > 0) {
            RpcStatus count = RpcStatus.getStatus(url, invocation.getMethodName());             // @2
            executesLimit = count.getSemaphore(max);                                                              // @3
            if(executesLimit != null && !(acquireResult = executesLimit.tryAcquire())) {              // @4
                throw new RpcException("Failed to invoke method " + invocation.getMethodName() + " in provider " + url + ", cause: The service using threads 
                      greater than <dubbo:service executes=\"" + max + "\" /> limited.");
            }
        }
        boolean isSuccess = true;
        try {
            Result result = invoker.invoke(invocation);                 // @5
            return result;
        } catch (Throwable t) {
            isSuccess = false;
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
            }
        } finally {
            if(acquireResult) {                                   // @6
                executesLimit.release();
            }
        }
    }

   代码@1:从服务提供者列表中获取参数executes的值,如果该值小于等于0,表示不启用并发度控制,直接沿着调用链进行调用。
   代码@2:根据服务提供者url和服务调用方法名,获取RpcStatus。

public static RpcStatus getStatus(URL url, String methodName) {
        String uri = url.toIdentityString();      
        ConcurrentMap<String, RpcStatus> map = METHOD_STATISTICS.get(uri);         
        if (map == null) {
            METHOD_STATISTICS.putIfAbsent(uri, new ConcurrentHashMap<String, RpcStatus>());    
            map = METHOD_STATISTICS.get(uri);
        }
        RpcStatus status = map.get(methodName);          /
        if (status == null) {
            map.putIfAbsent(methodName, new RpcStatus());
            status = map.get(methodName);
        }
        return status;
    }

   这里是并发容器ConcurrentHashMap的经典使用,从 这里可以看出ConcurrentMap< String, ConcurrentMap< String, RpcStatus>> METHOD_STATISTICS的存储结构为 { 服务提供者URL唯一字符串:{方法名:RpcStatus} }。
   代码@3:根据服务提供者配置的最大并发度,创建该服务该方法对应的信号量对象。

public Semaphore getSemaphore(int maxThreadNum) {
        if(maxThreadNum <= 0) {
            return null;
        }
        if (executesLimit == null || executesPermits != maxThreadNum) {
            synchronized (this) {
                if (executesLimit == null || executesPermits != maxThreadNum) {
                    executesLimit = new Semaphore(maxThreadNum);
                    executesPermits = maxThreadNum;
                }
            }
        }
        return executesLimit;
    }

   使用了双重检测来创建executesLimit 信号量。
   代码@4:如果获取不到锁,并不会阻塞等待,而是直接抛出RpcException,服务端的策略是快速抛出异常,供服务调用方(消费者)根据集群策略进行执行,例如重试其他服务提供者。
   代码@5:执行真实的服务调用。
   代码@6:如果成功申请到信号量,在服务调用结束后,释放信号量。
   总结:< dubbo:service executes=""/>的含义是,针对每个服务每个方法的最大并发度。如果超过该值,则直接抛出RpcException。

   2、源码分析ActiveLimitFilter
   @Activate(group = Constants.CONSUMER, value = Constants.ACTIVES_KEY )

  • 过滤器作用
       消费端调用服务的并发控制。
  • 使用场景
       控制同一个消费端对服务端某一服务的并发调用度,通常该值应该小于< dubbo:service executes=""/>
  • 阻断条件
       非阻断,但如果超过允许的并发度会阻塞,超过超时时间后将不再调用服务,而是直接抛出超时。

   源码分析ActiveLimitFilter的实现原理:
   ActiveLimitFilter#invoke

public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();
        int max = invoker.getUrl().getMethodParameter(methodName, Constants.ACTIVES_KEY, 0);    // @1
        RpcStatus count = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());           // @2
        if (max > 0) {                                          
            long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), Constants.TIMEOUT_KEY, 0);   // @3
            long start = System.currentTimeMillis();
            long remain = timeout;
            int active = count.getActive();                                                                                                                                          // @4
            if (active >= max) {                                                                                                                                                          // @5
                synchronized (count) {                                                                                                                                                                      
                    while ((active = count.getActive()) >= max) {                                                                                                     
                        try {
                            count.wait(remain);                                                                                                                                      
                        } catch (InterruptedException e) {
                        }
                        long elapsed = System.currentTimeMillis() - start;                               
                        remain = timeout - elapsed;
                        if (remain <= 0) {                                                                                                                                             // @6
                            throw new RpcException("Waiting concurrent invoke timeout in client-side for service:  "
                                    + invoker.getInterface().getName() + ", method: "
                                    + invocation.getMethodName() + ", elapsed: " + elapsed
                                    + ", timeout: " + timeout + ". concurrent invokes: " + active
                                    + ". max concurrent invoke limit: " + max);
                        }
                    }
                }
            }
        }
        try {
            long begin = System.currentTimeMillis();
            RpcStatus.beginCount(url, methodName);        // @7
            try {
                Result result = invoker.invoke(invocation);     // @8
                RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, true);    // @9
                return result;
            } catch (RuntimeException t) {
                RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, false);
                throw t;
            }
        } finally {
            if (max > 0) {
                synchronized (count) {
                    count.notify();     // @10
                }
            }
        }
    }

   代码@1:从Invoker中获取消息端URL中的配置的actives参数,为什么从Invoker中获取的Url是消费端的Url呢?这是因为在消费端根据服务提供者URL创建调用Invoker时,会用服务提供者URL,然后合并消费端的配置属性,其优先级 -D > 消费端 > 服务端。其代码位于:、
   RegistryDirectory#toInvokers
   URL url = mergeUrl(providerUrl);
   代码@2:根据服务提供者URL和调用服务提供者方法,获取RpcStatus。
   代码@3:获取接口调用的超时时间,默认为1s。
   代码@4:获取当前消费者,针对特定服务,特定方法的并发调用度,active值。
   代码@5:如果当前的并发 调用大于等于允许的最大值,则针对该RpcStatus申请锁,并调用其wait(timeout)进行等待,也就是在接口调用超时时间内,还是未被唤醒,则直接抛出超时异常。
   代码@6:判断被唤醒的原因是因为等待超时,还是由于调用结束,释放了"名额“,如果是超时唤醒,则直接抛出异常。
   代码@7:在一次服务调用前,先将 服务名+方法名对应的RpcStatus的active加一。
   代码@8:执行RPC服务调用。
   代码@9:记录成功调用或失败调用,并将active减一。
   代码@10:最终成功执行,如果开启了actives机制(dubbo:referecnce actives="")时,唤醒等待者。
   总结:< dubbo:reference actives=""/> 是控制消费端对 单个服务提供者单个服务允许调用的最大并发度。该值的取值不应该大于< dubbo:service executes=""/>的值,并且如果消费者机器的配置,如果性能不尽相同,不建议对该值进行设置。


欢迎加笔者微信号(dingwpmz),加群探讨,笔者优质专栏目录:
1、源码分析RocketMQ专栏(40篇+)
2、源码分析Sentinel专栏(12篇+)
3、源码分析Dubbo专栏(28篇+)
4、源码分析Mybatis专栏
5、源码分析Netty专栏(18篇+)
6、源码分析JUC专栏
7、源码分析Elasticjob专栏
8、Elasticsearch专栏(20篇+)
9、源码分析MyCat专栏

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

中间件兴趣圈

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值