RateLimiter具体实现

佐手、 提交于 2020-08-19 17:24:43

由 Guava 提供的单机版本基于令牌桶算法 的限流工具。

通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费)

两种模式: 

  • SmoothBursty(稳定模式):  平滑突发限流
  • SmoothWarmingUp(渐进模式): 平滑预热限流。
    关键在于其分发令牌的速率会随库存令牌数而改变。等存储令牌数从maxPermits降低到thresholdPermits时,发放令牌的时间也由coldInterval降低到了正常的stableInterval。也就是说消耗令牌越快时,生成令牌也越快。
    表现形式如下图所示:

特点:

  • 本次请求 当库存令牌不足以满足本次需求时, ( 差额令牌的创建时间 + 可获取令牌时间 ) - 当前时间 = 请求阻塞时间,但下一次请求才是真正按这个时间来阻塞等待!
  • 构造函数里有一个参数RateLimiter&SleepingStopwatch最终执行java.util.concurrent.TimeUnit.sleep方法控制请求阻塞指定市场。
  • 在核心方法中使用synchronized 加锁来控制并发。

成员变量

  • storedPermits:  当前存储令牌数
  • maxPermits: 最大存储令牌数
  • stableIntervalMicros:生成单个令牌需要时间。 eg. 比如说是每秒5个,那间隔时间200ms
  • nextFreeTicketMicros :请求可以获取令牌的时间 。 (这个时间的更新直接影响到阻塞时间)

SmoothBursty

假定每秒速率1,每次需要令牌1:

初始化时, nextFreeTicketMicros 赋值为 初始化对象时间; 此时库存令牌数 storedPermits 为 0 。

请求处理过程:

首先需判定:若当前请求时间 > old_nextFreeTicketMicros ,当前库存令牌数storedPermits 增加 截止当前请求时间生成的令牌数,  库存不超过最大容量;  nextFreeTicketMicros 赋值为 当前请求时间。

首次请求: 预先消费令牌后直接通过。 更新  new_nextFreeTicketMicros = old_nextFreeTicketMicros + (差额令牌生成时间 - (首次请求时间 - old_nextFreeTicketMicros ) ) , 更新剩余库存令牌数;
并发第二个请求: 接收到old_nextFreeTicketMicros 值与当前请求时间做阻塞时长计算。 以old_nextFreeTicketMicros 为 时间起点,再计算 new_nextFreeTicketMicros = old_nextFreeTicketMicros + 差额令牌生成时间;库存令牌数清零

执行初始化方法create不预热令牌

  1. 初始化实例SleepingStopwatch
  2. 确定好每个令牌在每秒生成的间隔时间stableIntervalMicros
  3. 确定好令牌最大数量maxPermits(SmoothBursty 模式下默认是1s的量)
  4. 确定好下一个令牌可供给时间nextFreeTicketMicros;  执行setRate方法时最终执行到方法resync初始化值为【创建RateLimiter时的SleepingStopwatch.readMicros()】
  5. 当前存储令牌数为0。

请求执行acquire方法后进入方法 reserveEarliestAvailable

  1. 先执行了 resync 方法来判定更新库存令牌storedPermits 及令牌可获取时间nextFreeTicketMicros
    若请求时间比此时nextFreeTicketMicros大, 更新令牌总量storedPermits 累加这段时间间隔内会生成的令牌; 并更新nextFreeTicketMicros 为当前请求时间
  2. 回到 reserveEarliestAvailable 方法。 计算所需要的令牌是否充足
    1. 这里返回的returnValue是 执行resync方法后的 nextFreeTicketMicros , 还没有执行 处理差额令牌的逻辑。 所以: 如果在执行 resync 方法时判定 【当前请求时间 小于 此时的nextFreeTicketMicros】, 那此处的这个nextFreeTicketMicros是在上一个请求处理得到的值
    2. 如果充足,则仅仅是更新库存令牌总量storedPermits = 原storedPermits  - 需求量requiredPermits
    3. 如果不充足,
      1. 更新nextFreeTicketMicros 的值为 原nextFreeTicketMicros  + 差额令牌生成需要的时间
      2. 更新令牌总量 storedPermits = 0;

最后SleepingStopwatch来休眠指定时长。

使用场景

