http://go12345.iteye.com/blog/1744728
流量預警和限流方案中,比較常用的有兩種。第一種滑窗模式,通過統計一段時間內的訪問次數來進行控制,訪問次數達到的某個峯值時進行限流。第二種爲併發用戶數模式,通過控制最大併發用戶數,來達到流量控制的目的。下面來簡單分析下兩種的優缺點。
1、滑窗模式
模式分析:
在每次有訪問進來時,我們判斷前N個單位時間內的總訪問量是否超過了設置的閾值,並對當前時間片上的請求數+1。
上圖每一個格式表示一個固定的時間(比如1s),每個格子一個計數器,我們要獲取前5s的請求量,就是對當前時間片i ~ i-4的時間片上計數器進行累加。
這種模式的實現的方式更加契合流控的本質意義。理解較爲簡單。但由於訪問量的不可預見性,會發生單位時間的前半段大量請求涌入,而後半段則拒絕所有請求的情況。(通常,需要可以將單位時間切的足夠的小來緩解 )其次,我們很難確定這個閾值設置在多少比較合適,只能通過經驗或者模擬(如壓測)來進行估計,即使是壓測也很難估計的準確。集羣部署中每臺機器的硬件參數不同,可能導致我們需要對每臺機器的閾值設置的都不盡相同。同一臺機子在不同的時間點的系統壓力也不一樣(比如晚上還有一些任務,或其他的一些業務操作的影響),能夠承受的最大閾值也不盡相同,我們無法考慮的周全。
所以滑窗模式通常適用於對某一資源的保護的需求上(或者說是承諾比較合適:我對某一接口的提供者承諾過,最高調用量不超過XX),如對db的保護,對某一服務的調用的控制上。
代碼實現思路:
每一個時間片(單位時間)就是一個獨立的計數器,用以數組保存。將當前時間以某種方式(比如取模)映射到數組的一項中。每次訪問先對當前時間片上的計數器+1,再計算前N個時間片的訪問量總合,超過閾值則限流。
- /**
- * 滑窗的實現
- * @author shimig
- *
- */
- public class SlidingWindow {
- /* 循環隊列 */
- private volatile AtomicInteger[] timeSlices;
- /* 隊列的總長度 */
- private volatile int timeSliceSize;
- /* 每個時間片的時長 */
- private volatile int timeMillisPerSlice;
- /* 窗口長度 */
- private volatile int windowSize;
- /* 當前所使用的時間片位置 */
- private AtomicInteger cursor = new AtomicInteger(0);
- public SlidingWindow(int timeMillisPerSlice, int windowSize) {
- this.timeMillisPerSlice = timeMillisPerSlice;
- this.windowSize = windowSize;
- // 保證存儲在至少兩個window
- this.timeSliceSize = windowSize * 2 + 1;
- }
- /**
- * 初始化隊列,由於此初始化會申請一些內容空間,爲了節省空間,延遲初始化
- */
- private void initTimeSlices() {
- if (timeSlices != null) {
- return;
- }
- // 在多線程的情況下,會出現多次初始化的情況,沒關係
- // 我們只需要保證,獲取到的值一定是一個穩定的,所有這裏使用先初始化,最後賦值的方法
- AtomicInteger[] localTimeSlices = new AtomicInteger[timeSliceSize];
- for (int i = 0; i < timeSliceSize; i++) {
- localTimeSlices[i] = new AtomicInteger(0);
- }
- timeSlices = localTimeSlices;
- }
- private int locationIndex() {
- long time = System.currentTimeMillis();
- return (int) ((time / timeMillisPerSlice) % timeSliceSize);
- }
- /**
- * <p>對時間片計數+1,並返回窗口中所有的計數總和
- * <p>該方法只要調用就一定會對某個時間片進行+1
- *
- * @return
- */
- public int incrementAndSum() {
- initTimeSlices();
- int index = locationIndex();
- int sum = 0;
- // cursor等於index,返回true
- // cursor不等於index,返回false,並會將cursor設置爲index
- int oldCursor = cursor.getAndSet(index);
- if (oldCursor == index) {
- // 在當前時間片裏繼續+1
- sum += timeSlices[index].incrementAndGet();
- } else {
- // 可能有其他thread已經置過1,問題不大
- timeSlices[index].set(1);
- // 清零,訪問量不大時會有時間片跳躍的情況
- clearBetween(oldCursor, index);
- // sum += 0;
- }
- for (int i = 1; i < windowSize; i++) {
- sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
- }
- return sum;
- }
- /**
- * 判斷是否允許進行訪問,未超過閾值的話纔會對某個時間片+1
- *
- * @param threshold
- * @return
- */
- public boolean allow(int threshold) {
- initTimeSlices();
- int index = locationIndex();
- int sum = 0;
- // cursor不等於index,將cursor設置爲index
- int oldCursor = cursor.getAndSet(index);
- if (oldCursor != index) {
- // 可能有其他thread已經置過1,問題不大
- timeSlices[index].set(0);
- // 清零,訪問量不大時會有時間片跳躍的情況
- clearBetween(oldCursor, index);
- }
- for (int i = 1; i < windowSize; i++) {
- sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
- }
- // 閾值判斷
- if (sum <= threshold) {
- // 未超過閾值才+1
- sum += timeSlices[index].incrementAndGet();
- return true;
- }
- return false;
- }
- /**
- * <p>將fromIndex~toIndex之間的時間片計數都清零
- * <p>極端情況下,當循環隊列已經走了超過1個timeSliceSize以上,這裏的清零並不能如期望的進行
- *
- * @param fromIndex 不包含
- * @param toIndex 不包含
- */
- private void clearBetween(int fromIndex, int toIndex) {
- for (int index = (fromIndex + 1) % timeSliceSize; index != toIndex; index = (index + 1) % timeSliceSize) {
- timeSlices[index].set(0);
- }
- }
- }
2、併發用戶數模式
模式分析:
每次操作執行時,我們通過判斷當前正在執行的訪問數是否超過某個閾值在決定是否限流。
該模式看着思路比較的另類,但卻有其獨到之處。實際上我們限流的根本是爲了保護資源,防止系統接受的請求過多,應接不暇,拖慢系統中其他接口的服務,造成雪崩。我們真正需要關心的是那些運行中的請求,而那些已經完成的請求已是過去時,不再是需要關心的了。
我們來看看其閾值的計算方式,對於一個請求來說,響應時間rt、qps是一個比較容易獲取的參數,那麼我們這樣計算:qps/1000*rt。
此外,一個應用往往是個複雜的系統,提供的服務或者暴露的請求、資源不止一個。內部GC、定時任務的執行、其他服務訪問的驟增,外部依賴方、db的抖動,抑或是代碼中不經意間的一個bug。都可能導致響應時間的變化,導致系統性能容量的改變 。而這種模式,則能恰如其分的自動做出調整,當系統不適時,rt增加,會自動的對qps做出適應。
代碼實現思路:
當訪問開始時,我們對當前計數器(原子計數器)+1,當完成時,-1。該計數器即爲當前正在執行的請求數。只需判斷這個計數器是否超過閾值即可。