sentinel熱點參數限流-漏桶算法vs令牌桶算法

引子

在上篇文章中我們介紹了sentinel中的滑動窗口算法,發現限流的準確度依賴于劃分的子窗口數量。而在很多情況下,我們的限流更多的是需要限制到參數級別,比如我們需要限制每個用戶調用接口的頻次,這種就需要精確到接口參數級別。而如果用滑動窗口來實現,就需要統計每個窗口內不同參數對應的請求數量,這樣過于復雜,對于這種熱點參數的限流,sentinel根據限流效果的不同,分別使用了漏桶算法和令牌桶算法來進行實現。

漏桶算法原理

主要目的是控制數據注入到網絡的速率,平滑網絡上的突發流量。漏桶算法提供了一種機制,通過它,突發流量可以被整形以便為網絡提供一個穩定的流量。看下漏桶算法的原理圖


漏桶算法

請求先進入到漏桶里,漏桶以一定的速度出水,當水請求過大會直接溢出,可以看出漏桶算法能強行限制數據的傳輸速率。

sentinel中的漏桶算法實現

@Spi(order = -3000)
public class ParamFlowSlot extends AbstractLinkedProcessorSlot<DefaultNode>{
    void checkFlow(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        if (args == null) {
            return;
        }
        if (!ParamFlowRuleManager.hasRules(resourceWrapper.getName())) {
            return;
        }
        //獲取熱點參數配置
        List<ParamFlowRule> rules = ParamFlowRuleManager.getRulesOfResource(resourceWrapper.getName());

        for (ParamFlowRule rule : rules) {
            applyRealParamIdx(rule, args.length);

            // Initialize the parameter metrics.
            //初始化熱點參數
            ParameterMetricStorage.initParamMetricsFor(resourceWrapper, rule);
            //passCheck 校驗請求是否通過
            if (!ParamFlowChecker.passCheck(resourceWrapper, rule, count, args)) {
                String triggeredParam = "";
                if (args.length > rule.getParamIdx()) {
                    Object value = args[rule.getParamIdx()];
                    triggeredParam = String.valueOf(value);
                }
                throw new ParamFlowException(resourceWrapper.getName(), triggeredParam, rule);
            }
        }
    }
}


public final class ParamFlowChecker{
    public static boolean passCheck(ResourceWrapper resourceWrapper, /*@Valid*/ ParamFlowRule rule, /*@Valid*/ int count,
                             Object... args) {
        if (args == null) {
            return true;
        }

        int paramIdx = rule.getParamIdx();
        if (args.length <= paramIdx) {
            return true;
        }

        // Get parameter value.
        Object value = args[paramIdx];

        // Assign value with the result of paramFlowKey method
        if (value instanceof ParamFlowArgument) {
            value = ((ParamFlowArgument) value).paramFlowKey();
        }
        // If value is null, then pass
        if (value == null) {
            return true;
        }
        //集群參數流控
        if (rule.isClusterMode() && rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) {
            return passClusterCheck(resourceWrapper, rule, count, value);
        }
        //單機參數流控
        return passLocalCheck(resourceWrapper, rule, count, value);
    }

    private static boolean passLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int count,
                                          Object value) {
        try {
            //參數類型如果為Collection,循環校驗每個參數
            if (Collection.class.isAssignableFrom(value.getClass())) {
                for (Object param : ((Collection)value)) {
                    if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                        return false;
                    }
                }
            }
            //參數類型如果為Collection,循環校驗每個參數
            else if (value.getClass().isArray()) {
                int length = Array.getLength(value);
                for (int i = 0; i < length; i++) {
                    Object param = Array.get(value, i);
                    if (!passSingleValueCheck(resourceWrapper, rule, count, param)) {
                        return false;
                    }
                }
            }
            //單個元素校驗
            else {
                return passSingleValueCheck(resourceWrapper, rule, count, value);
            }
        } catch (Throwable e) {
            RecordLog.warn("[ParamFlowChecker] Unexpected error", e);
        }

        return true;
    }

