令牌桶算法RateLimiter

令牌桶原理:
令牌桶算法的原理是系統會以一個恆定的速度往桶裏放入令牌,而如果請求需要被處理,則需要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。
算法描述:
  • 假如用戶配置的平均發送速率爲r,則每隔1/r秒一個令牌被加入到桶中(每秒會有r個令牌放入桶中);
  • 假設桶中最多可以存放b個令牌。如果令牌到達時令牌桶已經滿了,那麼這個令牌會被丟棄;
  • 當一個n個字節的數據包到達時,就從令牌桶中刪除n個令牌(不同大小的數據包,消耗的令牌數量不一樣),並且數據包被髮送到網絡;
  • 如果令牌桶中少於n個令牌,那麼不會刪除令牌,並且認爲這個數據包在流量限制之外(n個字節,需要n個令牌。該數據包將被緩存或丟棄);
  • 算法允許最長b個字節的突發,但從長期運行結果看,數據包的速率被限制成常量r。對於在流量限制外的數據包可以以不同的方式處理:(1)它們可以被丟棄;(2)它們可以排放在隊列中以便當令牌桶中累積了足夠多的令牌時再傳輸;(3)它們可以繼續發送,但需要做特殊標記,網絡過載的時候將這些特殊標記的包丟棄。
場景:
通常可應用於搶購限流防止沖垮系統;限制某接口、服務單位時間內的訪問量,譬如一些第三方服務會對用戶訪問量進行限制;限制網速,單位時間內只允許上傳下載多少字節等。平滑過渡方案
Google開源工具包Guava提供了限流工具類RateLimiter,該類基於令牌桶算法(Token Bucket)來完成限流,非常易於使用.RateLimiter經常用於限制對一些物理資源或者邏輯資源的訪問速率.它支持兩種獲取permits接口,一種是如果拿不到立刻返回false,一種會阻塞等待一段時間看能不能拿到.
RateLimiter和Java中的信號量(java.util.concurrent.Semaphore)類似,Semaphore通常用於限制併發量.
RateLimiter 允許某次請求拿走超出剩餘令牌數的令牌,但是下一次請求將爲此付出代價,一直等到令牌虧空補上,並且桶中有足夠本次請求使用的令牌爲止2。這裏面就涉及到一個權衡,是讓前一次請求乾等到令牌夠用才走掉呢,還是讓它先走掉後面的請求等一等呢?Guava 的設計者選擇的是後者,先把眼前的活幹了,後面的事後面再說.
源碼註釋中的一個例子,比如我們有很多任務需要執行,但是我們不希望每秒超過兩個任務執行,那麼我們就可以使用RateLimiter:
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}
另外一個例子,假如我們會產生一個數據流,然後我們想以每秒5kb的速度發送出去.我們可以每獲取一個令牌(permit)就發送一個byte的數據,這樣我們就可以通過一個每秒5000個令牌的RateLimiter來實現:
final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
    rateLimiter.acquire(packet.length);
    networkService.send(packet);
}
另外,我們也可以使用非阻塞的形式達到降級運行的目的,即使用非阻塞的tryAcquire()方法:
if(limiter.tryAcquire()) { //未請求到limiter則立即返回false
  doSomething();
}else{
  doSomethingElse();
}

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

public class RateLimiterDemo {
public static void main(String[] args) throws InterruptedException {
// smooth();
// bursty1();
// bursty2();
warmingUp();
}
/*
* 有很多個任務,但希望每秒不超過X個,可用此類
* */
public static void test1() {
//1代表一秒最多多少個
RateLimiter rateLimiter = RateLimiter.create(0.5);
List<Runnable> tasks = new ArrayList<Runnable>();
for (int i = 0; i < 20; i++) {
tasks.add(new UserRequest(i));
}
ExecutorService threadPool = Executors.newCachedThreadPool();
for (Runnable runnable : tasks) {
// 請求RateLimiter, 每秒超過permits會被阻塞
System.out.println("等待時間:" + rateLimiter.acquire());
threadPool.execute(runnable);
}
}

public static void smooth() { //SmoothBursty 平滑限流
RateLimiter limiter = RateLimiter.create(5);//表示桶容量爲5且每秒新增5個令牌,即每隔200毫秒新增一個令牌
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());
}

