高併發系統之限流技術

在開發高併發系統時,有三把利器用來保護系統:緩存、降級和限流。限流是指經過對併發訪問/請求進行限速或者對一個時間內的的請求進行限量來保護系統,一旦達到限制條件則能夠拒絕服務。git



整體來講,實現限流有三種主流方式:計數器,漏桶算法(leaky-bucket)和令牌桶算法(token-bucket)
github


計數器golang

1. 簡單計數器算法

簡單計數器是限流算法中最簡單也是最容易實現的一種算法。好比咱們規定,對於接口A來講,1分鐘的請求次數不能超過1000次。那麼,設置一個請求計數器,將初始值設爲0。當有請求進來時,會把計數器+1,若是在1分鐘間隔之內計數器的值大於1000,說明請求數過多,對後續請求拒絕服務;當1分鐘間隔後,重置計數器。
緩存



實現代碼微信


  • 限流器定義併發

type RequestLimitService struct { Interval time.Duration // 設置時間窗口大小 MaxCount int // 窗口內能支持的最大請求數(閾值) Lock sync.Mutex // 併發控制鎖  ReqCount int   // 當前窗口請求數(計數器)}
  • 實現限流器的兩個核心方法svg

// 判斷當前窗口請求數是否大於最大請求數func (reqLimit *RequestLimitService) IsAvailable() bool { reqLimit.Lock.Lock() defer reqLimit.Lock.Unlock()
return reqLimit.ReqCount < reqLimit.MaxCount}
// 對當前窗口請求數 +1func (reqLimit *RequestLimitService) Increase() { reqLimit.Lock.Lock() defer reqLimit.Lock.Unlock()
reqLimit.ReqCount += 1}
  • 生成限流器
    高併發

