高可用之限流-05-slide window 滑動視窗

老马啸西风發表於2024-10-14

限流系列

開源元件 rate-limit: 限流

高可用之限流-01-入門介紹

高可用之限流-02-如何設計限流框架

高可用之限流-03-Semaphore 訊號量做限流

高可用之限流-04-fixed window 固定視窗

高可用之限流-05-slide window 滑動視窗

高可用之限流-06-slide window 滑動視窗 sentinel 原始碼

高可用之限流-07-token bucket 令牌桶演算法

高可用之限流 08-leaky bucket漏桶演算法

高可用之限流 09-guava RateLimiter 入門使用簡介 & 原始碼分析

滑動日誌-Sliding Log

滑動日誌演算法,利用記錄下來的使用者的請求時間,請求數,當該使用者的一個新的請求進來時,比較這個使用者在這個視窗內的請求數是否超過了限定值,超過的話就拒絕這個請求。

優點:

  1. 避免了固定視窗演算法在視窗邊界可能出現的兩倍流量問題

  2. 由於是針對每個使用者進行統計的,不會引發驚群效應

缺點:

  1. 需要儲存大量的請求日誌

  2. 每個請求都需要考慮該使用者之前的請求情況,在分散式系統中尤其難做到

時間比例

滑動視窗演算法,結合了固定視窗演算法的低開銷和滑動日誌演算法能夠解決的邊界情況。

  1. 為每個視窗進行請求量的計數

  2. 結合上一個視窗的請求量和這一個視窗已經經過的時間來計算出上限,以此平滑請求尖鋒

舉例來說,限流的上限是每分鐘 10 個請求,視窗大小為 1 分鐘,上一個視窗中總共處理了 6 個請求。

在假設這個新的視窗已經經過了 20 秒,那麼 到目前為止允許的請求上限就是 10 - 6 * (1 - 20 / 60) = 8。

滑動視窗演算法是這些演算法中最實用的演算法:

  1. 有很好的效能

  2. 避免了漏桶演算法帶來的飢餓問題

  3. 避免了固定視窗演算法的請求量突增的問題

ps: 這裡是一種思路,但卻不是正宗的滑動視窗演算法。

滑動視窗

滑動視窗將固定視窗再等分為多個小的視窗。

image

滑動視窗可以透過更細粒度對資料進行統計。

在限流演算法裡:假設我們將1s劃分為4個視窗,則每個視窗對應250ms。

假設惡意使用者還是在上一秒的最後一刻和下一秒的第一刻衝擊服務,按照滑動視窗的原理,此時統計上一秒的最後750毫秒和下一秒的前250毫秒,這種方式能夠判斷出使用者的訪問依舊超過了1s的訪問數量,因此依然會阻攔使用者的訪問。

特點

滑動視窗具有以下特點:

1、每個小視窗的大小可以均等,dubbo的預設負載均衡演算法random就是透過滑動視窗設計的,可以調整每個每個視窗的大小,進行負載。

2、滑動視窗的個數及大小可以根據實際應用進行控制

滑動時間視窗

滑動時間視窗就是把一段時間片分為多個視窗,然後計算對應的時間落在那個視窗上,來對資料統計;

如上圖其實就是即時的滑動時間視窗,隨著時間流失,最開始的視窗將會失效,但是也會生成新的視窗;sentinel的就是透過這個原理來實時的限流資料統計。

關於滑動視窗,這裡介紹還是比較簡單,主要是大致的介紹滑動的原理以及時間視窗的設計;其實關於滑動視窗在我們學習的計算機網路中也涉及到。

java 實現

虛擬碼

全域性陣列 連結串列[]  counterList = new 連結串列[切分的滑動視窗數量];
//有一個定時器,在每一次統計時間段起點需要變化的時候就將索引0位置的元素移除,並在末端追加一個新元素。
int sum = counterList.Sum();
if(sum > 限流閾值) {
    return; //不繼續處理請求。
}

int 當前索引 = 當前時間的秒數 % 切分的滑動視窗數量;
counterList[當前索引]++;
// do something...

java 核心實現

該方法將時間直接切分為10分,然後慢慢處理。

暫時沒有做更加細緻的可配置化,後期考慮新增。

/**
 * 全域性的限制次數
 *
 * 固定時間視窗
 * @author houbinbin
 * Created by bbhou on 2017/9/20.
 * @since 0.0.5
 */
public class LimitFixedWindow extends LimitAdaptor {

    /**
     * 日誌
     * @since 0.0.4
     */
    private static final Log LOG = LogFactory.getLog(LimitFixedWindow.class);

    /**
     * 上下文
     * @since 0.0.4
     */
    private final ILimitContext context;

    /**
     * 計數器
     * @since 0.0.4
     */
    private AtomicInteger counter = new AtomicInteger(0);

