限流 RateLimiter

引貼: 高併發系統之限流特技java

在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流算法

  • 緩存 緩存的目的是提高系統訪問速度和增大系統處理容量
  • 降級 降級是當服務出現問題或者影響到核心流程時,須要暫時屏蔽掉,待高峯或者問題解決後再打開
  • 限流 限流的目的是經過對併發訪問/請求進行限速,或者對一個時間窗口內的請求進行限速來保護系統,一旦達到限制速率則能夠拒絕服務、排隊或等待、降級等處理

RateLimiter

由 guava 提供,經常使用在削峯控流的場景中。
Java 併發庫 的Semaphore信號量控制也能夠作到必定的控制: 經過 acquire() 獲取一個許可,若是沒有就等待,而 release() 釋放一個許可 。
固然在具體使用的業務場景中,既然都要限流了,要考慮是否線程池的配置合理!spring

RateLimiter的兩種模式: SmoothBursty(穩定模式) &  SmoothWarmingUp(漸進模式) 。
使用時須要考慮初始化的時機,避免剛初始化即有大量訪問併發形成等待(acquire)或訪問拒絕(tryAcquire)。(可在spring容器初始化階段提早構建)
也須要考慮SmoothBursty模式下的必定程度突發形成的峯值問題。緩存

  • SmoothBursty 是不預熱令牌的,而SmoothWarmingUp 預熱。
  • 併發請求時,當本次令牌不夠時會肯定下一個的請求阻塞時間,下一個纔會真正阻塞!! 
  • 構造參數裏有一個SleepingStopwatch其實是TimeUnit.sleep來控制當前請求的阻塞時長。
  •  SmoothWarmingUp 模式下很差用tryAcquire,由於有梯度速率變化。
  • 線程安全。在覈心方法中使用synchronized 加鎖來控制併發。

成員變量

  • storedPermits:  當前存儲令牌數
  • maxPermits: 最大存儲令牌數, 應對忽然的高併發
  • stableIntervalMicros:生成單個令牌須要時間。 eg. 好比說是每秒5個,那間隔時間200ms
  • nextFreeTicketMicros :下一次請求能夠獲取令牌的起始時間 。 因爲RateLimiter容許預消費,上次請求預消費令牌後, 下次請求須要等待相應的時間到nextFreeTicketMicros時刻才能夠獲取令牌

實現原理

SmoothBursty

  1. 在初始化時(create):不預熱令牌
    1. 初始化SleepingStopwatch;
    2. 肯定好每一個令牌在每秒生成的間隔時間stableIntervalMicros;
    3. 肯定好令牌最大數量(maxPermits)(SmoothBursty 模式下默認是1s的量)
    4. 肯定好下一個令牌可供給時間(nextFreeTicketMicros)<初始化值等於構建raterLimiter時的TimeMicros>
    5. 當前存儲令牌數爲0。
  2. 每次acquire時:
    1. 若請求時間比此時nextFreeTicketMicros大, 計算這段時間間隔內會生成的令牌,(不超過最大值),肯定此時的令牌總量storedPermits; 並更新nextFreeTicketMicros 爲當前請求時間
    2. 計算所須要的令牌是否充足
      1. 注意: 這裏返回的returnValue是更新以前的nextFreeTicketMicros,(若是當前請求時間 小於 此時的nextFreeTicketMicros,那這個nextFreeTicketMicros是在上一個請求處理獲得的值)!!
      2. 若是充足,則直接處理令牌總量 storedPermits = 原storedPermits  - 需求量requiredPermits
      3. 若是不充足,
        1. 更新nextFreeTicketMicros 的值爲 原 nextFreeTicketMicros  + 生成差額令牌須要的時間
        2. 更新令牌總量 storedPermits = 0;
      依靠SleepingStopwatch來休眠(其實是TimeUnit.sleep)指定時長。

SmoothWarmingUp

  • 最大數則是由初始化時預熱時長warmupPeriod 除以間隔時間stableIntervalMicros的值,初始化時預熱令牌
  • 冷啓動時會以一個比較大的速率慢慢到平均速率,而後趨於平均速率(梯形降低到平均速率)。 warmupPeriod * TimeUnit 越大,上升速率越平滑

區別

  1. doSetRate: 在設置初始值時不一樣。
  2. 處理等待時間的計算規則上不一樣:
  • 穩定模式SmoothBursty : 
    waitMicros = 差額 * 間隔時長 
    調用時將容許必定時間的突發,取決於: 上一次訪問距當前訪問時間內生成的不超過最大容量的令牌數
  • 漸進模式SmoothWarmingUp:
    waitMicros  =  差額 * 間隔時長 + 對slope的處理即便差額爲0,也須要與slope進行計算,會產生一個浮動值。
    冷啓動時會以一個比較大的速率慢慢到平均速率,而後趨於平均速率

模式驗證

SmoothBursty容許必定程度的突發

