文案摘抄自網絡與同事分享。
1、為什么要限流:
在開發高並發系統時有三把利器用來保護系統:緩存、降級和限流。本文結合作者的一些經驗介紹限流的相關概念、算法和常規的實現方式。
緩存
緩存比較好理解,在大型高並發系統中,如果沒有緩存數據庫將分分鍾被爆,系統也會瞬間癱瘓。使用緩存不單單能夠提升系統訪問速度、提高並發訪問量,也是保護數據庫、保護系統的有效方式。大型網站一般主要是“讀”,緩存的使用很容易被想到。在大型“寫”系統中,緩存也常常扮演者非常重要的角色。比如累積一些數據批量寫入,內存里面的緩存隊列(生產消費),以及HBase寫數據的機制等等也都是通過緩存提升系統的吞吐量或者實現系統的保護措施。甚至消息中間件,你也可以認為是一種分布式的數據緩存。
降級
服務降級是當服務器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行。降級往往會指定不同的級別,面臨不同的異常等級執行不同的處理。根據服務方式:可以拒接服務,可以延遲服務,也有時候可以隨機服務。根據服務范圍:可以砍掉某個功能,也可以砍掉某些模塊。總之服務降級需要根據不同的業務需求采用不同的降級策略。主要的目的就是服務雖然有損但是總比沒有好。
限流
限流可以認為服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。一般來說系統的吞吐量是可以被測算的,為了保證系統的穩定運行,一旦達到的需要限制的閾值,就需要限制流量並采取一些措施以完成限制流量的目的。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。
常見算法:
1.計數器法:
原理:
系統維護一個計數器,來一個請求就加1,請求解決完成就減1,當計數器大於指定的閾值,就拒絕新的請求。
基於這個簡單的方法,可以再延伸出少量高級功能,比方閾值可以不是固定值,是動態調整的。另外,還可以有多組計數器分別管理不同的服務,以保證互不影響等。
缺點:
惡意用戶通過在時間窗口的重置節點處突發請求, 可以瞬間超過我們的速率限制。用戶有可能通過算法的這個漏洞,瞬間壓垮我們的應用。
2、隊列:
就是基於FIFO隊列,所有請求都進入隊列,后台程序從隊列中取出待解決的請求依次解決。
基於隊列的方法,也可以延伸出更多的玩法來,比方可以設置多個隊列以配置不同的優先級。
3、滑動窗口,又稱rolling window(隊列的升級版)
比如某個服務最多只能每秒鍾處理100個請求。我們可以設置一個1秒鍾的滑動窗口,窗口中有10個格子,每個格子100毫秒,每100毫秒移動一次,每次移動都需要記錄當前服務請求的次數。內存中需要保存10次的次數。可以用數據結構LinkedList來實現。格子每次移動的時候判斷一次,當前訪問次數和LinkedList中最后一個相差是否超過100,如果超過就需要限流了。
當滑動窗口的格子划分的越多,那么滑動窗口的滾動就越平滑,限流的統計就會越精確。
這種模式的實現的方式更加契合流控的本質意義,理解較為簡單。但由於訪問量的不可預見性,會發生單位時間的前半段大量請求涌入,而后半段則拒絕所有請求的情況(通常,需要可以將單位時間切的足夠的小來緩解);其次,很難確定這個閾值設置在多少比較合適,只能通過經驗或者模擬(如壓測)來進行估計,即使是壓測也很難估計的准確。集群部署中每台機器的硬件參數不同,可能導致需要對每台機器的閾值設置的都不盡相同。同一台機子在不同的時間點的系統壓力也不一樣(比如晚上還有一些任務,或其他的一些業務操作的影響),能夠承受的最大閾值也不盡相同,無法考慮的周全。
所以滑窗模式通常適用於對某一資源的保護的需求上,如對db的保護,對某一服務的調用的控制上。
4、漏桶算法
漏桶(Leaky Bucket)算法思路很簡單,水(請求)先進入到漏桶里,漏桶以一定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),然后就拒絕請求,可以看出漏桶算法能強行限制數據的傳輸速率。
因為漏桶的漏出速率是固定的參數,所以,即使網絡中不存在資源沖突(沒有發生擁塞),漏桶算法也不能使流突發(burst)到端口速率.因此,漏桶算法對於存在突發特性的流量來說缺乏效率
5、令牌桶算法
首先還是要基於一個隊列,請求放到隊列里面。但除了隊列以外,還要設置一個令牌桶,另外有一個腳本以持續恆定的速度往令牌桶里面放令牌,后台解決程序每解決一個請求就必需從桶里拿出一個令牌,假如令牌拿完了,那就不能解決請求了。我們可以控制腳本放令牌的速度來達到控制后台解決的速度,以實現動態流控。
1.每秒會有 r 個令牌放入桶中,或者說,每過 1/r 秒桶中增加一個令牌
2.桶中最多存放 b 個令牌,如果桶滿了,新放入的令牌會被丟棄
3.當一個 n 字節的數據包到達時,消耗 n 個令牌,然后發送該數據包
4.如果桶中可用令牌小於 n,則該數據包將被緩存或丟棄
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法(Token Bucket)來完成限流, 將一秒鍾切割為令牌數的時間片段,每個時間片段等同於一個token。非常易於使用.RateLimiter經常用於限制對一些物理資源或者邏輯資源的訪問速率.
6、分布式限流
實際生產環境下最快捷且有效的方式是使用RateLimiter實現,但是這很容易踩到一個坑,單節點模式下,使用RateLimiter進行限流一點問題都沒有。但線上是分布式系統,布署了多個節點,而且多個節點最終調用的是同一個API/服務商接口。雖然我們對單個節點能做到將QPS限制在400/s,但是多節點條件下,如果每個節點均是400/s,那么到服務商那邊的總請求就是節點數x400/s,於是限流效果失效。使用該方案對單節點的閾值控制是難以適應分布式環境的。
方式一:redis
@GetMapping("/")
public void index(HttpServletResponse response) throws IOException {
Jedis jedis = jedisPool.getResource();
String token = RedisRateLimiter.acquireTokenFromBucket(jedis, LIMIT, TIMEOUT);
if (token == null) {
response.sendError(500);
}else{
//TODO 你的業務邏輯
}
jedisPool.returnResource(jedis);
}
方式二:方式二 Reids+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