高並發系統設計的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】閱讀更多精彩好文。