Dubbo 源码解析–限流

导读:本篇文章讲解 Dubbo 源码解析–限流,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

常用的四种限流算法

参考我的另一篇博客:常用的四种限流算法图解

TPSLimiter

TPSLimiter用于服务提供者,提供限流功能
判断在配置的时间间隔内是否允许对服务提供者方法的特定调用,主要由调用间隔和速率

源码分析:
主要涉及三个类:

  • TPSLimiter
  • DefaultTPSLimiter
  • StatItem
/**
 * 限制服务或特定方法的TPS(每秒事务数)。
 * Service或method url可以定义<b>tps</b>或<b>tps.interval</b>来控制。默认使用{@link DefaultTPSLimiter}
 * 作为它的限制检查器。如果提供者服务方法配置为<b>tps</b>(可选的<b>tp.interval</b>),
 * 则调用计数超过配置的<b>tps</b>值(默认值为-1意味着无限),那么调用将得到RpcException。
 * */
@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 {
        // tps默认实现计数器限流算法
        if (!tpsLimiter.isAllowable(invoker.getUrl(), invocation)) {
            throw new RpcException(
                    "Failed to invoke service " +
                            invoker.getInterface().getName() +
                            "." +
                            invocation.getMethodName() +
                            " because exceed max service tps.");
        }

        return invoker.invoke(invocation);
    }

}

关键代码在tpsLimiter.isAllowable(),它调用了DefaultTPSLimiter

/**
 * DefaultTPSLimiter是tps过滤器的默认实现。
 * 它是一个存储tps信息的基于内存的实现。它在内部使用
 *
 * @see org.apache.dubbo.rpc.filter.TpsLimitFilter
 */
public class DefaultTPSLimiter implements TPSLimiter {

    /**
     * 存储每个服务的key和每个服务限制的请求数
     */
    private final ConcurrentMap<String, StatItem> stats = new ConcurrentHashMap<String, StatItem>();

    @Override
    public boolean isAllowable(URL url, Invocation invocation) {
        // 最大请求数
        int rate = url.getParameter(TPS_LIMIT_RATE_KEY, -1);
        // tps间隔是60s
        long interval = url.getParameter(TPS_LIMIT_INTERVAL_KEY, DEFAULT_TPS_LIMIT_INTERVAL);
        // 服务唯一key
        String serviceKey = url.getServiceKey();
        if (rate > 0) {
            // 获取计数器
            StatItem statItem = stats.get(serviceKey);
            if (statItem == null) {
                stats.putIfAbsent(serviceKey, new StatItem(serviceKey, rate, interval));
                statItem = stats.get(serviceKey);
            } else {
                // rate或间隔有修改,则重新构建
                if (statItem.getRate() != rate || statItem.getInterval() != interval) {
                    stats.put(serviceKey, new StatItem(serviceKey, rate, interval));
                    statItem = stats.get(serviceKey);
                }
            }
            // 是否允许
            return statItem.isAllowable();
        }
        // 默认不限速
        else {
            StatItem statItem = stats.get(serviceKey);
            if (statItem != null) {
                stats.remove(serviceKey);
            }
        }

        return true;
    }

}

通过StatItem对象来维护每个serverKey的限流:

/**
 * 计数器限流算法
 * <p>
 * 判断在配置的时间间隔内是否允许对服务提供者方法的特定调用。
 * 作为一种状态,它包含键名(例如方法),最后一次调用时间、间隔和速率计数。
 */
class StatItem {

    private String name;

    private long lastResetTime;

    private long interval;

    private LongAdder token;

    private int rate;

    StatItem(String name, int rate, long interval) {
        this.name = name;
        this.rate = rate;
        this.interval = interval;
        this.lastResetTime = System.currentTimeMillis();
        this.token = buildLongAdder(rate);
    }

    /**
     * 判断是否还有剩余token,并把token数减1
     * @return
     */
    public boolean isAllowable() {
        long now = System.currentTimeMillis();
        // 计数间隔时间到,重置计数器token
        if (now > lastResetTime + interval) {
            // 重新设置token的个数
            token = buildLongAdder(rate);
            // 更新最后重置时间
            lastResetTime = now;
        }

        // 如果token小于0则不允许
        if (token.sum() < 0) {
            return false;
        }
        // 将token数-1
        token.decrement();
        return true;
    }

    public long getInterval() {
        return interval;
    }


    public int getRate() {
        return rate;
    }


    long getLastResetTime() {
        return lastResetTime;
    }

    long getToken() {
        return token.sum();
    }

    @Override
    public String toString() {
        return new StringBuilder(32).append("StatItem ")
                .append("[name=").append(name).append(", ")
                .append("rate = ").append(rate).append(", ")
                .append("interval = ").append(interval).append("]")
                .toString();
    }

    private LongAdder buildLongAdder(int rate) {
        LongAdder adder = new LongAdder();
        adder.add(rate);
        return adder;
    }

}

真正的限流是在StateItem实现,通过调用isAllowable()方法来判断此serverKey是否允许调用

  1. 使用ConcurrentHashMap来存储StatItem,其key为URL中的serviceKey
  2. isAllowable方法从URL中读取tps参数,默认为-1,小于0则从ConcurrentHashMap中移除,大于0则创建或者获取StatItem,调用StatItem的isAllowable(重置或递减token并返回结果)
  3. StatItem定义了LongAdder类型的token,其isAllowable方法会判断是否需要重置token,如果需要则使用buildLongAdder重置token,不需要的话则在token.sum() < 0时返回false,如果大于等于0则递减token

