限流 - Guava RateLimiter

限流

限流的目的是經過對併發訪問/請求進行限速或者一個時間窗口內的的請求進行限速來保護系統,一旦併發訪問/請求達到限制速率或者超過其承受範圍時候則能夠拒絕服務、排隊或引流。html

目前經常使用的限流算法有兩個:漏桶算法和令牌桶算法。java

漏桶算法-Leaky Bucket

http://en.wikipedia.org/wiki/Leaky_bucket算法

根據wiki上的介紹,Leaky Bucket實際上有兩種不一樣的含義。api

1)as a meter(做爲計量工具)緩存

2)as a queue(做爲調度隊列)網絡

Leaky Bucket 做爲計量工具,以下所示:併發

如圖,桶自己具備一個恆定的速率往下漏水,而上方時快時慢地會有水進入桶中。當桶還未滿時,上方的水能夠加入。一旦水滿,上方的水就沒法加入了。桶滿正是算法中的一個的關鍵觸發條件(即流量異常判斷成立的條件)。而此條件下如何處理上方漏下的水,則有了下面兩種常見的方式。app

在桶滿水以後,常見的兩種處理方式爲:ssh

1)暫時攔截住上方水的向下流動,等待桶中的一部分水漏走後,再放行上方水。工具

2)溢出的上方水直接拋棄。

將水看做網絡通訊中數據包的抽象,則方式1起到的效果稱爲Traffic Shaping,方式2起到的效果稱爲Traffic Policing。

Comparing Traffic Policing and Traffic Shaping for Bandwidth Limiting

The following diagram illustrates the key difference. Traffic policing propagates bursts. When the traffic rate reaches the configured maximum rate, excess traffic is dropped (or remarked). The result is an output rate that appears as a saw-tooth with crests and troughs. In contrast to policing, traffic shaping retains excess packets in a queue and then schedules the excess for later transmission over increments of time. The result of traffic shaping is a smoothed packet output rate.

參考:https://www.cisco.com/c/en/us/support/docs/quality-of-service-qos/qos-policing/19645-policevsshape.html

令牌桶算法-Token Bucket

對於不少應用場景來講,除了要求可以限制數據的平均傳輸速率外,還要求容許某種程度的突發傳輸。這時候漏桶算法可能就不合適了,令牌桶算法更爲適合。

https://en.wikipedia.org/wiki/Token_bucket

令牌桶算法最初來源於計算機網絡。在網絡傳輸數據時,爲了防止網絡擁塞,需限制流出網絡的流量,使流量以比較均勻的速度向外發送。令牌桶算法就實現了這個功能,可控制發送到網絡上數據的數目,並容許突發數據的發送。

令牌桶算法是網絡流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型狀況下,令牌桶算法用來控制發送到網絡上的數據的數目,並容許突發數據的發送。

大小固定的令牌桶可自行以恆定的速率源源不斷地產生令牌。若是令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢出。最後桶中能夠保存的最大令牌數永遠不會超過桶的大小。

傳送到令牌桶的數據包須要消耗令牌。不一樣大小的數據包,消耗的令牌數量不同。

令牌桶這種控制機制基於令牌桶中是否存在令牌來指示何時能夠發送流量。令牌桶中的每個令牌都表明一個字節。若是令牌桶中存在令牌,則容許發送流量;而若是令牌桶中不存在令牌,則不容許發送流量。所以,若是令牌桶中有足夠的令牌,那麼流量就能夠以峯值速率發送。

算法描述:

  1. 假如用戶配置的平均發送速率爲r,則每隔1/r秒一個令牌被加入到桶中(每秒會有r個令牌放入桶中);
  2. 假設桶中最多能夠存放b個令牌。若是令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄;
  3. 當一個n個字節的數據包到達時,就從令牌桶中刪除n個令牌(不一樣大小的數據包,消耗的令牌數量不同),而且數據包被髮送到網絡;
  4. 若是令牌桶中少於n個令牌,那麼不會刪除令牌,而且認爲這個數據包在流量限制以外(n個字節,須要n個令牌。該數據包將被緩存或丟棄);
  5. 算法容許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對於在流量限制外的數據包能夠以不一樣的方式處理:(1)它們能夠被丟棄;(2)它們能夠排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸;(3)它們能夠繼續發送,但須要作特殊標記,網絡過載的時候將這些特殊標記的包丟棄。

令牌桶算法VS漏桶算法

漏桶

漏桶的出水速度是恆定的,那麼意味着若是瞬時大流量的話,將有大部分請求被丟棄掉(也就是所謂的溢出)。

令牌桶

生成令牌的速度是恆定的,而請求去拿令牌是沒有速度限制的。這意味,面對瞬時大流量,該算法能夠在短期內請求拿到大量令牌,並且拿令牌的過程並非消耗很大的事情。

Java 實現 - Guava ReateLimtier

Guava RateLimiter提供了令牌桶算法實現:平滑突發限流(SmoothBursty)和平滑預熱限流(SmoothWarmingUp)實現。

SmoothBursty

//RateLimiter.create(5) 表示桶容量爲5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

輸入以下,

0.0
0.191546
0.19138
0.197585
0.194843
0.195557
  1. RateLimiter.create(5) 表示桶容量爲5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;
  2. limiter.acquire()表示消費一個令牌,若是當前桶中有足夠令牌則成功(返回值爲0),若是桶中沒有令牌則暫停一段時間,好比發令牌間隔是200毫秒,則等待200毫秒後再去消費令牌(如上測試用例返回的爲0.198239,差很少等待了200毫秒桶中才有令牌可用),這種實現將突發請求速率平均爲了固定請求速率。

突發的請求速率以下,

// guava Stopwatch
Stopwatch stopwatch = Stopwatch.createUnstarted();

//RateLimiter.create(5) 表示桶容量爲5且每秒新增5個令牌,即每隔200毫秒新增一個令牌;
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(10)); // 0.0,獲取令牌成功

System.out.println("獲取令牌start...");
stopwatch.start();
System.out.println(limiter.acquire(1));
long time = stopwatch.elapsed(TimeUnit.MILLISECONDS);
System.out.println("獲取令牌花費的時間=" + time);

System.out.println(limiter.acquire(1));

輸出以下,

獲取令牌start...
1.996403
獲取令牌花費的時間=2002
0.193232

第一秒突發了10個請求,令牌桶算法也容許了這種突發(容許消費將來的令牌),但接下來的limiter.acquire(1)將等待差很少2秒桶中才能有令牌,且接下來的請求也整形爲固定速率了。

**SmoothWarmingUp **

平滑預熱限流方式建立以下,

RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);

System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

Thread.sleep(1000L);
System.out.println("sleep end...");

System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

輸出以下,

0.0
0.51702
0.355476
0.220346
0.197056
sleep end...
0.0
0.365465
0.221425
0.199815
0.199381

能夠看到剛開始獲取令牌所花費的時候比較大,隨着時間的推移,所花費的時間愈來愈少,趨於穩定,說明速率趨於穩定。這就是所謂的預熱。

======END======

相關文章
相關標籤/搜索