最常用的限流演算法以及如何在http中介軟體中加入流控

小魔童哪吒發表於2021-06-05
[TOC]

何為限流?

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

說白了就是限制請求數量,或者是在某一段時間內限制總的請求數量

例如秒殺網站,限制22點5分 – 22點10分 秒殺999份產品, 限制放行 5w 個請求,若在該段時間內,請求在第5w以後的請求,直接拒之門外, 也就是我們在進入網站的時候顯示,系統繁忙

為什麼要限流?

  • 後臺服務能力有限,需要限流,否則服務會崩掉
  • 可以根據測試效能去評估限流的設定,例如測試最大連線數,qps數量(每秒鐘能處理的最大請求數)
  • 防止爬蟲、惡意攻擊

例如當系統的訪問量突然劇增,大量的請求湧入過來,我們可能會知道會突然有一波高峰,這個時候如果伺服器承受不了壓力的話,就會崩潰,例如如下幾類業務

  • 秒殺業務
  • 各種刷單
  • 微博上的熱搜
  • 因為某些原因使用者猛增,太過熱情
  • 大量使用者(可以是惡意的,也可以是正常的使用者量請求過多)高頻訪問伺服器,伺服器承受能力不足
  • 網頁爬蟲 等等

限流一般是如何去實現的?

我們在某寶或某東的熱門節日上剁手,付款的時候,還急我們懷著焦灼的心等待著排隊的人數一個一個下降的時候嗎?

我們在瘋狂搶購商品,由於點選太快,熱情太高,導致多次彈出系統繁忙,請稍後再試,還記得嗎?

更有甚者,在流量過大的時候,直接提示拒絕訪問的,這些是不是都一一浮現在腦海呢?

根據如上場景,我們的限流思路會是這個樣子的:

  • 拒絕請求
  • 設定排隊,直到單位請求數趨於正常水平

關於拒絕請求就相對簡單粗暴,對於設定排隊就會有多種排隊方式了,我們們繼續聊

除了限流還有什麼方式可以解決或者緩解這種突然大量請求的情況呢?

還有熔斷,降級,都可以有效的解決這樣的問題

那啥是降級?

服務降級,當我們的伺服器壓力劇增時,為了保證核心模組的高可用,這裡指的是我們自身的系統出現了故障而降級有如下2個**常用的解決方式

  • 降低非核心模組的效能
  • 直接關閉不重要的功能,為保障核心模組的功能正常

如圖,某網站,當使用者請求數猛增,伺服器吃不消的時候,就可以選擇把評論功能,修改密碼等功能關閉,確保支付系統,資料系統等核心功能能夠正常執行

哦?那熔斷是啥?

與服務降級還是有區別的,這裡指的是指依賴的外部介面出現故障的情況下,會設定斷絕和外部介面的關係。

伺服器A依賴於伺服器B的對外介面,在某個時刻伺服器B的介面出現異常,響應時間極其的慢,可是此介面會影響到伺服器的整個運作,那麼這個時候,伺服器A就可以在請求伺服器B該介面的時候,預設設定返回錯誤

最常用的限流演算法

我們來分享一個最常用的限流演算法,大致分為以下 4

  • 固定視窗計數器
  • 滑動視窗計數器
  • 漏桶
  • 令牌桶

固定時間視窗控制

最簡單的是 使用計數器來控制,設定固定的時間內,處理固定的請求數

上述圖,固定時間視窗來做限制,1 s只能處理2個請求,紅色請求則會被直接丟棄

  • 固定每1秒限制同時請求數為2
  • 上述紅色部分的請求會被扔掉,扔掉之後 整個服務負荷可能會降低
  • 但是這個會丟掉請求,對於體驗不好

滑動視窗計數器演算法

能夠去平滑一下處理的任務數量。滑動視窗計數器是通過將視窗再細分,並且按照時間滑動,這種演算法避免了固定視窗演算法帶來的雙倍突發請求,但時間區間精度越高,演算法所需的空間容量越大

  • 將時間劃分為多個區間
  • 在每個區間內每有一次請求就講計數器加1維持一個時間視窗,佔據多個區間
  • 每經過一個區間的時間,則拋棄最老的一個區間,並納入最新的一個區間
  • 若當前的視窗內區間的請求總數和超過了限制數量,則本視窗內的請求都被丟棄

漏桶

為了解決上述紅色部分丟掉的問題,引入了 漏桶的方式進行限流,漏桶是有快取的,有請求就會放到快取中

漏桶,聽起來有點像漏斗的樣子,也是一滴一滴的滴下去的

如圖,水滴即為請求的事件,如果漏桶可以快取5000個事件,實際伺服器1s處理1000個事件,那麼在高峰期的時候,響應時間最多等5秒,但是不能一直是高峰期,否則,一直響應時間都是5s,就會是很慢的時間了,這個時間也是很影響體驗的

如果桶滿了,還有請求過來的話,則會被直接丟棄,這種做法,還是丟棄了請求

  • 將每個請求看成 水滴, 放入水滴 進行儲存
  • 漏桶以固定的速率往外漏水,若桶空了則停止漏水。比如說,1s 漏 1000滴水,正如1s 處理1000個請求
  • 如果漏桶慢了,則多餘的水滴也會被直接捨棄

優勢

  • 有一定的快取能力,比上述2種方式會好一些

劣勢

  • 桶滿的時候若有新請求,仍然會丟掉資料
  • 長時間桶滿,則會影響響應速率,這個根據桶的大小來定體驗是否ok

