什麼叫限流?
即限制流量進入
- 緩存,是用來增加系統吞吐量,提升通路速度提供高并發。
- 降級,是在系統某些服務元件不可用的時候、流量暴增、資源耗盡等情況下,暫時屏蔽掉出問題的服務,繼續提供降級服務,給使用者盡可能的友好提示,傳回兜底資料,不會影響整體業務流程,待問題解決再重新上線服務
- 限流,是指在使用緩存和降級無效的場景。比如當達到門檻值後限制接口調用頻率,通路次數,庫存個數等,在出現服務不可用之前,提前把服務降級。隻服務好一部分使用者。
在我們使用微信、支付寶、短信等等這些api的時候,每個接口都會有調用上的限流。
限流是對某一時間視窗内的請求數進行限制,保持系統的可用性、穩定性和安全性,防止因流量暴增而導緻的系統運作緩慢或當機。
限流算法
計數器算法
- 簡單粗暴
- 比如線程池大小,資料庫連接配接池大小、nginx連接配接數等都屬于計數器算法。
- 全局或某段時間範圍達到門檻值則限流。
漏桶算法
- 削峰
- 緩沖
- 消費速度固定 因為計算性能固定
- 保證桶不能忙
令牌桶算法
- 平滑的流入速率限制,消費/秒。
- 可以用于對外服務接口,内部叢集調用
差別
- 令牌桶是按照固定速率從桶裡拿令牌消費,如果令牌為0,則拒絕新請求
- 漏桶是按照固定速率流出請求,流入速率不控制,當桶内請求達到門檻值,新請求則被拒絕。
- 令牌桶支援每次拿多個令牌,平均流入速率,并支援突發流入,還可以支援緩慢提升流入速度
并發限流
設定系統門檻值總的qps個數
Tomcat中配置的
- acceptCount 響應連接配接數
- maxConnections 瞬時最大連接配接數
- maxThreads 最大線程數
接口限流
接口總數
可以使用atomic類或者semaphore進行限流
這種方式簡單粗暴。沒有平滑處理。使用限制某個接口的總并發數,或限制某賬号服務調用總次數。
比如某些開放平台限制試用賬号。
if (atomic.incrementAndGet() > 100){
// 拒絕
}finally{
atomic.decrementAndGet();
}
接口時間視窗
此時可以使用
Guava Cache
,類似于一個
ConcurrentMap
,但并不完全一樣。
最基礎的不同是
ConcurrentMap
儲存所有的元素知道它們被明确删除,
Guava Cache
可以配置自動過期
//計數器
counter;
// 限制數量
limit;
// 限制機關 1000=秒
unit;
// 獲得目前時間
current = system.currentTimeMillis() / unit
//判斷時間窗内是否限制通路
if (counter.get(current).incrementAndGet() > limit){
// 拒絕
}
使用guava實作令牌桶
穩定模式(SmoothBursty:令牌生成速度恒定)
RateLimiter.create(2) //容量和突發量,令牌桶算法允許将一段時間内沒有消費的令牌暫存到令牌桶中,用來突發消費。
漸進模式(SmoothWarmingUp:令牌生成速度緩慢提升直到維持在一個穩定值)
// 平滑限流,從冷啟動速率(滿的)到平均消費速率的時間間隔
RateLimiter limiter = RateLimiter.create(2,1000l,TimeUnit.MILLISECONDS);
分布式系統限流
Nginx + Lua
可以使用resty.lock保持原子特性,請求之間不會産生鎖的重入
https://github.com/openresty/lua-resty-lock
使用lua_shared_dict存儲資料
local locks = require "resty.lock"
local function acquire()
local lock =locks:new("locks")
local elapsed, err =lock:lock("limit_key") --互斥鎖 保證原子特性
local limit_counter =ngx.shared.limit_counter --計數器
local key = "ip:" ..os.time()
local limit = 5 --限流大小
local current =limit_counter:get(key)
if current ~= nil and current + 1> limit then --如果超出限流大小
lock:unlock()
return 0
end
if current == nil then
limit_counter:set(key, 1, 1) --第一次需要設定過期時間,設定key的值為1,
過期時間為1秒
else
limit_counter:incr(key, 1) --第二次開始加1即可
end
lock:unlock()
return 1
end
ngx.print(acquire())