高併發架構設計,6種常見限流策略,5分鐘徹底搞懂

在進行高併發架構設計時,有很多手段來保護系統,如緩存、降級和限流等。

通過使用緩存,可以提升系統的訪問速度;通過降級策略可以暫時屏蔽非核心業務,保障核心業務不受影響;通過限流對併發訪問進行限速,達到一定的速率就可以拒絕服務、排隊等待、降級。

歡迎關注筆者,每天分享架構乾貨。

常見的限流方式有:

限制總併發數(數據庫連接池、線程池)限制瞬時併發數(如Nginx的limit_conn模塊)限制時間窗口的平均速率(如Guava的RateLimiter、Nginx的limit_req模塊)限制遠程接口的調用速率限制MQ的消費速率等

從應用的層面上來講,又可以分為:接入層限流、應用層限流和分佈式限流等。

我們今天主要從兩個層面來講解分佈式流控策略,其一:限流算法;其二:應用層限流。

1 限流算法

1.1 令牌桶算法

令牌桶算法是一個存放固定容量令牌的容器,按照固定速率添加令牌,算法描述如下:

假設限制2r/s,則按照500ms的固定速率添加令牌。桶的總容量為N,當達到總容量時,新添加的令牌則被丟棄或拒絕。當一個n個字節大小的數據包到達,則從桶中刪除n個令牌,然後處理數據包。如果桶中的令牌不足n個,則不會刪除令牌,但是數據包將會被限流。

令牌桶算法(Token Bucket)

1.2 漏桶算法

漏桶可以用於流量整型和流量控制,算法描述如下:

一個固定容量的漏桶,會按照固定的速率流出水滴。如果桶中無水,則不需要流出水滴。可以以任意速率流入水滴。如果流入的水滴超出了桶容量,則新添加的則會被丟棄。

漏桶算法

1.3 兩種算法對比

兩者的​主要區別在於“漏桶算法”能夠強行限制數據的傳輸速率,而“令牌桶算法”除了能夠限制數據的平均傳輸速率外,還允許某種程度的突發傳輸。在“令牌桶算法”中,只要令牌桶中存在令牌,那麼就允許突發地傳輸數據直到達到用戶配置的門限,因此它適合於具有突發特性的流量。總結一下:

令牌桶允許一定程度的突發請求(有令牌就可以處理),漏桶的主要目的是來平滑流入的速率。

2 應用級限流

2.1 限制總併發數/連接/請求數

對於一個應用來說,總會有一個TPS/QPS的閥值,如果超過了閥值,則系統就會變得非常慢跟甚至無法響應。因此需要對系統進行過載保護,避免大量請求擊垮系統。

如Tomcat的Connector中的以下幾個參數:

acceptCount:如果Tomcat的線程都忙於響應,新來的連接將會進入隊列,如果超出隊列大小,則會拒絕連接。maxConnections:瞬時最大連接數,超出的會排隊等待。maxThreads:用來處理請求的最大線程數,如果請求處理量一直遠遠大於線程數,則會引起響應變慢甚至會僵死。

2.2 限制接口的總併發/請求數

在Java中可以用線程安全的AtomicLong或者Semaphore進行處理,如下使用了AtomicLong進行簡單的統計:

try {

if (atomic.incrementAndSet() > 閥值) {

// 拒絕請求

}

// 處理請求

} finally {

atomic.decrementAndGet();

}

這種方式實現起來比較簡單暴力,沒有平滑處理,這需要根據實際情況選擇使用。

2.3 限流接口每秒的請求數

限制每秒的請求數,可以使用Guava的Cache來存儲計數器,設置過期時間為2s(保證能記錄1S內的計數)。下面代碼使用當前時間戳的秒數作為key進行統計,這種限流的方式也比較簡單。

LoadingCache counter = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.SECONDS) .build(new CacheLoader() { @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; } //業務處理 }

上面介紹的兩種限流策略都是單機限流,當系統進行多實例部署時,就無法實現整體對外功能的限流了。如果平行的應用服務器需要共享限流閥值指標,可以使用Redis作為共享的計數器。

2.4平滑限流接口的請求數

Guava的RateLimiter提供的令牌桶算法可以用於平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

2.5 平滑突發限流

平滑突發限流顧名思義,就是允許突發的流量進入,後面再慢慢的平穩限流。下面給出幾個示例:

創建了容量為5的桶,並且每秒新增5個令牌,即每200ms新增一個令牌

RateLimiter limiter = RateLimiter.create(5); while (true) { // 獲取令牌,獲取後可以執行後續的業務邏輯 System.out.println(limiter.acquire()); }

上面代碼執行結果如下所示:

0.0 0.188216 0.191938 0.199089 0.19724 0.19997

上面while循環中執行的limiter.acquire(),當沒有令牌時,此方法會阻塞。實際應用當中應當使用tryAcquire()方法,如果獲取不到就直接執行拒絕服務。

下面再介紹一箇中途休眠的場景:

RateLimiter limiter = RateLimiter.create(2); System.out.println(limiter.acquire()); Thread.sleep(1500L); while (true) { System.out.println(limiter.acquire()); }

上面代碼執行結果如下:

0.0 0.0 0.0 0.0 0.499794 0.492334

從上面結果可以看出,當線程休眠時,會囤積令牌,留給後續的acquire()使用。但是上面的代碼只能囤積1s的令牌(也就是2個),當睡眠時間超過1.5s時,執行結果還是相同的。

2.6 平滑預熱限流

平滑突發限流有可能瞬間帶來了很大的流量,如果系統扛不住的話,系統很容易掛掉。平滑預熱限流可以解決這個問題。方式如下:

permitsPerSecond表示每秒新增的令牌數,warmupPeriod表示從冷啟動速率過渡到平均速率所需要的時間間隔:

RateLimiter.create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS); for (int i = 1; i < 5; i++) { System.out.println(limiter.acquire()); } Thread.sleep(1000L); for (int i = 1; i < 50; i++) { System.out.println(limiter.acquire()); }

執行結果如下:

0.0 0.513566 0.353789 0.215167 0.0 0.519854 0.359071 0.219118 0.197874 0.197322 0.197083 0.196838

上面結果可以看出來,平滑預熱限流的耗時是慢慢趨近平均值的。

歡迎關注筆者,每天分享架構乾貨。

參考鏈接:https://www.jianshu.com/p/5c218b3b5d38