package com.noob;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Stopwatch;
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterTest {
	public static void main(String args[]) throws Exception {

		RateLimiterWrapper limiter = new RateLimiterWrapper(
				RateLimiter.create(2));

		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		System.out.println("Thread.sleep 2s");
		try {
			Thread.sleep(2000L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();

	}

	static class RateLimiterWrapper {

		private RateLimiter limiter;
		private Stopwatch stopwatch;
		private String simpleName;
		private Field storedPermits;
		private Field nextFreeTicketMicros;

		public RateLimiterWrapper(RateLimiter limiter) throws Exception {
			this.limiter = limiter;

			Class<?> clas = limiter.getClass();
			simpleName = clas.getSimpleName();
			storedPermits = clas.getSuperclass().getDeclaredField(
					"storedPermits");
			storedPermits.setAccessible(true);
			nextFreeTicketMicros = clas.getSuperclass().getDeclaredField(
					"nextFreeTicketMicros");
			nextFreeTicketMicros.setAccessible(true);
			Field stopwatchField = clas.getSuperclass().getSuperclass()
					.getDeclaredField("stopwatch");
			stopwatchField.setAccessible(true);
			Object sleepingStopwatch = stopwatchField.get(limiter);
			Field stopwatchFiled = (sleepingStopwatch.getClass()
					.getDeclaredField("stopwatch"));
			stopwatchFiled.setAccessible(true);
			stopwatch = (Stopwatch) stopwatchFiled.get(sleepingStopwatch);
			System.out
					.println(String
							.format("%s -> 初始化階段:  init-storedPermits: %s, init-nextFreeTicketMicros: %s",
									simpleName, 
									storedPermits.get(limiter),
									nextFreeTicketMicros.get(limiter)));
		}

		long readMicros() {
			return stopwatch.elapsed(TimeUnit.MICROSECONDS);
		}

		double acquire() throws Exception {
			long reqTimeMirco = readMicros();
			Object beforeStoredPermits = storedPermits.get(this.limiter);
			Object beforeNextFreeTicketMicros = nextFreeTicketMicros
					.get(this.limiter);

			double waitMirco = this.limiter.acquire();

			Object afterStoredPermits = storedPermits.get(this.limiter);
			Object afterNextFreeTicketMicros = nextFreeTicketMicros
					.get(this.limiter);

			System.out
					.println(String
							.format("reqTimeMirco: %s, before-storedPermits: %s, before-nextFreeTicketMicros: %s, waitSeconds: %ss, after-storedPermits: %s, after-nextFreeTicketMicros: %s",
									reqTimeMirco, convert(beforeStoredPermits),
									beforeNextFreeTicketMicros,
									convert(waitMirco),
									convert(afterStoredPermits),
									afterNextFreeTicketMicros));
			return waitMirco;

		}
	}

	public static BigDecimal convert(Object o) {
		return new BigDecimal(String.valueOf(o)).setScale(4,
				RoundingMode.HALF_UP);
	}
}

執行結果


此處能夠看到設置的桶容量爲2(即容許的突發量),這是由於SmoothBursty中有一個參數:最大突發秒數(maxBurstSeconds)默認值是1s,突發量(桶容量)=速率*maxBurstSeconds,因此本示例 桶容量(突發量)爲2。安全

發現: 在線程等待後,是第四個請求才開始有等待併發

處理過程

  1. resync方法中:
    由於 請求時間reqTimeMirco >  before-nextFreeTicketMicros, 因此計算得出:
    1. 截止到這次請求時間點令牌總數 = 原剩餘令牌數 + 可以生成的令牌數 (reqTimeMirco - before-nextFreeTicketMicros) /  單個令牌生成時間stableIntervalMicros ), 不超過maxPermits。
    2. nextFreeTicketMicros 被替換成了reqTimeMirco。
  2.  reserveEarliestAvailable方法中: 接着更新  nextFreeTicketMicros = before-nextFreeTicketMicros  + 差額令牌數 ( 需求量 - 令牌總數) * 單個令牌生成時間stableIntervalMicro

因此按這個邏輯:
此時的stableIntervalMicros = 500000;app

  1. 第一次請求中: 高併發

    1. 雖然 before-nextFreeTicketMicros = 745,但在reserveEarliestAvailable方法中返回的 nextFreeTicketMicros 的值就是reqTimeMirco值19298 ,因此與請求時間比較得到的等待時間waitSeconds = 0s。工具

    2. 差額令牌時間 = (需求量 1 - (請求時間 19298 - 原令牌可供時間 745)/ 單個令牌生成時間 500000) * 單個令牌生成時間 500000  = 481447;  因此更新以後的nextFreeTicketMicros  = 481447+ 19298 =500745 !ui

  2. 第二次請求:

    1. reqTimeMirco  < before-nextFreeTicketMicros,因此不計算可以生成令牌數量。 直接比較等待時間 waitSeconds  = 500745 - 21164= 479581= 0.4796s;

    2. 更新以後的nextFreeTicketMicros  = 500745 + 1 * 500000 = 1000745 !

SmoothWarmingUp 速率平滑

由於SmoothBursty容許必定程度的突發,會擔憂若是容許這種突發,假設忽然間來了很大的流量,那麼系統極可能扛不住這種突發。所以須要一種平滑速率的限流工具,從而系統冷啓動後慢慢的趨於平均固定速率(即剛開始速率小一些,而後慢慢趨於設置的固定速率)

public static void main(String args[]) throws Exception {
		RateLimiterWrapper limiter = new RateLimiterWrapper(RateLimiter.create(
				5, 1, TimeUnit.SECONDS));
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
		limiter.acquire();
	}

執行結果1

執行結果2

RateLimiterWrapper limiter = new RateLimiterWrapper(RateLimiter.create(5, 3, TimeUnit.SECONDS));

經常使用限流算法

漏桶算法

思路: 水(請求)先進入到漏桶裏,漏桶以必定的速度出水,當水流入速度過大會直接溢出,能夠看出漏桶算法能強行限制數據的傳輸速率。(相似於隊列,控制出隊速率)

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

令牌桶算法

系統會以一個恆定的速度往桶裏放入令牌,而若是請求須要被處理,則須要先從桶裏獲取一個令牌,當桶裏沒有令牌可取時,則拒絕服務。

相關文章
相關標籤/搜索