ExecuteLimitFilter(服务端限流)

在服务提供者中通过executes统一配置开启,表示每个服务的每个方法最大可并行执行的请求数
ExecuteLimitFilter通过信号量来实现对服务端的并发数控制

代码实现:


/**
 * 提供者的每个方法每个服务的最大并行执行请求计数。
 * 如果配置的最大执行被设置为10,并且如果调用已经是10的请求,那么它将抛出异常。
 * 它继续同样的行为直到它小于10。
 */
@Activate(group = CommonConstants.PROVIDER, value = EXECUTES_KEY) // 提供者 并且配置 executes
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);
        // note 判断是否可以通过,不通过则抛出RpcException
        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);
            }
        }
    }

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

    @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);
    }

    /**
     * getElapsed⽅法取出 execute_limit_filter_start_time 值,计算执⾏耗时
     *
     * @param invocation
     * @return
     */
    private long getElapsed(Invocation invocation) {
        Object beginTime = invocation.get(EXECUTE_LIMIT_FILTER_START_TIME);
        return beginTime != null ? System.currentTimeMillis() - (Long) beginTime : 0;
    }
}

关键代码在RpcStatus.beginCount(url, methodName, max),进入RpcStatus看看

public class RpcStatus {

    private static final ConcurrentMap<String, RpcStatus> SERVICE_STATISTICS = new ConcurrentHashMap<String,
            RpcStatus>();
	...

    public static void beginCount(URL url, String methodName) {
        beginCount(url, methodName, Integer.MAX_VALUE);
    }

    /**
     * @param url
     */
    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);
        // 最大活跃数==最大数直接短路返回false
        if (methodStatus.active.get() == Integer.MAX_VALUE) {
            return false;
        }
        for (int i; ; ) {
            i = methodStatus.active.get();

            // 活跃数+1>配置的最大活跃数返回false
            if (i == Integer.MAX_VALUE || i + 1 > max) {
                return false;
            }

            if (methodStatus.active.compareAndSet(i, i + 1)) {
                break;
            }
        }

        // 活跃数+1,活跃数在限流和负载均衡均有用
        appStatus.active.incrementAndGet();

        return true;
    }

    /**
     *
     * @param url
     * @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. 首先会先去获得提供者服务每个方法最大可并行执行请求数
  2. 调用beginCount()方法判断是否能够获得一个信号量
  3. 如果返回false,则抛出RpcException异常
  4. 如果返回true,则调用服务
  5. 服务结束会调用endCount()释放信号量

ActiveLimitFilter(客户端限流)

ActiveLimitFilter限制客户端对服务或服务方法的并发客户端调用

配置示例:

<dubbo:reference id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService" "actives"="2"/>

在上面的例子中,最多允许2个并发调用
如果不止配置的(在本例2中)试图调用远程方法,那么剩余的调用将等待配置超时(默认为0秒),直到调用被dubbo杀死。

源码解析:
基本过程与服务端类似

@Activate(group = CONSUMER, value = ACTIVES_KEY) // 消费端 并且配置 actives 才生效
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();
        // 获取方法 actives 属性值 默认是0 ,这actives 就是"每服务消费者每服务每方法最大并发调用数"
        int max = invoker.getUrl().getMethodParameter(methodName, ACTIVES_KEY, 0);
        // 获取对应url 对应method的一个RpcStatus
        final RpcStatus rpcStatus = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName());

        // 开始计数(如果超过最大活跃数或等于上限返回false)
        if (!RpcStatus.beginCount(url, methodName, max)) {
            // 获取超时时间
            long timeout = invoker.getUrl().getMethodParameter(invocation.getMethodName(), TIMEOUT_KEY, 0);
            long start = System.currentTimeMillis();
            long remain = 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(RpcException.LIMIT_EXCEEDED_EXCEPTION,
                                "Waiting concurrent invoke timeout in client-side for service:  " +
                                        invoker.getInterface().getName() + ", method: " + invocation.getMethodName() +
                                        ", elapsed: " + elapsed + ", timeout: " + timeout + ". concurrent invokes: " +
                                        rpcStatus.getActive() + ". max concurrent invoke limit: " + max);
                    }
                }
            }
        }

        // 开始时间统计
        invocation.put(ACTIVELIMIT_FILTER_START_TIME, System.currentTimeMillis());

        // 执行
        return invoker.invoke(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);
    }

    @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;
    }


    private void notifyFinish(final RpcStatus rpcStatus, int max) {
        if (max > 0) {
            synchronized (rpcStatus) {
                // 执行完成唤醒等待线程
                rpcStatus.notifyAll();
            }
        }
    }
}

RpcStatus使用的与服务端是一样的

流程基本与服务端类似,不同的是在客户端有超时判断

  1. 获取每服务消费者中每个方法最大并发调用数
  2. 获取对应url 对应method的一个RpcStatus
  3. 调用beginCount()如果超过最大活跃数或等于上限,则返回false进入超时判断
  4. 如果超过超时时间则抛出RpcException
  5. 如果调用beginCount()没有超过上限,则调用服务
  6. 服务调用完成后会回调onError()onResponse()方法,方法中会唤醒正在等待的服务调用请求线程

总结

限流对象 限流算法 限速应用方
TpsLimitFilter 计数器 服务提供方(服务端)
ExecuteLimitFilter 信号量的方式 服务提供方(服务端)
ActiveLimitFilter 信号量的方式 服务消费方(客户端)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17842.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!