重點來了,passThrottleLocalCheck方法就是漏桶算法的具體實現,主要原理就是:根據rule配置的每多少秒可以通過多少請求來計算出一個請求需要多少毫秒costTime,取出上次請求通過的時間+costTime,就能得到我們期望此次請求到來的時間(expectedTime),如果請求到達時間大于期望時間,則放行;如果小于期望時間,說明流量過于密集,需要限制其速率,等待一段時間后(expectedTime-now)再放行。

    /**
     * 勻速排隊限流
     * @param resourceWrapper
     * @param rule
     * @param acquireCount
     * @param value
     * @return
     */
    static boolean passThrottleLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                          Object value) {
        ParameterMetric metric = getParameterMetric(resourceWrapper);
        CacheMap<Object, AtomicLong> timeRecorderMap = metric == null ? null : metric.getRuleTimeCounter(rule);
        if (timeRecorderMap == null) {
            return true;
        }

        // Calculate max token count (threshold)
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        long tokenCount = (long)rule.getCount();
        if (exclusionItems.contains(value)) {
            tokenCount = rule.getParsedHotItems().get(value);
        }

        if (tokenCount == 0) {
            return false;
        }
        //根據rule配置的每多少秒可以通過多少請求來計算出一個請求需要多少毫秒
        long costTime = Math.round(1.0 * 1000 * acquireCount * rule.getDurationInSec() / tokenCount);
        while (true) {
            long currentTime = TimeUtil.currentTimeMillis();
            AtomicLong timeRecorder = timeRecorderMap.putIfAbsent(value, new AtomicLong(currentTime));
            if (timeRecorder == null) {
                return true;
            }
            //AtomicLong timeRecorder = timeRecorderMap.get(value);
            //上次請求通過的時間
            long lastPassTime = timeRecorder.get();
            //期望的請求到達時間
            long expectedTime = lastPassTime + costTime;

            /**
             * 實際請求到達時間如果大于期望的請求到達時間 并且 多出來的這部分時間大于設置的等待時間,則限流
             * 否則請求
             */
            if (expectedTime <= currentTime || expectedTime - currentTime < rule.getMaxQueueingTimeMs()) {
                AtomicLong lastPastTimeRef = timeRecorderMap.get(value);
                if (lastPastTimeRef.compareAndSet(lastPassTime, currentTime)) {
                    long waitTime = expectedTime - currentTime;
                    /**
                     * 等待時間大于0,說明請求在期望時間之前到達,流量太過密集,等待之后放行;小于0,說明請求在期望時間之后到達,直接放行
                     */
                    if (waitTime > 0) {
                        lastPastTimeRef.set(expectedTime);
                        try {
                            TimeUnit.MILLISECONDS.sleep(waitTime);
                        } catch (InterruptedException e) {
                            RecordLog.warn("passThrottleLocalCheck: wait interrupted", e);
                        }
                    }
                    return true;
                } else {
                    Thread.yield();
                }
            } else {
                return false;
            }
        }
    }
}

令牌桶算法原理

對于很多應用場景來說,除了要求能夠限制數據的平均傳輸速率外,還要求允許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更為適合。令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發送到網絡上的數據的數目,并允許突發數據的發送。如圖所示,令牌桶算法的原理是系統會以一個恒定的速度往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當桶里沒有令牌可取時,則拒絕服務。


令牌桶算法

sentinel-令牌桶算法實現