要注意的是,需要考虑初始化的时机 (eg. 针对于API 是在应用启动时就预先初始化好限流器 还是 推迟到首次请求时创建) 。

假设刚初始化就同一时刻有大量并发请求时, 造成tryAcquire返回访问拒绝, acquire 则会持续阻塞。此时就没法预支令牌,系统在这一瞬间处理量不会升起来。当然,也需要注意SmoothBursty模式下的一定程度突发造成的峰值(1s的生成量+1)问题。

	public static void main(String[] args) {
		RateLimiter a = RateLimiter.create(2);
		new Thread(() -> {
			System.out.println(System.currentTimeMillis() + "----1----: " + a.tryAcquire());
		}).start();
		new Thread(() -> {
			System.out.println(System.currentTimeMillis() + "----2----: " + a.tryAcquire());
		}).start();
		new Thread(() -> {
			System.out.println(System.currentTimeMillis() + "----3----: " + a.tryAcquire());
		}).start();
		new Thread(() -> {
			System.out.println(System.currentTimeMillis() + "----4----: " + a.tryAcquire());
		}).start();
		new Thread(() -> {
			System.out.println(System.currentTimeMillis() + "----5----: " + a.tryAcquire());
		}).start();
	}

测试用例里,只有1个能通过!不支持突发。

SmoothWarmingUp

public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
    checkArgument(warmupPeriod >= 0, "warmupPeriod must not be negative: %s", warmupPeriod);
    return create(
        permitsPerSecond, warmupPeriod, unit, 3.0, SleepingStopwatch.createFromSystemTimer());
  }

  • 初始化时预热令牌。等于warmupPeriod 除以间隔时间stableIntervalMicros
  • 冷启动时会以一个比较大的速率慢慢到平均速率,然后趋于平均速率(梯形下降到平均速率)。 warmupPeriod * TimeUnit 越大,上升速率越平滑

两种模式的实现差异

  • doSetRate  设置初始值时不同。
    

  • reserveEarliestAvailable   计算的等待时间逻辑不同
    

SmoothBursty稳定模式下:  waitMicros = 差额令牌 * 间隔时长 。 

SmoothWarmingUp渐进模式下:waitMicros  =  差额 * 间隔时长 + 对slope的处理即使差额为0,也需要与slope进行计算,会产生一个浮动值。

验证

package com.noob.limit;

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 {

		// testSmoothBursty();
		 testSmoothWarmingUp();

	}

	private static void testSmoothWarmingUp() 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();

	}

	private static void testSmoothBursty() 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;

		}

		void acquireAsync() {
			new Thread(() -> {
				try {
					acquire();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}).start();
		}

	}

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

SmoothBursty允许一定程度的突发

执行结果

可以看到设置的桶容量为2(即允许的突发量),这是因为SmoothBursty中有一个参数:最大突发秒数(maxBurstSeconds)默认值是1s,突发量(桶容量)= 速率 * maxBurstSeconds,所以本示例 桶容量(突发量)为2。

在线程等待后,是第四个请求才开始有等待!

处理过程如下:

此时的stableIntervalMicros = 500000;

  • 首先在resync方法中: 因为  请求时间 reqTimeMirco >  before-nextFreeTicketMicros, 所以计算得出:
    1. 截止到此次请求时间点令牌总数 = 原剩余令牌数 + 能够生成的令牌数 (reqTimeMirco - before-nextFreeTicketMicros) /  单个令牌生成时间stableIntervalMicros ), 不超过maxPermits。
    2. nextFreeTicketMicros 被替换成了reqTimeMirco。
  • 接着reserveEarliestAvailable方法内执行完resync方法后: 接着更新  nextFreeTicketMicros before-nextFreeTicketMicros  + 差额令牌数 ( 需求量 - 令牌总数) * 单个令牌生成时间stableIntervalMicro

所以按这个逻辑:

  1. 第一次请求

    1. 虽然 before-nextFreeTicketMicros = 745,但在reserveEarliestAvailable方法中返回的 returnValue的值就是reqTimeMirco值19298 ,所以与请求时间比较获得的等待时间waitSeconds = 0s。

    2. 差额令牌时间 = (需求量 1 - (请求时间 19298 - 原令牌可供时间 745)/ 单个令牌生成时间 500000) * 单个令牌生成时间 500000  = 481447;  所以更新之后的nextFreeTicketMicros  = 481447+ 19298 =500745 !

  2. 第二次请求

    1. reqTimeMirco  < before-nextFreeTicketMicros,所以不计算能够生成令牌数量。 直接比较等待时间 waitSeconds  = 500745 - 21164= 479581= 0.4796s;

    2. 更新之后的nextFreeTicketMicros  500745 + 1 * 500000 = 1000745 !

