基於分散式環境下限流系統的設計
前提
業務背景
就拿前些天的雙十一的 “搶券活動” 來說,一般是設定整點開始搶的,你想想,淘寶的使用者群體非常大,可以達到億級別,而服務介面每秒能處理的量是有限的,那麼這個時候問題就會出現,我們如何通過程式來控制使用者搶券呢,於是就必須加上這個限流功能了。
生產環境
1、服務介面所能提供的服務上限(limit)假如是 500次/s
2、使用者請求介面的次數未知,QPS可能達到 800次/s,1000次/s,或者更高
3、當服務介面的訪問頻率超過 500次/s,超過的量將拒絕服務,多出的資訊將會丟失
4、線上環境是多節點部署的,但是呼叫的是同一個服務介面
於是,為了保證服務的可用性,就要對服務介面呼叫的速率進行限制(介面限流)。
什麼是限流?
限流是對系統的出入流量進行控制,防止大流量出入,導致資源不足,系統不穩定。
限流系統是對資源訪問的控制組件,控制主要的兩個功能:限流策略和熔斷策略,對於熔斷策略,不同的系統有不同的熔斷策略訴求,有的系統希望直接拒絕、有的系統希望排隊等待、有的系統希望服務降級、有的系統會定製自己的熔斷策略,這裡只針對限流策略這個功能做詳細的設計。
限流演算法
1、限制瞬時併發數
Guava RateLimiter 提供了令牌桶演算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。
2、限制某個介面的時間窗最大請求數
即一個時間視窗內的請求數,如想限制某個介面/服務每秒/每分鐘/每天的請求數/呼叫量。如一些基礎服務會被很多其他系統呼叫,比如商品詳情頁服務會呼叫基礎商品服務呼叫,但是怕因為更新量比較大將基礎服務打掛,這時我們要對每秒/每分鐘的呼叫量進行限速;一種實現方式如下所示:
LoadingCache<Long, AtomicLong> counter =
CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.SECONDS)
.build(new CacheLoader<Long, AtomicLong>() {
@Override
public AtomicLong load(Long seconds) throws Exception {
return new AtomicLong(0);
}
});
long limit = 1000;
while(true) {
//得到當前秒
long currentSeconds = System.currentTimeMillis() / 1000;
if(counter.get(currentSeconds).incrementAndGet() > limit) {
System.out.println("限流了:" + currentSeconds);
continue;
}
//業務處理
}
使用Guava的Cache來儲存計數器,過期時間設定為2秒(保證1秒內的計數器是有的),然後我們獲取當前時間戳然後取秒數來作為KEY進行計數統計和限流,這種方式也是簡單粗暴,剛才說的場景夠用了。
3、令牌桶
演算法描述:
- 假如使用者配置的平均傳送速率為r,則每隔1/r秒一個令牌被加入到桶中
- 假設桶中最多可以存放b個令牌。如果令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄
- 當流量以速率v進入,從桶中以速率v取令牌,拿到令牌的流量通過,拿不到令牌流量不通過,執行熔斷邏輯
屬性
- 長期來看,符合流量的速率是受到令牌新增速率的影響,被穩定為:r
- 因為令牌桶有一定的儲存量,可以抵擋一定的流量突發情況
- M是以位元組/秒為單位的最大可能傳輸速率。 M>r
- T max = b/(M-r) 承受最大傳輸速率的時間
- B max = T max * M 承受最大傳輸速率的時間內傳輸的流量
優點:流量比較平滑,並且可以抵擋一定的流量突發情況
4、GOOGLE GUAVA 提供的工具庫中 RATELIMITER 類(內部也是採用令牌桶演算法實現)
最快的方式是使用 RateLimit 類,但是這僅限制在單節點,如果是分散式系統,每個節點的 QPS 是一樣的,請求量到服務介面那的話就是 QPS * 節點數 了。所以這種方案在分散式的情況下不適用!
5、基於 REDIS 實現,儲存兩個 KEY,一個用於計時,一個用於計數。請求每呼叫一次,計數器增加 1,若在計時器時間內計數器未超過閾值,則可以處理任務。
這種能夠很好地解決了分散式環境下多例項所導致的併發問題。因為使用redis設定的計時器和計數器均是全域性唯一的,不管多少個節點,它們使用的都是同樣的計時器和計數器,因此可以做到非常精準的流控。
程式碼就不公佈了,畢竟涉及公司隱私了。