高併發系統的限流演算法與實現

全菜工程師小輝發表於2019-07-23

開發高併發系統時有三把利器用來保護系統:快取、降級和限流。

  • 快取:快取的目的是提升系統訪問速度和增大系統處理容量。

  • 降級:降級是當伺服器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放伺服器資源以保證核心任務的正常執行。

  • 限流:限流的目的是通過對併發請求進行限速,或者對一個時間視窗內的請求進行限速來保護系統,一旦達到限制速率則可以進行拒絕服務、排隊或等待、降級等處理。

限流是限制系統的輸入和輸出流量,以達到保護系統的目的,而限流的實現主要是依靠限流演算法,限流演算法主要有4種:

  1. 固定時間視窗演算法(計數器)

  2. 滑動時間視窗演算法

  3. 令牌桶演算法

  4. 漏桶演算法

1. 固定時間視窗演算法

又稱計數器演算法。固定時間視窗演算法就是統計記錄單位時間內進入系統或者某一介面的請求次數,在限定的次數內的請求則正常接收處理,超過次數的請求則拒絕掉或者改為非同步處理等限流措施。

時間視窗長度如果為1分鐘,如圖。

640?wx_fmt=png

此演算法在單機還是分散式環境下實現都非常簡單,使用redis的incr原子自增性即可輕鬆實現。

單機虛擬碼如下。

class CounterDemo {
    public       long timeStamp = getNowTime();
    public       int  reqCount  = 0;
    public final int  limit     = 100; // 時間視窗內最大請求數
    public final long interval  = 1000; // 時間視窗ms

    public boolean grant() {
        long now = getNowTime();
        if (now < timeStamp + interval) {
            // 在時間視窗內
            reqCount++;
            // 判斷當前時間視窗內是否超過最大請求控制數
            return reqCount <= limit;
        } else {
            timeStamp = now;
            // 超時後重置
            reqCount = 1;
            return true;
        }
    }
}

演算法特點

  1. 實現簡單。

  2. 時間視窗固定,每個視窗開始時計數為零,這樣後面的請求不會受到之前的影響,做到了前後請求隔離。

  3. 因為兩個時間視窗之間沒有任何聯絡,所以呼叫者可以在一個時間視窗的結束到下一個時間視窗的開始這個非常短的時間段內發起兩倍於閾值的請求。所以固定時間視窗演算法無法限制視窗間突發流量。

2. 滑動時間視窗演算法

滑動時間視窗演算法其實是固定時間視窗演算法的優化,主要是為了解決固定時間視窗演算法無法限制視窗間突發流量的缺點。
上面的計數器的單位時間是1分鐘,而在使用滑動時間視窗,可以把1分鐘分成6格,每格時間長度是10s,每一格又各自管理一個計數器,單位時間用一個長度為60s的視窗描述。一個請求進入系統,對應的時間格子的計數器便會+1,而每過10s,這個視窗便會向右滑動一格。只要視窗包括的所有格子的計數器總和超過限流上限,便會執行限流措施。

640?wx_fmt=png

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

演算法特點

  1. 因為視窗順延,所以可以抵禦視窗間突發流量(對比固定時間視窗演算法)。

  2. 假如限流10萬次/小時,如果某個呼叫者在前10分鐘呼叫了10萬次那麼他必須再等待1小時才能發起下一次正常請求。所以沒有做到前後請求隔離。

阿里開源的Sentinel,採用的是滑動視窗演算法進行限流,可以閱讀相關程式碼,加深對滑動時間視窗演算法的理解。

3. 漏桶演算法(leaky bucket)

漏桶演算法其實很簡單,可以粗略的認為就是注水漏水過程,往桶中以一定速率流出水,以任意速率流入水,當水超過桶流量則丟棄,因為桶容量是不變的,保證了整體的速率。這個從桶底流出去的水就是系統正常處理的請求,從旁邊流出去的水就是系統拒絕掉的請求。

640?wx_fmt=jpeg

單機虛擬碼如下。

class LeakyDemo {
    public long timeStamp = getNowTime();
    public int capacity; // 桶的容量
    public int rate; // 水漏出的速度
    public int water; // 當前水量(當前累積請求數)

    public boolean grant() {
        long now = getNowTime();
        water = max(0, water - (now - timeStamp) * rate); // 先執行漏水,計算剩餘水量
        timeStamp = now;
        if ((water + 1) < capacity) {
            // 嘗試加水,並且水還未滿
            water += 1;
            return true;
        } else {
            // 水滿,拒絕加水
            return false;
        }
    }
}

演算法特點

  1. 因為流出的速度是一定的,可以抵禦突發流量,做到更加平滑的限流,而且不允許流量突發。

4. 令牌桶演算法(Token Bucket)

令牌桶演算法是比較常見的限流演算法之一,Google開源專案Guava中的RateLimiter使用的就是令牌桶演算法。流程如下:

  1. 所有的請求在處理之前都需要拿到一個可用的令牌才會被處理。

  2. 根據限流大小,設定按照一定的速率往桶裡新增令牌。

  3. 桶設定最大的放置令牌限制,當桶滿時、新新增的令牌就被丟棄或者拒絕。

  4. 請求到達後首先要獲取令牌桶中的令牌,拿著令牌才可以進行其他的業務邏輯,處理完業務邏輯之後,將令牌直接刪除。

640?wx_fmt=png

單機虛擬碼如下,分散式環境可以使用Redisson。

class TokenBucketDemo {
    public long timeStamp = getNowTime();
    public int capacity; // 桶的容量
    public int rate; // 令牌放入速度
    public int tokens; // 當前令牌數量

    public boolean grant() {
        long now = getNowTime();
        // 先新增令牌
        tokens = min(capacity, tokens + (now - timeStamp) * rate);
        timeStamp = now;
        if (tokens < 1) {
            // 若桶中沒有令牌,則拒絕
            return false;
        } else {
            // 還有令牌,領取令牌
            tokens -= 1;
            return true;
        }
    }
}

演算法特點

  1. 可以抵禦突發流量,因為桶內的令牌數不會超過給定的最大值

  2. 可以做到更加平滑的限流,因為令牌是勻速放入的。

  3. 令牌桶演算法允許流量一定程度的突發。(相比漏桶演算法)

在時間點重新整理的臨界點上,只要剩餘token足夠,令牌桶演算法會允許對應數量的請求通過,而後重新整理時間因為token不足,流量也會被限制在外,這樣就比較好的控制了瞬時流量。因此,令牌桶演算法也被廣泛使用。

 

更多內容,歡迎關注微信公眾號:全菜工程師小輝~

640?wx_fmt=png

 

 

相關文章