//突發了10個請求,令牌桶算法也允許了這種突發(允許消費未來的令牌),但接下來的limiter.acquire(1)將等待差不多2秒桶中纔能有令牌,且接下來的請求也整形爲固定速率了
public static void bursty1() { //SmoothBursty應對突發限流,突發過來避免空等,然後再平滑
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(10));//應對突發流量,提前消耗後面令牌,後面請求要通過時間補償
System.out.println(limiter.acquire(1));
System.out.println(limiter.acquire(10));
System.out.println(limiter.acquire(1));
}

/*
1、創建了一個桶容量爲2且每秒新增2個令牌;
2、首先調用limiter.acquire()消費一個令牌,此時令牌桶可以滿足(返回值爲0);
3、然後線程暫停2秒,接下來的兩個limiter.acquire()都能消費到令牌,第三個limiter.acquire()也同樣消費到了令牌,到第四個時就需要等待500毫秒了。
此處可以看到我們設置的桶容量爲2(即允許的突發量),這是因爲SmoothBursty中有一個參數:最大突發秒數(maxBurstSeconds)默認值是1s,突發量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突發量爲2,
例子中前兩個是消費了之前積攢的突發量,而第三個開始就是正常計算的了。令牌桶算法允許將一段時間內沒有消費的令牌暫存到令牌桶中,留待未來使用,並允許未來請求的這種突發。
*/
public static void bursty2() throws InterruptedException { //SmoothBursty應對突發限流,突發過來避免空等,然後再平滑
RateLimiter limiter = RateLimiter.create(2);
System.out.println(limiter.acquire(20));//0.0 第一次請求消費一個令牌
Thread.sleep(10000L);//默認允許最多積攢1s的剩餘令牌,即最大併發量2,積攢2個令牌
System.out.println(limiter.acquire());//0.0 第二次請求獲取上面積攢令牌
System.out.println(limiter.acquire());//0.0 第三次請求獲取上面積攢令牌
System.out.println(limiter.acquire());//0.0 第三次請求獲取當前令牌
System.out.println(limiter.acquire());//0.5 第三次請求獲取需等待500s
System.out.println(limiter.acquire());
}

/*
*因爲SmoothBursty允許一定程度的突發,會有人擔心如果允許這種突發,假設突然間來了很大的流量,那麼系統很可能扛不住這種突發。
* 因此需要一種平滑速率的限流工具,從而系統冷啓動後慢慢的趨於平均固定速率(即剛開始速率小一些,然後慢慢趨於我們設置的固定速率)。
* Guava也提供了SmoothWarmingUp來實現這種需求,其可以認爲是漏桶算法,但是在某些特殊場景又不太一樣
* */
public static void warmingUp() throws InterruptedException { //SmoothWarmingUp
RateLimiter limiter = RateLimiter.create(5,1000, TimeUnit.MILLISECONDS);
//RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)
//permitsPerSecond表示每秒新增的令牌數,warmupPeriod表示在從冷啓動速率過渡到平均速率的時間間隔
for(int i =1; i < 5;i++) {
System.out.println(limiter.acquire(10));
}
Thread.sleep(1000L);
for(int i =1; i < 5;i++) {
System.out.println(limiter.acquire());
}
//maxPermits等於熱身(warmup)期間能產生的令牌數,比如QPS=4,warmup爲2秒,則maxPermits=8.halfPermits爲maxPermits的一半.
//速率是梯形上升速率的,也就是說冷啓動時會以一個比較大的速率慢慢到平均速率;然後趨於平均速率(梯形下降到平均速率)。可以通過調節warmupPeriod參數實現一開始就是平滑固定速率。
}
}

class UserRequest implements Runnable {
private int id;

public UserRequest(int id) {
this.id = id;
}

public void run() {
System.out.println("第" + (id + 1) + "個請求");
}


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章