限流算法在Sentinel中的实践(最全最详细面试不用愁)

120 阅读5分钟

面试老被问限流算法在Sentinel中的应用和实现细节,似懂非懂回答不上来的痛苦,家人们谁懂啊!之前笔者浅谈了限流算法,今天,笔者就从Sentinel源码入手看看这神秘的限流算法是如何实现的

使用过Sentinel的同学可能都知道,Sentinel有三种流控效果:直接拒绝、Warm Up和排队等待

image-20241127152554066.png

这三种流控效果在源码中的实现如下,相应的有四个实现类,接下来就一一看看它们是如何实现的

image-20241127152908076.png

直接拒绝

直接拒绝的实现类DefaultController,核心代码如下

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // 以qps为例,获取当前的秒级qps
    // Sentinel是如何统计当前的秒级qps的?当然是滑动时间窗限流算法
    int curCount = avgUsedTokens(node);
    // 看有没有超过设定的限流阈值
    if (curCount + acquireCount > count) {
        // 这里有种特殊情况,允许占用下一秒的请求资源
        // 也就是说当前秒的请求数已用完,可以将当前请求阻塞到下一秒,以减少采用拒绝访问这种不太友好的处理方式
        if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
            long currentTime;
            long waitInMs;
            currentTime = TimeUtil.currentTimeMillis();
            waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
            if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                node.addOccupiedPass(acquireCount);
                sleep(waitInMs);
​
                // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                throw new PriorityWaitException(waitInMs);
            }
        }
        // 超过限流阈值则不放行
        return false;
    }
    // 未超过限流阈值则放行
    return true;
}

Warm Up

这里要先提提令牌桶算法借鉴了Google Guava的轻量级实现,里面蕴含了一个预热模型,笔者本着拿来主义从这篇博客中拿了个图供大家参考,如果侵权了请联系笔者删除,带着这个模型往下看,要来然会对下面的计算逻辑云里雾里,当然也可以忽略,只考虑算法的大致运行情况

06bd3dfc09e6b899046d153c20bca956.png

记住图中第8条,这应该是预热模型设定的条件,关系不是很大挖个坑后续再查阅下资料

Warm Up的实现类是WarmUpController,也就是令牌桶限流算法的实现

其构造方法如下

private void construct(double count, int warmUpPeriodInSec, int coldFactor) {
​
    if (coldFactor <= 1) {
        throw new IllegalArgumentException("Cold factor should be larger than 1");
    }
​
    // 这里的count和stableInterval是有关系的,count*stableInterval=1s
    this.count = count;
​
    this.coldFactor = coldFactor;
​
    // thresholdPermits = 0.5 * warmupPeriod / stableInterval.
    // warningToken = 100;
    // 利用1的面积来算
    warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
    // / maxPermits = thresholdPermits + 2 * warmupPeriod /
    // (stableInterval + coldInterval)
    // maxToken = 200
    // 利用2的面积来算
    maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
​
    // slope
    // slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
    slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
​
}

其核心代码如下

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
​
    // 获取当前秒的qps
    long passQps = (long) node.passQps();
​
    // 获取前一秒的qps
    long previousQps = (long) node.previousPassQps();
​
    // 生成令牌,这里实现也是很轻量级的,下面会详细看看它的实现
    syncToken(previousQps);
​
    // 要不要放行
    long restToken = storedTokens.get();
    if (restToken >= warningToken) {
        long aboveToken = restToken - warningToken;
        // current interval = restToken*slope+1/count
        double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
        if (passQps + acquireCount <= warningQps) {
            // 令牌数超出预警值,当时qps还未超出警告qps,放行
            return true;
        }
    } else {
        if (passQps + acquireCount <= count) {
            // 令牌数未超出预警值,当时qps小于阈值,放行
            return true;
        }
    }
​
    // 除了以上两种情况外不放行
    return false;
}

生成令牌的详细实现

protected void syncToken(long passQps) {
    // 计算当前的时间
    long currentTime = TimeUtil.currentTimeMillis();
    currentTime = currentTime - currentTime % 1000;
    // 上次生成令牌的时间
    long oldLastFillTime = lastFilledTime.get();
    if (currentTime <= oldLastFillTime) {
        return;
    }
​
    // 桶中现存的令牌数
    long oldValue = storedTokens.get();
    // 根据当前时间和上次生成令牌的时间计算新的令牌数
    long newValue = coolDownTokens(currentTime, passQps);
    // 更改令牌桶的令牌数
    if (storedTokens.compareAndSet(oldValue, newValue)) {
        // 扣减上次时间窗的所需的令牌数
        long currentValue = storedTokens.addAndGet(-passQps);
        if (currentValue < 0) {
            storedTokens.set(0L);
        }
        lastFilledTime.set(currentTime);
    }
​
}
private long coolDownTokens(long currentTime, long passQps) {
    // 当前令牌数
    long oldValue = storedTokens.get();
    long newValue = oldValue;
​
    // 添加令牌的前提条件
    if (oldValue < warningToken) {
        // 当令牌数低于警戒线的时候
        newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
    } else if (oldValue > warningToken) {
        if (passQps < (int)count / coldFactor) {
            // 当令牌数不低于警戒线并且qps小于最大令牌数时每秒令牌生成数的时候
            newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
        }
    }
    return Math.min(newValue, maxToken);
}

可以想象当系统平时以低流量运行时,令牌数会填满令牌桶,当有突发流量进来,qps会慢慢增长,直到令牌生成速率达到最大

排队等待

排队等待的实现类是RateLimiterController,它实际上就是漏斗限流算法的实现,其核心代码如下

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    // Pass when acquire count is less or equal than 0.
    if (acquireCount <= 0) {
        return true;
    }
    // Reject when count is less or equal than 0.
    // Otherwise,the costTime will be max of long and waitTime will overflow in some cases.
    if (count <= 0) {
        return false;
    }
​
    long currentTime = TimeUtil.currentTimeMillis();
    // Calculate the interval between every two requests.
    long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
​
    // Expected pass time of this request.
    long expectedTime = costTime + latestPassedTime.get();
​
    if (expectedTime <= currentTime) {
        // Contention may exist here, but it's okay.
        latestPassedTime.set(currentTime);
        return true;
    } else {
        // Calculate the time to wait.
        long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
        if (waitTime > maxQueueingTimeMs) {
            return false;
        } else {
            long oldTime = latestPassedTime.addAndGet(costTime);
            try {
                waitTime = oldTime - TimeUtil.currentTimeMillis();
                if (waitTime > maxQueueingTimeMs) {
                    latestPassedTime.addAndGet(-costTime);
                    return false;
                }
                // in race condition waitTime may <= 0
                if (waitTime > 0) {
                    Thread.sleep(waitTime);
                }
                return true;
            } catch (InterruptedException e) {
            }
        }
    }
    return false;
}

这个代码逻辑比较简单,就不详细分析了,大致就是根据固定速率算出一个请求所需的时间,然后按照先来后到进行排序依次等待请求

还有一个就不分析了,就是Warm Up和排队等待的结合体,有兴趣的宝子们可以自行研究


如果写的有误欢迎批评指正,也欢迎和我沟通,一块学习进步

创作不易,感兴趣的兄弟集美可以点赞关注给兄弟点支持

如需转发,请注明出处

笔者也分享在这里啦