Eureka限流源码解析

622 阅读1分钟

令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务,令牌桶算法的限流的特点:让流量平稳,而不是瞬间流量,1000 QPS 相对平均的分摊在这一秒内,而不是第 1 ms 999 请求,后面 999 ms 0 请求。

Eureka的限流策略入口是在Eureka-Server的web.xml里,是个过滤器,使用的是RateLimiting,接下来来看看RateLimiting是怎么实现的。

<filter>
    <filter-name>rateLimitingFilter</filter-name>
    <filter-class>com.netflix.eureka.RateLimitingFilter</filter-class>
  </filter>

RateLimiter 目前支持分钟级和秒级两种速率限制。

/**
     * 支持分钟级和秒级的限流
     */
    public RateLimiter(TimeUnit averageRateUnit) {
        switch (averageRateUnit) {
            case SECONDS:
                rateToMsConversion = 1000;
                break;
            case MINUTES:
                rateToMsConversion = 60 * 1000;
                break;
            default:
                throw new IllegalArgumentException("TimeUnit of " + averageRateUnit + " is not supported");
        }
    }

调用 #acquire(...) 方法,获取令牌,并返回是否获取成功。

/**
     * 获取令牌
     *
     * @param burstSize   令牌桶上限
     * @param averageRate 令牌再装平均速率
     * @return
     */
    public boolean acquire(int burstSize, long averageRate) {
        return acquire(burstSize, averageRate, System.currentTimeMillis());
    }

    public boolean acquire(int burstSize, long averageRate, long currentTimeMillis) {
        if (burstSize <= 0 || averageRate <= 0) { // Instead of throwing exception, we just let all the traffic go
            return true;
        }

        //填充令牌
        refillToken(burstSize, averageRate, currentTimeMillis);

        //消费令牌
        return consumeToken(burstSize);
    }

burstSize 参数 :令牌桶上限。

averageRate 参数 :令牌填充平均速率。

我们举个 例子来理解这两个参数 + 构造方法里的一个参数:

averageRateUnit = SECONDS

averageRate = 2000

burstSize = 10

每秒可获取 2000 个令牌。例如,每秒允许请求 2000 次。

每毫秒可填充 2000 / 1000 = 2 个消耗的令牌。

每毫秒可获取 10 个令牌。例如,每毫秒允许请求上限为 10 次,并且请求消耗掉的令牌,需要逐步填充。这里要注意下,虽然每毫秒允许请求上限为 10 次,这是在没有任何令牌被消耗的情况下,实际每秒允许请求依然是 2000 次。

以下是填充令牌和消费令牌源码。

 /**
     * 填充令牌
     *
     * @param burstSize         令牌桶上限
     * @param averageRate       令牌填充平均速率
     * @param currentTimeMillis
     */
    private void refillToken(int burstSize, long averageRate, long currentTimeMillis) {
        //获取最后填充令牌的时间
        long refillTime = lastRefillTime.get();

        //获取过去多少毫秒
        long timeDelta = currentTimeMillis - refillTime;

        //可填充最大令牌数
        long newTokens = timeDelta * averageRate / rateToMsConversion;

        if (newTokens > 0) {
            //计算新的令牌填充时间
            long newRefillTime = refillTime == 0
                    ? currentTimeMillis
                    : refillTime + newTokens * rateToMsConversion / averageRate;
            if (lastRefillTime.compareAndSet(refillTime, newRefillTime)) {
                while (true) {
                    //计算 填充令牌后的已消耗令牌数量
                    int currentLevel = consumedTokens.get();
                    int adjustedLevel = Math.min(currentLevel, burstSize); // In case burstSize decreased
                    int newLevel = (int) Math.max(0, adjustedLevel - newTokens);
                    // CAS 避免和正在消费令牌的线程冲突
                    if (consumedTokens.compareAndSet(currentLevel, newLevel)) {
                        return;
                    }
                }
            }
        }
    }

    /**
     * 消费令牌
     *
     * @param burstSize 令牌桶上限
     * @return
     */
    private boolean consumeToken(int burstSize) {
        // 死循环,直到没有令牌,或者获取令牌成功
        while (true) {
            int currentLevel = consumedTokens.get();
            // 没有令牌
            if (currentLevel >= burstSize) {
                return false;
            }
            // CAS 避免和正在消费令牌或者填充令牌的线程冲突
            if (consumedTokens.compareAndSet(currentLevel, currentLevel + 1)) {
                return true;
            }
        }
    }