SmoothWarmingUp 速率平滑

允许一定程度的突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。因此需要一种平滑速率的限流,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于设置的固定速率)

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

执行结果: 等待时长越来越短

SmoothWarmingUp -> 初始化阶段:  init-storedPermits: 5.0, init-nextFreeTicketMicros: 161
reqTimeMirco: 17591, before-storedPermits: 5.0000, before-nextFreeTicketMicros: 161, waitSeconds: 0.0000s, after-storedPermits: 4.0000, after-nextFreeTicketMicros: 537622
reqTimeMirco: 22915, before-storedPermits: 4.0000, before-nextFreeTicketMicros: 537622, waitSeconds: 0.5147s, after-storedPermits: 3.0000, after-nextFreeTicketMicros: 897622
reqTimeMirco: 541985, before-storedPermits: 3.0000, before-nextFreeTicketMicros: 897622, waitSeconds: 0.3556s, after-storedPermits: 2.0000, after-nextFreeTicketMicros: 1117622
reqTimeMirco: 898307, before-storedPermits: 2.0000, before-nextFreeTicketMicros: 1117622, waitSeconds: 0.2193s, after-storedPermits: 1.0000, after-nextFreeTicketMicros: 1317622
reqTimeMirco: 1117588, before-storedPermits: 1.0000, before-nextFreeTicketMicros: 1317622, waitSeconds: 0.2000s, after-storedPermits: 0.0000, after-nextFreeTicketMicros: 1517622
reqTimeMirco: 1317815, before-storedPermits: 0.0000, before-nextFreeTicketMicros: 1517622, waitSeconds: 0.1998s, after-storedPermits: 0.0000, after-nextFreeTicketMicros: 1717622
reqTimeMirco: 1518300, before-storedPermits: 0.0000, before-nextFreeTicketMicros: 1717622, waitSeconds: 0.1993s, after-storedPermits: 0.0000, after-nextFreeTicketMicros: 1917622
  • RateLimiterWrapper limiter = new RateLimiterWrapper(RateLimiter.create(5, 3, TimeUnit.SECONDS));
    

执行结果: 等待间隔变化更加平滑

SmoothWarmingUp -> 初始化阶段:  init-storedPermits: 15.0, init-nextFreeTicketMicros: 220
reqTimeMirco: 24130, before-storedPermits: 15.0000, before-nextFreeTicketMicros: 220, waitSeconds: 0.0000s, after-storedPermits: 14.0000, after-nextFreeTicketMicros: 597507
reqTimeMirco: 35770, before-storedPermits: 14.0000, before-nextFreeTicketMicros: 597507, waitSeconds: 0.5617s, after-storedPermits: 13.0000, after-nextFreeTicketMicros: 1117507
reqTimeMirco: 603176, before-storedPermits: 13.0000, before-nextFreeTicketMicros: 1117507, waitSeconds: 0.5143s, after-storedPermits: 12.0000, after-nextFreeTicketMicros: 1584173
reqTimeMirco: 1118582, before-storedPermits: 12.0000, before-nextFreeTicketMicros: 1584173, waitSeconds: 0.4656s, after-storedPermits: 11.0000, after-nextFreeTicketMicros: 1997506
reqTimeMirco: 1585241, before-storedPermits: 11.0000, before-nextFreeTicketMicros: 1997506, waitSeconds: 0.4123s, after-storedPermits: 10.0000, after-nextFreeTicketMicros: 2357506
reqTimeMirco: 1998069, before-storedPermits: 10.0000, before-nextFreeTicketMicros: 2357506, waitSeconds: 0.3594s, after-storedPermits: 9.0000, after-nextFreeTicketMicros: 2664172
reqTimeMirco: 2358021, before-storedPermits: 9.0000, before-nextFreeTicketMicros: 2664172, waitSeconds: 0.3061s, after-storedPermits: 8.0000, after-nextFreeTicketMicros: 2917505
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!