限流(Rate limit)算法總結

一、前言

保障服務穩定的三大利器:熔斷降級服務限流故障模擬。今天和大家談談限流算法的幾種實現方式,本文所說的限流並非是Nginx層面的限流,而是業務代碼中的邏輯限流。

那麼爲什麼需要限流呢?

按照服務的調用方,可以分爲以下幾種類型服務

1、與用戶打交道的服務

比如web服務、對外API,這種類型的服務有以下幾種可能導致機器被拖垮:

用戶增長過快(這是好事)
因爲某個熱點事件(微博熱搜)
競爭對象爬蟲
惡意的刷單

這些情況都是無法預知的,不知道什麼時候會有10倍甚至20倍的流量打進來,如果真碰上這種情況,擴容是根本來不及的(彈性擴容都是虛談,一秒鐘你給我擴一下試試)。

2、對內的RPC服務

一個服務A的接口可能被BCDE多個服務進行調用,在B服務發生突發流量時,直接把A服務給調用掛了,導致A服務對CDE也無法提供服務。 這種情況時有發生,解決方案有兩種:

每個調用方採用線程池進行資源隔離
使用限流手段對每個調用方進行限流


二、限流算法簡介

限流即流量限制,也叫做流量整形。限流的目的是在遇到流量高峯期或者流量突增(流量尖刺)時,通過對流量速率進行限制,當達到限制速率時,可以拒絕服務(定向到錯誤頁或告知資源沒有了)排隊或等待(比如秒殺、評論、下單)降級(返回兜底數據或默認數據,如商品詳情頁庫存默認有貨)。以把流量控制在系統所能接受的合理範圍之內,不至於讓系統被高流量擊垮。

三、常見限流算法

常用的限流算法大致有三種:漏桶算法、令牌桶算法、計數器算法

1.漏桶算法

漏桶作爲計量工具(The Leaky Bucket Algorithm as a Meter)時,可以用於流量整形(Traffic Shaping)和流量控制(TrafficPolicing),漏桶算法的描述如下:

一個固定容量的漏桶,按照常量固定速率流出水滴
如果桶是空的,則不需流出水滴
可以以任意速率流入水滴到漏桶
如果流入水滴超出了桶的容量,則流入的水滴溢出了(被丟棄),而漏桶容量是不變的

2.令牌桶算法

從某種意義上講,令牌桶算法是對漏桶算法的一種改進,桶算法能夠限制請求調用的速率而令牌桶算法能夠在限制調用的平均速率的同時還允許一定程度的突發調用

在令牌桶算法中,存在一個桶,用來存放固定數量的令牌。算法中存在一種機制,以一定的速率往桶中放令牌。每次請求調用需要先獲取令牌,只有拿到令牌,纔有機會繼續執行,否則選擇選擇等待可用的令牌、或者直接拒絕。

放令牌這個動作是持續不斷的進行,如果桶中令牌數達到上限,就丟棄令牌,所以就存在這種情況,桶中一直有大量的可用令牌,這時進來的請求就可以直接拿到令牌執行,比如設置qps爲100,那麼限流器初始化完成一秒後,桶中就已經有100個令牌了,這時服務還沒完全啓動好,等啓動完成對外提供服務時,該限流器可以抵擋瞬時的100個請求。所以,只有桶中沒有令牌時,請求才會進行等待,最後相當於以一定的速率執行。

令牌桶算法是一個存放固定容量令牌的桶,按照固定速率往桶裏添加令牌。令牌桶算法的描述如下:

假設限制2r/s,則按照500毫秒的固定速率往桶中添加令牌
桶中最多存放b個令牌,當桶滿時,新添加的令牌被丟棄或拒絕
當一個n個字節大小的數據包到達,將從桶中刪除n個令牌,接着數據包被髮送到網絡上
如果桶中的令牌不足n個,則不會刪除令牌,且該數據包將被限流(要麼丟棄,要麼緩衝區等待)

 

令牌桶和漏桶對比:

令牌桶是按照固定速率往桶中添加令牌,請求是否被處理需要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;
漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;
令牌桶限制的是平均流入速率(允許突發請求,只要有令牌就可以處理,支持一次拿3個令牌,4個令牌),並允許一定程度突發流量;
漏桶限制的是常量流出速率(即流出速率是一個固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;
令牌桶允許一定程度的突發,而漏桶主要目的是平滑流入速率;
兩個算法實現可以一樣,但是方向是相反的,對於相同的參數得到的限流效果是一樣的

3.計數器算法

使用計數器來進行限流,主要用來限制總併發數(集羣限流),比如數據庫連接池、線程池、秒殺的併發數;只要全局總請求數或者一定時間段的總請求數設定的閥值則進行限流,是簡單粗暴的總數量限流,而不是平均速率限流。

計數器算法是限流算法裏最簡單也是最容易實現的一種算法。比如我們規定,對於A接口來說,我們1分鐘的訪問次數不能超過100個。那麼我們可以這麼做:在一開 始的時候,我們可以設置一個計數器counter,每當一個請求過來的時候,counter就加1,如果counter的值大於100並且該請求與第一個 請求的間隔時間還在1分鐘之內,那麼說明請求數過多;如果該請求與第一個請求的間隔時間大於1分鐘,且counter的值還在限流範圍內,那麼就重置 counter。

這種算法雖然簡單,但有很致命的問題,用戶通過在時間窗口的重置節點處突發請求,可以瞬間超過我們的速率限制,然後壓垮我們的應用。

4.滑動窗口算法

剛纔的問題其實是因爲我們統計的精度太低。那麼如何很好地處理這個問題呢?

滑動窗口,又稱rolling window。

 

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

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

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

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

四、限流算法代碼

1.漏桶算法實現的僞代碼
 

long timeStamp=getNowTime();
int capacity;  // 桶的容量
int rate ;     // 水漏出的速度
int water;     // 當前水量
 
bool grant() {
  //先執行漏水,因爲rate是固定的,所以可以認爲“時間間隔*rate”即爲漏出的水量
  long now = getNowTime();
  water = max(0, water- (now - timeStamp)*rate);
  timeStamp = now;
 
  if water < capacity { // 水還未滿,加水
    water ++;
    return true;
  } else {
    return false;//水滿,拒絕加水
  }
}

2.令牌桶算法代碼演示

Guava是google提供的java擴展類庫,其中的限流工具類RateLimiter採用的就是令牌桶算法。RateLimiter 從概念上來講,速率限制器會在可配置的速率下分配許可證,如果必要的話,每個acquire() 會阻塞當前線程直到許可證可用後獲取該許可證,一旦獲取到許可證,不需要再釋放許可證。通俗的講RateLimiter會按照一定的頻率往桶裏扔令牌,線程拿到令牌才能執行,比如你希望自己的應用程序QPS不要超過1000,那麼RateLimiter設置1000的速率後,就會每秒往桶裏扔1000個令牌。例如我們需要處理一個任務列表,但我們不希望每秒的任務提交超過兩個,此時可以採用如下方式:

public class RateLimiterDemo {
    private static RateLimiter limiter = RateLimiter.create(5);
 
    public static void exec() {
        limiter.acquire(1);
        try {
            // 處理核心邏輯
            TimeUnit.SECONDS.sleep(1);
            System.out.println("--" + System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

有一點很重要,那就是請求的許可數從來不會影響到請求本身的限制(調用acquire(1) 和調用acquire(1000) 將得到相同的限制效果,如果存在這樣的調用的話),但會影響下一次請求的限制,也就是說,如果一個高開銷的任務抵達一個空閒的RateLimiter,它會被馬上許可,但是下一個請求會經歷額外的限制,從而來償付高開銷任務。注意:RateLimiter 並不提供公平性的保證。 

3.計數器限流示例1

public class CountRateLimiterDemo1 {
    private static AtomicInteger count = new AtomicInteger(0);
    public static void exec() {
        if (count.get() >= 5) {
            System.out.println("請求用戶過多,請稍後在試!"+System.currentTimeMillis()/1000);
        } else {
            count.incrementAndGet();
            try {
                //處理核心邏輯
                TimeUnit.SECONDS.sleep(1);
                System.out.println("--"+System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                count.decrementAndGet();
            }
        }
    }
}

使用AomicInteger來進行統計當前正在併發執行的次數,如果超過域值就簡單粗暴的直接響應給用戶,說明系統繁忙,請稍後再試或其它跟業務相關的信息。 
弊端:使用 AomicInteger 簡單粗暴超過域值就拒絕請求,可能只是瞬時的請求量高,也會拒絕請求。 

4.計數器限流示例2

public class CountRateLimiterDemo2 {
 
    private static Semaphore semphore = new Semaphore(5);
 
    public static void exec() {
        if(semphore.getQueueLength()>100){
            System.out.println("當前等待排隊的任務數大於100,請稍候再試...");
        }
        try {
            semphore.acquire();
            // 處理核心邏輯
            TimeUnit.SECONDS.sleep(1);
            System.out.println("--" + System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semphore.release();
        }
    }
}

 

使用Semaphore信號量來控制併發執行的次數,如果超過域值信號量,則進入阻塞隊列中排隊等待獲取信號量進行執行。如果阻塞隊列中排隊的請求過多超出系統處理能力,則可以在拒絕請求。

相對Atomic優點:如果是瞬時的高併發,可以使請求在阻塞隊列中排隊,而不是馬上拒絕請求,從而達到一個流量削峯的目的。
----------------------

參考:https://blog.csdn.net/m0_37609579/article/details/100190091

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章