令牌桶

通過動態控制令牌的數量,來更好的服務客戶端的請求事情,令牌的生成數量和生產速率都是可以靈活控制的

如上,令牌桶和漏桶不同的地方在於

  • 令牌桶可以自己控制生成令牌的速率,例如高峰期就可以多生成一些令牌來滿足客戶端的需求
  • 還可以快取資料

若發現一直是出於高峰期,可以考慮擴大令牌桶

優勢
  • 令牌桶可以動態的自己控制生成令牌的速率
  • 還可以快取資料

如何在http middleware加入流控

如何在http 中介軟體中加入流控呢,目的是限流,每一個請求,都需要經過這個中介軟體,才有機會向後走,才有機會被處理

type middleWareHandler struct {
    r *httprouter.Router
    l *ConnLimiter
}

func NewMiddleWareHandler(r *httprouter.Router, cc int) http.Handler {
    m := middleWareHandler{}
    m.r = r
    m.l = NewConnLimiter(cc) // 限制數量
    return m
}

說完令牌桶,我們來說說限流器

限流器

限流器是後臺服務中的非常重要的元件

它可以用做啥呢?

  • 限制請求速率

  • 保護服務

    限流器的實現方法有很多種,基本上都是基於上述的限流演算法來實現的

  • 滑動視窗法

  • Token Bucket(令牌桶)

  • Leaky Bucket(漏桶)

golang標準庫中就自帶了限流演算法的實現,不需要我們自己造輪子

golang.org/x/time/rate,直接用就好了,該限流器是基於Token Bucket(令牌桶)實現的

令牌桶就是我們上面說的桶,裡面裝令牌,系統會以恆定速率向桶中放令牌

桶滿則暫時不放。 使用者請求就要向桶裡面拿令牌

  • 如果有剩餘Token就可以一直取

  • 如果沒有剩餘令牌,則需要等到系統中被放置了Token才可以往下進行

我們來看看限流器咋用

構造一個限流物件

limiter := NewLimiter(5, 1);
  • 第一個引數是r Limit,這是代表每秒可以向令牌桶中產生多少令牌
  • 第二個引數是b int,這是代表令牌桶的容量大小

也就是說,其構造出的限流器是

  • 令牌桶大小為1
  • 以每秒5個令牌的速率向桶中放置令牌

我們當然也可以使用另外的設定方式,包中也有提供

limit := Every(500 * time.Millisecond);
limiter := NewLimiter(limit, 1);

可以用Every方法來指定向Token桶中放置令牌的間隔,上面程式碼就表示每500 ms往桶中放一個令牌

也就說,上述程式碼是1 秒鐘,產生2個令牌

Limiter是支援可以調整速率和桶大小的,我們來看看原始碼

// 改變放入令牌的速率
SetLimit(Limit) 
// 改變令牌桶大小
SetBurst(int) 

我們來寫一個案例看看:

package main

import (
    "context"
    "log"
    "time"

    "golang.org/x/time/rate"
)


func main() {
    l := rate.NewLimiter(1, 2)
    // limit表示每秒產生token數,buret最多存令牌數
    log.Println(l.Limit(), l.Burst())
    for i := 0; i < 50; i++ {
        //這裡是阻塞等待的,一直等到取到一個令牌為止
        log.Println("... ... Wait")
        c, _ := context.WithTimeout(context.Background(), time.Second*2)
        // Wait阻塞等待
        if err := l.Wait(c); err != nil {
            log.Println("limiter wait error : " + err.Error())
        }
        log.Println("Wait  ... ... ")

        // Reserve返回等待時間,再去取令牌
        // 返回需要等待多久才有新的令牌, 這樣就可以等待指定時間執行任務
        r := l.Reserve()
        log.Println("reserve time :", r.Delay())

        //判斷當前是否可以取到令牌
        // Allow判斷當前是否可以取到令牌
        a := l.Allow()
        log.Println("Allow == ", a)
    }
}

看到個數的案例,我們可以看到,包裡面提供給我們使用的消費方法有3種

img

  • Wait

Wait , 等於 WaitN(ctx,1)

若此時桶內令牌陣列不足(小於N),那麼Wait方法將會阻塞一段時間,直至令牌滿足條件,否則就一直阻塞

若滿足條件,則直接返回結果

Wait的context引數。 可以設定超時時間

  • Allow

看看函式原型

func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool

Allow 等於 AllowN(time.Now(),1), 當前取一個令牌,若滿足,則為true,否則 false

AllowN方法 指的是,截止到某一時刻,目前桶中令牌數目是否至少為N個,滿足則返回true,同時從桶中消費N個令牌。 反之返回不消費令牌,返回false

  • Reserve

Reserve , 等於 ReserveN(time.Now(), 1)

ReserveN 當呼叫完成後,無論令牌是否充足,都會返回一個Reservation*物件

我們可以呼叫該物件的Delay()方法,有如下注意:

該方法返回了需要等待的時間

  • 如果等待時間為0秒,則說明不用等待
  • 若大於0秒,則必須等到等待時間之後,才能向後進行

當然,若不想等待,你可以歸還令牌,一個都不能少,呼叫該物件的Cancel() 方法即可

總結

  • 簡單介紹了限流,熔斷,服務降級
  • 形象分享了限流的4種演算法
  • 介紹了http 中介軟體接入流控的簡單寫法
  • 分享了go golang.org/x/time/rate中,限流器的基本使用

好了,本次就到這裡,下一次 網際網路協議介紹和分享

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章