func NewRequestLimitService(interval time.Duration, maxCnt int) *RequestLimitService { reqLimit := &RequestLimitService{ Interval: interval, MaxCount: maxCnt, }
  go func() {    ticker := time.NewTicker(interval) // 當達到窗口時間,將計數器清零 for { <-ticker.C reqLimit.Lock.Lock() fmt.Println("Reset Count...") reqLimit.ReqCount = 0 reqLimit.Lock.Unlock() } }()
return reqLimit}


簡單計數器算法實現起來很是方便,可是簡單說明了它能考慮到的問題不夠全面。設想,在上面的例子中,若是某用戶在00:59到01:00之間發送了500個請求,而且在01:00和01:01之間又發送了500個請求,那麼用戶其實在這2秒間就已經發送了1000個請求,他的請求應該被拒絕。可是,簡單計數器的設計則容許了這樣的狀況發生。所以,若是惡意用戶利用經過在臨近時間窗口的重置計數機制而發起大量突發請求,那麼咱們的系統很容易就被弄癱瘓。
flex


2. 滑動窗口
對於剛纔的問題,存在的問題就在於統計的精度過低,所以引入了滑動窗口(rolling window)的概念。



在上圖中,整個紅色的虛線矩形框表示一個時間窗口,該例中,一個時間窗口就是一分鐘。將時間窗口進行劃分,劃成6格,因此每格表明的是10秒鐘。每過10秒鐘,窗口就會往右滑動一格。每個格子都有本身獨立的計數器counter,好比當一個請求在0:35秒的時候到達,0:30~0:39對應的counter就會加1。


那麼滑動窗口怎麼解決剛纔的臨界問題的?咱們能夠根據上圖延用剛纔的例子,0:59至01:00到達的500個請求會落在綠色的格子中,而01:00至01:01到達的請求會落在黃色的格子中。當時間到達1:00時,咱們的窗口會往右移動一格,那麼此時時間窗口內的總請求數量一共是1000個(假定前面的格子沒有發生請求),達到了限定1000的條件,因此此時可以檢測出來觸發了限流。



總結:簡單計數器算法其實就是滑動窗口算法的最簡單實現。只是它沒有對時間窗口作進一步地劃分,只有1格。因而可知,當滑動窗口的格子劃分的越多,滑動窗口的滾動就越平滑,限流的統計就會越精確。




漏桶算法

漏桶算法的思想比較好理解。首先,咱們有一個固定容量的桶,有水流進來,也有水流出去。咱們沒法預計一共有多少水會流進來,也沒法預計水流入的速度。可是這個桶能夠固定水流出的速度。並且,當桶滿了以後,多餘的水將會溢出。



咱們將算法中的水換成實際應用中的請求,能夠看到漏桶算法天生就限制了請求的速度。當使用了漏桶算法,咱們能夠保證接口會以一個常速速率來處理請求。因此漏桶算法不會出現上述的臨界問題。


僞代碼實現


// 定義漏桶結構type leakyBucket struct { timestamp time.Time // 當前注水時間戳 (當前請求時間戳) capacity float64 // 桶的容量(接受緩存的請求總量) rate float64// 水流出的速度(處理請求速度) water float64 // 當前水量(當前累計請求數)}
// 判斷是否加水(是否處理請求)func addWater(bucket leakyBucket) bool { now := time.Now()  // 先執行漏水,計算剩餘水量 leftWater := math.Max(0,bucket.water - now.Sub(bucket.timestamp).Seconds()*bucket.rate) bucket.timestamp = now if leftWater + 1 < bucket.water { // 嘗試加水,此時水桶未滿 bucket.water = leftWater +1 return true }else { // 水滿了,拒絕加水 return false }}


uber 在 Github 上開源了一套用於服務限流的 go 語言庫 ratelimit, 該庫便是基於漏桶算法實現。



令牌桶算法

對於不少應用場景來講,除了要求可以限制數據的平均傳輸速率外,還要求容許某種程度的突發傳輸。這時候漏桶算法就不合適了,令牌桶算法派上了用場。



從圖中咱們能夠看到,令牌桶算法比漏桶算法稍顯複雜。首先,咱們有一個固定容量的桶,桶裏存放着令牌(token)。桶一開始是空的,token以一個固定的速率r往桶裏填充,直到達到桶的容量,多餘的令牌將會被丟棄。每當一個請求過來時,就會嘗試從桶裏移除一個令牌,若是沒有令牌的話,請求沒法經過。


僞代碼實現


// 定義令牌桶結構type tokenBucket struct { timestamp time.Time // 當前時間戳 capacity float64 // 桶的容量(存放令牌的最大量) rate float64// 令牌放入速度 tokens float64 // 當前令牌總量}
// 判斷是否獲取令牌(若能獲取,則處理請求)func getToken(bucket tokenBucket) bool { now := time.Now() // 先添加令牌 leftTokens := math.Max(bucket.capacity, bucket.tokens + now.Sub(bucket.timestamp).Seconds()*bucket.rate) bucket.timestamp = now if leftTokens < 1 { // 若桶中一個令牌都沒有了,則拒絕 return false }else { // 桶中還有令牌,領取令牌 bucket.tokens -= 1 return true }}


其實,Go官方團隊已實現了基於令牌桶算法的限流庫,即 golang.org/x/time/rate


令牌桶和漏桶算法對比


  • 令牌桶是按照固定速率往桶中添加令牌,請求是否被處理須要看桶中令牌是否足夠,當令牌數減爲零時則拒絕新的請求;


  • 漏桶則是按照常量固定速率流出請求,流入請求速率任意,當流入的請求數累積到漏桶容量時,則新流入的請求被拒絕;


  • 令牌桶限制的是平均流入速率(容許突發請求,只要有令牌就能夠處理,支持一次拿多個令牌),並容許必定程度的突發流量;


  • 漏桶限制的是常量流出速率(即流出速率是一個固定常量值,好比都是1的速率流出,而不能一次是1,下次又是2),從而平滑突發流入速率;


  • 令牌桶容許必定程度的突發,而漏桶主要目的是平滑流入速率;


  • 兩個算法實現能夠同樣,可是方向是相反的,對於相同的參數獲得的限流效果是同樣的。




總結,漏桶算法和令牌桶算法的主要區別在於,「漏桶算法」可以強行限制數據的傳輸速率(或請求頻率),而「令牌桶算法」在可以限制數據的平均傳輸速率外,還容許某種程度的突發傳輸。





參考資料

https://en.wikipedia.org/wiki/Leaky_bucket

https://en.wikipedia.org/wiki/Token_bucket

https://github.com/uber-go/ratelimit/

https://godoc.org/golang.org/x/time/rate

https://www.cyhone.com/articles/analisys-of-golang-rate/

https://www.iteye.com/blog/jinnianshilongnian-2305117




本文分享自微信公衆號 - Golang技術分享(gh_1ac13c0742b7)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索