常見限流方案設計與實現
- 2019 年 11 月 4 日
- 筆記
高並發系統設計的3個利器:快取、限流、降級,本文就限流相關演算法,分析其設計與實現。
從分散式角度來看,限流可分為分散式限流(比如基於Sentinel或者Redis的集群限流)和單機限流。從演算法實現角度來看,限流演算法可分為漏桶演算法、令牌桶演算法和滑動時間窗口演算法。下面主要分析這3種限流演算法和分散式限流實現方案。
漏桶演算法
把請求比作是水,水來了都先放進桶里,並以恆定速度出水(處理請求),當水流量過大會導致桶溢出,即拒絕服務。請求的最大處理速度也就是水從漏桶流出的速度。
基於漏桶(桶+恆定處理速率),可以起到對請求整流效果。漏桶演算法可基於執行緒池來實現,執行緒池使用固定容量的阻塞隊列+固定個數的處理執行緒來實現;最簡單且最常見的漏桶思想的實現就是基於SynchronousQueue的執行緒池,其相當於一個空桶+固定處理執行緒 : )。
注意:原生的漏桶演算法以恆定速度出水(處理請求),但是實際場景中請求的處理耗時可能不相等,為了實現恆定速率,一般都是限定同時處理請求的最大執行緒數。
令牌桶演算法
很多場景中,需要允許某種程度的突發請求,請求的最大速度也就是所有桶大小。這時候漏桶演算法就不合適了,令牌桶演算法更為適合。
令牌桶演算法的原理是系統以恆定的速率產生令牌,然後把令牌放到令牌桶中,令牌桶有一個容量,當令牌桶滿了的時候,再向其中放令牌,那麼多餘的令牌會被丟棄;當想要處理一個請求的時候,需要從令牌桶中取出一個令牌,如果此時令牌桶中沒有令牌,那麼則拒絕該請求。
令牌桶演算法的一個實現方案是:起一個Timer執行緒以固定頻率往桶中放令牌,桶滿時令牌溢出,業務執行緒在獲取令牌時直接從桶中獲取即可。該方案容易理解,但是需要一個Timer執行緒,資源佔用較重。
令牌桶演算法還有一種實現方案不需要用Timer執行緒,這個經典實現就是Guava
中的RateLimiter
。RateLimiter
實現原理如下:
startTick
記錄RateLimiter初始化時的時間戳(單位ns),後續nowMicros
(當前時間點)都是取(System.nanoTime()-startTick)/1000;nextFreeTicketMicros
記錄下次可獲取令牌的開始時間點,在RateLimiter初始化和獲取到令牌之後會進行更新;- 如果nowMicros大於等於nextFreeTicketMicros,表示可以獲取令牌;如果nowMicros大於nextFreeTicketMicros,會計算二者差值併除以放一個令牌的周期,然後賦值給
storedPermits
欄位(表示當前桶中令牌數,注意不能超過桶容量); - 然後storedPermits減去當前需要令牌數,如果此時要獲取令牌數大於storedPermits,那麼會將nextFreeTicketMicros再往後推進
(要獲取令牌 - storedPermits) * 放一個令牌的周期
的時間。
更具體的步驟及程式碼實現可參考RateLimiter源碼,這裡不再贅述。
從步驟4可以看出,初始化一個RateLimiter.create(100),是可以執行rateLimiter.tryAcquire(200)的,只不多會將nextFreeTicketMicros再往後推進而已。
滑動時間窗口演算法
滑動時間窗口演算法就是根據當前時間獲取對應的時間窗口,時間窗口保存有流量相關的統計值,根據該統計值判斷是否觸發流控。
一般來說,時間窗口可以循環復用,在復用時重新初始化即可,具體實現可參考sentinel的滑動窗口實現。滑動時間窗口能夠支援的瞬時流量最大可為該窗口上限,而令牌桶演算法能夠支援的瞬時流量最大為桶大小;注意,滑動時間窗口演算法中獲取token數量一次最大不能超過窗口上限,而RateLimiter實現的令牌桶可以支援一次獲取超過桶大小的token。
分散式限流
上述所說的幾種限流都是單台機器上的限流演算法,有些場景下我們還需要分散式限流,一種是基於Redis做分散式限流,另一種類似於Sentinel分散式限流。
Sentinel
Sentinel分散式限流是啟動一個token server伺服器,其他sentinel client端就是token client端,當做限流操作時,從token server獲取token,獲取成功表示未觸發限流;否則表示觸發了限流;通訊出現異常,可配置降級走本地Sentinel限流機制。分散式限流文檔:Sentinel集群流控
sentinel的分散式限流是token client調用以下方法到服務端獲取token,相當於是每次都會獲取acquireCount個token:
//獲取令牌Token, 參數規則Id,獲取令牌數,優先順序 TokenResult requestToken(Long ruleId, int acquireCount, boolean prioritized);
基於Redis限流
基於Redis做限流操作,使用lua腳本保證命令原子性,比如qps設置為10,如果key不存在,就設置key過期時間1s,value=1;如果value小於10,則自增value;value達到10觸發流控。示例lua程式碼如下:
local key = "rate.limit:" .. KEYS[1] local limit = tonumber(ARGV[1]) local expire_time = ARGV[2] local is_exists = redis.call("EXISTS", key) if is_exists == 1 then if redis.call("INCR", key) > limit then return 0 else return 1 end else redis.call("SET", key, 1) redis.call("EXPIRE", key, expire_time) return 1 end
常用的限流演算法有漏桶、令牌桶和滑動窗口,根據具體場景可選擇不同限流演算法;如果需要集群限流,可選用Sentinel或者基於Redis做分散式限流。
關於Sentinel,估計挺多小夥伴還不知道Sentinel是個什麼東東,Sentinel是一個以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性的框架。github地址為:https://github.com/alibaba/Sentinel。
筆者整理了一份《Sentinel不完全指南》,需要的小夥伴可以關注「TopCoder」公眾號發送
sentinel
來獲取,《Sentinel不完全指南》和Sentinel官方文檔,二者互為補充,結合起來學習Sentinel效果更好呦 : )
歡迎小夥伴關注【TopCoder】閱讀更多精彩好文。