精度不夠,滑動時間來湊「限流算法第二把法器:滑動時間窗口算法」- 第301篇


一、回顧:計算器算法存在問題

對於秒級以上的時間周期來說,會存在一個非常嚴重的問題,那就是臨界問題。

 從上圖中我們可以看到,假設有一個惡意用戶,他在0:59時,瞬間發送了100個請求,並且1:00又瞬間發送了100個請求,那么其實這個用戶在 1秒里面,瞬間發送了200個請求。我們剛才規定的是1分鍾最多100個請求,也就是每秒鍾最多1.7個請求,用戶通過在時間窗口的重置節點處突發請求, 可以瞬間超過我們的速率限制。用戶有可能通過算法的這個漏洞,瞬間壓垮我們的應用。

 

下面就說說如何解決這個由於問題。計算器的問題其實是因為我們統計的精度太低(統計的1秒內的流量情況,只有1個值進行記錄)。那么如何很好地處理這個問題呢?或者說,如何將臨界問題的影響降低呢?我們可以看下面的滑動窗口算法。

 二、滑動窗口算法

滑動窗口,又稱rolling window。為了解決計數器法統計精度太低的問題,引入了滑動窗口算法。下面這張圖,很好 地解釋了滑動窗口算法:

 

 

      是一分鍾。然后我們將時間窗口進行划分,比如圖中,我們就將滑動窗口划成了6格,所以每格代表的是10秒鍾。每過10秒鍾,我們的時間窗口就會往右滑動一格。每一個格子都有自己獨立的計數器counter,比如當一個請求 在0:35秒的時候到達,那么0:30~0:39對應的counter就會加1。

      那么滑動窗口怎么解決剛才的臨界問題的呢?在上圖中,0:59到達的100個請求會落在灰色的格子中,而1:00到達的請求會落在橘×××的格子中。當時間到達1:00時,我們的窗口會往右移動一格,那么此時時間窗口內的總請求數量一共是200個,超過了限定的100個,所以此時能夠檢測出來觸發了限流。

      回顧一下上面的計數器算法,我們可以發現,計數器算法其實就是滑動窗口算法。只是它沒有對時間窗口做進一步地划分,所以只有1格。

      由此可見,當滑動窗口的格子划分的越多,那么滑動窗口的滾動就越平滑,限流的統計就會越精確。

BTW

(1)滑動窗口算法是以當前這個時間點為基准,往前推移1秒進行計算當前1秒內的請求量情況。

(2)滑動窗口限流統計的精准度是由划分的格子多少決定的,這個怎么理解吶,就是把1秒中進行划分成多個時間段,比如2個格子的話,那么就是2段,0-500ms和501-1000ms。那么就會兩個值進行存儲統計請求量,比如數組[0,1] 各存儲一個段的請求值。

(3)計算器算法是滑動窗口算法將時間段划分為1的特殊情況。

  綜上我們對滑動窗口算法下個定義:滑動窗口算法是將時間周期分為N個小周期,分別記錄每個小周期內訪問次數,並且根據時間滑動刪除過期的小周期。

三、滑動窗口算法實現

       我們看一段代碼:

/**
* 滑動時間窗口限流實現
* 假設某個服務最多只能每秒鍾處理100個請求,我們可以設置一個1秒鍾的滑動時間窗口,
* 窗口中有10個格子,每個格子100毫秒,每100毫秒移動一次,每次移動都需要記錄當前服務請求的次數 
*/
public class SlidingTimeWindow {
    // 時間窗口內最大請求數
    public final int limit = 100; 
    // 服務訪問次數
    Long counter = 0L;
    // 使用LinkedList來記錄滑動窗口的10個格子。
    LinkedList<Long> slots = new LinkedList<Long>();
    // 時間划分多少段落
    int split = 10;
    // 是否限流了,true:限流了,false:允許正常訪問。
    boolean isLimit = false;

    private void doCheck() throws InterruptedException {
        while (true) {
            slots.addLast(counter);
            if (slots.size() > split) {
                slots.removeFirst();// 超出了,就把第一個移出。
            }

            // 比較最后一個和第一個,兩者相差100以上就限流
            if ((slots.peekLast() - slots.peekFirst()) > limit) {
                System.out.println("限流了。。");
                // 修改限流標記為true
                isLimit = true;
            } else {
                // 修改限流標記為false
                isLimit = false;
            }
            Thread.sleep(1000/split);
        }
    }

    /**
     * 測試
     * @param args
     * @throws InterruptedException 
     */
    public static void main(String[] args) throws InterruptedException {
        SlidingTimeWindow timeWindow = new SlidingTimeWindow();
        //開啟一個線程判斷當前的限流情況.
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    timeWindow.doCheck();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();


        //模擬請求.
        while(true) {
            //判斷是否被限流了.
            if(!timeWindow.isLimit) {
                timeWindow.counter++;
                //未被限流執行相應的業務方法.
                //  executeBusinessCode();
                //模擬業務執行方法時間.
                Thread.sleep(new Random().nextInt(15));
                System.out.println("業務方法執行完了...");
            }else {
                System.out.println("被限流了,直接返回給用戶");
            }
        }
    }
}

說明:

(1)doCheck方法,就是改變是否限流標志位的方法:這里通過LinkedList(鏈表)來進行模擬,通過LinkedList的大小來控制1秒分隔多少段。peekLast()、slots.peekFirst()就是這個時間窗口的一個流量值。

(2)在main方法中,我們通過開啟一個線程來執行我們的doCheck()方法,由於doCheck()方法是while(true),所以會一直執行,sleep的時間剛好是每個時間間隔值,那么就會不斷刷新LinkedList中的值。

(3)最后while(true)代碼是模擬業務執行調用代碼,也就是controller發起調用的代碼了。

四、小結:

(1)滑動窗口算法是將時間周期分為N個小周期,分別記錄每個小周期內訪問次數,並且根據時間滑動刪除過期的小周期。

(2)計算器算法也屬於滑動窗口算法,是滑動窗口算法的一種情況(划分周期為1)。

(3)滑動窗口算法實現方式:數組(多少周期,數組大小就是多大,將每個時間通過一定的算法落到每個數組下標即可);鏈表(通過鏈表中的大小來判斷划分周期個數,如果超過了,可以使用鏈表的removeFirst()移除第一個,從而實現往后移動窗口)

(4)數組實現的實例:這個可以看Sentinel的源碼,就是定義了一個數組來實現的,時間划分是2。

五、小思考       

當在系統剛剛啟動的時候,系統很多資源都未分配完成,瞬間來了n個請求,如果使用滑動窗口算法的話,那么只有達到了QPS控制閾值,才會觸發限流機制,那么這樣針對系統剛啟動資源未分配的情況,就無法防止系統癱瘓了。

針對上面的這種情況,那么你有什么對應的限流算法可以應對吶?

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM