高併發場景下如何實現系統限流?
在分佈式高可用設計中,限流應該是應用最廣泛的技術手段之一,今天一起來討論一下,爲什麼需要限流,以及常見的限流算法都有哪些。
一、常見限流算法
限流是服務降級的一種手段,顧名思義,通過限制系統的流量,從而實現保護系統的目的。
合理的限流配置,需要了解系統的吞吐量,所以,限流一般需要結合容量規劃和壓測來進行。當外部請求接近或者達到系統的最大閾值時,觸發限流,採取其他的手段進行降級,保護系統不被壓垮。常見的降級策略包括延遲處理、拒絕服務、隨機拒絕等。
限流後的策略,其實和 Java 併發編程中的線程池非常類似,我們都知道,線程池在任務滿的情況下,可以配置不同的拒絕策略,比如:
-
AbortPolicy,會丟棄任務並拋出異常;
-
DiscardPolicy,丟棄任務,不拋出異常;
-
DiscardOldestPolicy 等,當然也可以自己實現拒絕策略。
Java 的線程池是開發中一個小的功能點,但是見微知著,也可以引申到系統的設計和架構上,將知識進行合理地遷移複用。
限流方案中有一點非常關鍵,那就是如何判斷當前的流量已經達到我們設置的最大值,具體有不同的實現策略,下面進行簡單分析。
二、計數器法
一般來說,我們進行限流時使用的是單位時間內的請求數,也就是平常說的 QPS,統計 QPS 最直接的想法就是實現一個計數器。
計數器法是限流算法裏最簡單的一種算法,我們假設一個接口限制 100 秒內的訪問次數不能超過 10000 次,維護一個計數器,每次有新的請求過來,計數器加 1。這時候判斷,如果計數器的值小於限流值,並且與上一次請求的時間間隔還在 100 秒內,允許請求通過,否則拒絕請求;如果超出了時間間隔,要將計數器清零。
下面的代碼裏使用 AtomicInteger 作爲計數器,可以作爲參考:
public class CounterLimiter {
//初始時間
private static long startTime = System.currentTimeMillis();
//初始計數值
private static final AtomicInteger ZERO = new AtomicInteger(0);
//時間窗口限制
private static final int interval = 10000;
//限制通過請求
private static int limit = 100;
//請求計數
private AtomicInteger requestCount = ZERO;
//獲取限流
public boolean tryAcquire() {
long now = System.currentTimeMillis();
//在時間窗口內
if (now < startTime + interval) {
//判斷是否超過最大請求
if (requestCount.get() < limit) {
requestCount.incrementAndGet();
return true;
}
return false;
} else {
//超時重置
requestCount = ZERO;
startTime = now;
return true;
}
}
}
計數器策略進行限流,可以從單點擴展到集羣,適合應用在分佈式環境中。單點限流使用內存即可,如果擴展到集羣限流,可以用一個單獨的存儲節點,比如 Redis 或者 Memcached 來進行存儲,在固定的時間間隔內設置過期時間,就可以統計集羣流量,進行整體限流。
計數器策略有一個很大的缺點,是對臨界流量不友好,限流不夠平滑。假設這樣一個場景,我們限制用戶一分鐘下單不超過 10 萬次,現在在兩個時間窗口的交匯點,前後一秒鐘內,分別發送 10 萬次請求。也就是說,窗口切換的這兩秒鐘內,系統接收了 20 萬下單請求,這個峯值可能會超過系統閾值,影響服務穩定性。
對計數器算法的優化,就是避免出現兩倍窗口限制的請求,可以使用滑動窗口算法實現,感興趣的小夥伴可以去了解一下。
三、漏桶和令牌桶算法
漏桶算法和令牌桶算法,在實際應用中更加廣泛,也經常被拿來對比,所以我們放在一起進行分析。
漏桶算法可以用漏桶來對比,假設現在有一個固定容量的桶,底部鑽一個小孔可以漏水,我們通過控制漏水的速度,來控制請求的處理,實現限流功能。
漏桶算法的拒絕策略很簡單,如果外部請求超出當前閾值,則會在水桶裏積蓄,一直到溢出,系統並不關心溢出的流量。漏桶算法是從出口處限制請求速率,並不存在上面計數器法的臨界問題,請求曲線始終是平滑的。
漏桶算法的一個核心問題是,對請求的過濾太精準了,我們常說,水至清則無魚,其實在限流裏也是一樣的, 我們限制每秒下單 10 萬次,那 10 萬零 1 次請求呢?是不是必須拒絕掉呢?
大部分業務場景下這個答案是否定的,雖然限流了,但還是希望系統允許一定的突發流量,這時候就需要令牌桶算法。
再來看一下令牌桶算法,在令牌桶算法中,假設我們有一個大小恆定的桶,這個桶的容量和設定的閾值有關,桶裏放着很多令牌,通過一個固定的速率,往裏邊放入令牌,如果桶滿了,就把令牌丟掉,最後桶中可以保存的最大令牌數永遠不會超過桶的大小。
當有請求進入時,就嘗試從桶裏取走一個令牌,如果桶裏是空的,那麼這個請求就會被拒絕。
不知道你有沒有應用過 Google 的 Guava 開源工具包,在 Guava 中,就有限流策略的工具類 RateLimiter,RateLimiter 基於令牌桶算法實現流量限制,使用非常方便。
RateLimiter 會按照一定的頻率往桶裏扔令牌,線程拿到令牌才能執行,RateLimter 的 API 可以直接應用,主要方法是 acquire 和 tryAcquire,acquire 會阻塞,tryAcquire 方法則是非阻塞的。下面是一個簡單的示例:
複製代碼
public class LimiterTest {
public static void main(String[] args) throws InterruptedException {
//允許10個,permitsPerSecond
RateLimiter limiter = RateLimiter.create(100);
for(int i=1;i<200;i++){
if (limiter.tryAcquire(1)){
System.out.println("第"+i+"次請求成功");
}else{
System.out.println("第"+i+"次請求拒絕");
}
}
}
}
四、不同限流算法的比較
計數器算法實現比較簡單,特別適合集羣情況下使用,但是要考慮臨界情況,可以應用滑動窗口策略進行優化,當然也是要看具體的限流場景。
漏桶算法和令牌桶算法,漏桶算法提供了比較嚴格的限流,令牌桶算法在限流之外,允許一定程度的突發流量。在實際開發中,我們並不需要這麼精準地對流量進行控制,所以令牌桶算法的應用更多一些。
如果我們設置的流量峯值是 permitsPerSecond=N,也就是每秒鐘的請求量,計數器算法會出現 2N 的流量,漏桶算法會始終限制 N 的流量,而令牌桶算法允許大於 N,但不會達到 2N 這麼高的峯值。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/7k2ih0A97eWtLfVOsiF4oA