【Dubbo】服務提供方併發控制

一. 概述

版本:2.7.8

解決問題

  • 對服務方法或服務接口中所有服務方法併發調用請求數量進行限制

調用時機

  • 服務提供方通過 org.apache.dubbo.rpc.filter.ExecuteLimitFilter 進行限制
  • 服務提供方的 AbstractProxyInvoker 的 invoke () 方法在真正執行服務調用前,請求會先經過 ExecuteLimitFilter 的 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);
            }
        }
    }
}

ExecuteLimitFilter 過濾器

源碼說明:

  1. 是服務提供端並且URL有指定key(executes) Filter才生效(@Activate(group = CommonConstants.PROVIDER, value = EXECUTES_KEY))
  2. invoke:調用 RpcStatus.beginCount 判斷是否滿足併發控制條件
  3. RpcStatus.beginCount返回false:直接拋異常。
  4. onResponse/onError:方法執行完成或發生異常則調用RpcStatus.endCount減去相應數量

package org.apache.dubbo.rpc.filter;

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

    private static final String EXECUTE_LIMIT_FILTER_START_TIME = "execute_limit_filter_start_time";

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

        URL url = invoker.getUrl();
        String methodName = invocation.getMethodName();

        // 併發調用次數
        int max = url.getMethodParameter(methodName, EXECUTES_KEY, 0);

        // 判斷是不是超過併發限制
        // RpcStatus 根據URL和調用方法名獲取對應方法的RPC狀態對象
        // RpcStatus.beginCount返回false:直接拋異常。
        if (!RpcStatus.beginCount(url, methodName, max)) {
            throw new RpcException(RpcException.LIMIT_EXCEEDED_EXCEPTION,
                    "Failed to invoke method " + invocation.getMethodName() + " in provider " +
                            url + ", cause: The service using threads greater than <dubbo:service executes=\"" + max +
                            "\" /> limited.");
        }

        invocation.put(EXECUTE_LIMIT_FILTER_START_TIME, System.currentTimeMillis());

        try {
            return invoker.invoke(invocation);
        } catch (Throwable t) {
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
            }
        }
    }

    /**
     * 服務方法正常返回
     *
     * @param appResponse
     * @param invoker
     * @param invocation
     */
    @Override
    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {

        RpcStatus.endCount(invoker.getUrl(), invocation.getMethodName(), getElapsed(invocation), true);
    }

    /**
     * 服務方法異常
     *
     * @param t
     * @param invoker
     * @param invocation
     */
    @Override
    public void onError(Throwable t, Invoker<?> invoker, Invocation invocation) {

        if (t instanceof RpcException) {
            RpcException rpcException = (RpcException) t;
            if (rpcException.isLimitExceed()) {
                return;
            }
        }
        RpcStatus.endCount(invoker.getUrl(), invocation.getMethodName(), getElapsed(invocation), false);
    }

    private long getElapsed(Invocation invocation) {

        Object beginTime = invocation.get(EXECUTE_LIMIT_FILTER_START_TIME);
        return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
    }
}


三. 使用

    
    public static void main(String[] args) throws IOException {

        ServiceConfig<GreetingService> serviceConfig = new ServiceConfig<>();
        serviceConfig.setApplication(new ApplicationConfig("dubbo-provider");
        serviceConfig.setRegistry(new RegistryConfig("ZKAddress"));
        serviceConfig.setInterface(GreetingService.class);
        serviceConfig.setRef(new GreetingServiceImpl());

        serviceConfig.setVersion("1.0.0");
        serviceConfig.setGroup("dubbo");

        // 服務接口所有方法
        serviceConfig.setExecutes(10);

        // 指定服務方法
        final List<MethodConfig> methodList = new ArrayList<MethodConfig>();

        MethodConfig methodConfig = new MethodConfig();
        methodConfig.setExecutes(10);
        methodConfig.setName("sayHello");
        methodList.add(methodConfig);
        serviceConfig.setMethods(methodList);

        serviceConfig.export();

        System.out.println("server is started");
        System.in.read();
    }
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章