public final class ParamFlowChecker {
    static boolean passDefaultLocalCheck(ResourceWrapper resourceWrapper, ParamFlowRule rule, int acquireCount,
                                         Object value) {
        ParameterMetric metric = getParameterMetric(resourceWrapper);
        //令牌計數器
        CacheMap<Object, AtomicLong> tokenCounters = metric == null ? null : metric.getRuleTokenCounter(rule);
        //時間計數器
        CacheMap<Object, AtomicLong> timeCounters = metric == null ? null : metric.getRuleTimeCounter(rule);

        if (tokenCounters == null || timeCounters == null) {
            return true;
        }

        // Calculate max token count (threshold)
        //針對參數例外項單獨限流
        Set<Object> exclusionItems = rule.getParsedHotItems().keySet();
        long tokenCount = (long)rule.getCount();
        if (exclusionItems.contains(value)) {
            tokenCount = rule.getParsedHotItems().get(value);
        }

        if (tokenCount == 0) {
            return false;
        }
        //最大令牌數 = 設置的閾值 + 額外允許的突發流量數
        long maxCount = tokenCount + rule.getBurstCount();
        if (acquireCount > maxCount) {
            return false;
        }

        while (true) {
            long currentTime = TimeUtil.currentTimeMillis();
            //獲取上一次令牌更新時間
            AtomicLong lastAddTokenTime = timeCounters.putIfAbsent(value, new AtomicLong(currentTime));
            //表示系統啟動后的第一次請求,直接返回校驗通過
            if (lastAddTokenTime == null) {
                // Token never added, just replenish the tokens and consume {@code acquireCount} immediately.
                //計算在時間窗口內有多少次請求可以通過
                tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
                return true;
            }

            // Calculate the time duration since last token was added.
            //計算此次請求距離上一次請求(這里的上一次請求指的是一個統計窗口周期內的第一個請求)過去了多久
            long passTime = currentTime - lastAddTokenTime.get();
            // A simplified token bucket algorithm that will replenish the tokens only when statistic window has passed.
            if (passTime > rule.getDurationInSec() * 1000) {
                //一個統計時間窗口已經過去,會重新計算在此時間窗口內有多少次請求可以通過
                AtomicLong oldQps = tokenCounters.putIfAbsent(value, new AtomicLong(maxCount - acquireCount));
                if (oldQps == null) {
                    // Might not be accurate here.
                    lastAddTokenTime.set(currentTime);
                    return true;
                } else {
                    //獲取請求通過時剩余令牌數
                    long restQps = oldQps.get();
                    /** * 舉例說明,比如1s 允許通過100個請求,當前時間是1900ms,
                     * 上一次請求時間是800ms * 那么totalCount = 1100 * 100 / 1 * 1000 = 110
                     */
                    long toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000);
                    /** 
                     * 最新的qps需要之前剩余的加上間隔時間內新增的,但是不能超過最大值
                     */
                    long newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
                        : (restQps + toAddCount - acquireCount);


                    if (newQps < 0) {
                        return false;
                    }
                    if (oldQps.compareAndSet(restQps, newQps)) {
                        //這里需要更新lastAddTokenTime
                        lastAddTokenTime.set(currentTime);
                        return true;
                    }
                    Thread.yield();
                }
            } else {
                //未超過時間窗口,獲取剩余令牌數
                AtomicLong oldQps = tokenCounters.get(value);
                if (oldQps != null) {
                    long oldQpsValue = oldQps.get();
                    if (oldQpsValue - acquireCount >= 0) {
                        if (oldQps.compareAndSet(oldQpsValue, oldQpsValue - acquireCount)) {
                            //這里不需要更新lastAddTokenTime,因為還在一個統計窗口時間周期內
                            return true;
                        }
                    } else {
                        return false;
                    }
                }
                Thread.yield();
            }
        }
    }
}

代碼較為冗長,我們來看一下整體的流程圖:


令牌桶算法邏輯

令牌桶算法最重要的就是關注令牌生成的部分。而在sentinel的實現中,令牌生成是在 當前請求時間距離上一次請求時間已經大于設置的窗口時間。那么該算法是怎么支持突發流量的呢。
我們以一個具體示例來看:
假定我們設置一分鐘請求不能超過1000次。時間窗口durationInSec = 60s
,tokenCount = 1000;允許突發的流量為burstCount = 2000; maxCount = tokenCount + burstCount = 3000;
假設第1s來了一個請求,因為首次請求,通過,剩余令牌數為 2999。在第10s-20s 流量增大陸續來了2900次請求,因為都在一個窗口時間內且令牌數足夠,所有請求均通過,剩余令牌數為9。隨后教長一段時間沒有請求,一直到第90s請求陸續到達,這時90s距離上一次請求時間20s已經過去了一個時間窗口,我們需要計算在這 70s內我們需要新增多少個令牌數
toAddCount = (passTime * tokenCount) / (rule.getDurationInSec() * 1000)
=70s * 1000ms * 1000 / 60s * 1000ms = 1166.67

newQps = toAddCount + restQps > maxCount ? (maxCount - acquireCount)
: (restQps + toAddCount - acquireCount);
上次剩余的令牌數 restQps = 9
newQps = 1166.67 + 9 -1 = 1174.67 < maxCount
所有在接下來的 60s-120s 時間段內,請求總數不可以超過1174.67。
但是隨著時間的流逝,只要在一段時間內沒有請求過來,令牌又會達到maxCount個,理論上只要3分鐘內沒有請求到達,令牌數最少又會恢復到maxCount=3000個。
我們發現,sentinel應對突發流量,有個緩沖的效果,首次流量突發之后,如果再有流量突發的情況,必須給系統足夠的緩沖時間,否則依然會被限流。

總結:

漏桶算法和令牌桶算法兩者主要區別在于“漏桶算法”能夠強行限制數據的傳輸速率,而“令牌桶算法”在能夠限制數據的平均傳輸速率外,還允許某種程度的突發傳輸。在“令牌桶算法”中,只要令牌桶中存在令牌,那么就允許突發地傳輸數據直到達到用戶配置的門限,所以它適合于具有突發特性的流量

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容