    /**
     * 限制狀態的工具
     *
     * 避免不同執行緒的 notify+wait 報錯問題
     *
     * @since 0.0.4
     */
    private CountDownLatch latch = new CountDownLatch(1);

    /**
     * 構造器
     * @param context 上下文
     * @since 0.0.4
     */
    public LimitFixedWindow(ILimitContext context) {
        this.context = context;

        // 定時將 count 清零。
        final long interval = context.interval();
        final TimeUnit timeUnit = context.timeUnit();

        // 任務排程
        ExecutorServiceUtil.singleSchedule(new Runnable() {
            @Override
            public void run() {
                initCounter();
            }
        }, interval, timeUnit);
    }

    @Override
    public synchronized void acquire() {

        // 超過閾值,則進行等待
        if (counter.get() >= this.context.count()) {
            try {
                LOG.debug("[Limit] fixed count need wait for notify.");
                latch.await();
                LOG.debug("[Limit] fixed count need wait end ");
                this.latch = new CountDownLatch(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                LOG.error("[Limit] fixed count is interrupt", e);
            }
        }

        // 結束
        int value = this.counter.incrementAndGet();
        LOG.debug("[Limit] fixed count is " + value);
    }

    /**
     * 初始化計數器
     * @since 0.0.4
     */
    private void initCounter() {
        LOG.debug("[Limit] fixed count init counter start");

        // 通知可以繼續執行(這裡不能無腦 notify)會卡主
        if(this.counter.get() >= this.context.count()) {
            this.counter = new AtomicInteger(0);

            LOG.debug("[Limit] fixed count notify all start");
            latch.countDown();
            LOG.debug("[Limit] fixed count notify all end");
        }  else {
            this.counter = new AtomicInteger(0);
        }
    }

}

基於 queue 的解法

另外一種解法,個人也是比較喜歡的。

直接建立一個佇列,佇列大小等於限制的數量。

直接對比隊首隊尾的時間,從而保證固定當達到指定固定的次數時,時間一定是滿足的。

ps: 這個後續在看看,不一定是滑動視窗的。

public class LimitSlideWindowQueue extends LimitAdaptor {

    private static final Log LOG = LogFactory.getLog(LimitSlideWindowQueue.class);

    /**
     * 用於存放時間的佇列
     * @since 0.0.3
     */
    private final BlockingQueue<Long> timeBlockQueue;

    /**
     * 當前時間
     * @since 0.0.5
     */
    private final ICurrentTime currentTime = Instances.singleton(CurrentTime.class);

    /**
     * 等待間隔時間
     * @since 0.0.5
     */
    private final long intervalInMills;

    /**
     * 構造器
     * @param context 上下文
     * @since 0.0.3
     */
    public LimitSlideWindowQueue(ILimitContext context) {
        this.timeBlockQueue = new ArrayBlockingQueue<>(context.count());
        this.intervalInMills = context.timeUnit().toMillis(context.interval());
    }

    @Override
    public synchronized void acquire() {
        long currentTimeInMills = currentTime.currentTimeInMills();

        //1. 將時間放入佇列中 如果放得下,直接可以執行。反之,需要等待
        //2. 等待完成之後,將第一個元素剔除。將最新的時間加入佇列中。
        boolean offerResult = timeBlockQueue.offer(currentTimeInMills);
        if(!offerResult) {
            //獲取佇列頭的元素
            //1. 取出頭節點,獲取最初的時間
            //2. 將頭結點移除
            long headTimeInMills = timeBlockQueue.poll();

            //當前時間和頭的時間差
            long durationInMills = currentTimeInMills - headTimeInMills;
            if(intervalInMills > durationInMills) {
                //需要沉睡的時間
                long sleepInMills = intervalInMills - durationInMills;
                DateUtil.sleep(sleepInMills);
            }

            currentTimeInMills = currentTime.currentTimeInMills();
            boolean addResult = timeBlockQueue.offer(currentTimeInMills);
            LOG.debug("[Limit] acquire add result: " + addResult);
        }
    }

}

參考資料

限流技術總結

固定視窗和滑動視窗演算法瞭解一下

Sentinel之滑動時間視窗設計(二)

限流滑動視窗

限流演算法之固定視窗與滑動視窗

限流--基於某個滑動時間視窗限流

【限流演算法】java實現滑動時間視窗演算法

談談高併發系統的限流

TCP協議的滑動視窗具體是怎樣控制流量的?

漏銅令牌桶

漏桶演算法&令牌桶演算法理解及常用的演算法

流量控制演算法——漏桶演算法和令牌桶演算法

Token Bucket 令牌桶演算法

華為-令牌桶演算法

簡單分析Guava中RateLimiter中的令牌桶演算法的實現

相關文章