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

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

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

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

常見的限流方式有:

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

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

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

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

1 限流算法

1.1 令牌桶算法

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

  • 假設限制2r/s,則按照500ms的固定速率添加令牌。
  • 桶的總容量為N,當達到總容量時,新添加的令牌則被丟棄或拒絕。
  • 當一個n個字節大小的數據包到達,則從桶中刪除n個令牌,然後處理數據包。
  • 如果桶中的令牌不足n個,則不會刪除令牌,但是數據包將會被限流。
高併發架構設計,6種常見限流策略,5分鐘徹底搞懂

令牌桶算法(Token Bucket)

1.2 漏桶算法

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

  • 一個固定容量的漏桶,會按照固定的速率流出水滴。
  • 如果桶中無水,則不需要流出水滴。
  • 可以以任意速率流入水滴。
  • 如果流入的水滴超出了桶容量,則新添加的則會被丟棄。
高併發架構設計,6種常見限流策略,5分鐘徹底搞懂

漏桶算法

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


分享到:


相關文章: