由 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:不预热令牌
- 初始化实例SleepingStopwatch;
- 确定好每个令牌在每秒生成的间隔时间stableIntervalMicros;
- 确定好令牌最大数量maxPermits;(SmoothBursty 模式下默认是1s的量)
- 确定好下一个令牌可供给时间nextFreeTicketMicros; 执行setRate方法时最终执行到方法resync初始化值为【创建RateLimiter时的SleepingStopwatch.readMicros()】;
- 当前存储令牌数为0。
请求执行acquire方法后进入方法 reserveEarliestAvailable :
- 先执行了 resync 方法来判定更新库存令牌storedPermits 及令牌可获取时间nextFreeTicketMicros。
若请求时间比此时nextFreeTicketMicros大, 更新令牌总量storedPermits 累加这段时间间隔内会生成的令牌; 并更新nextFreeTicketMicros 为当前请求时间 - 回到 reserveEarliestAvailable 方法。 计算所需要的令牌是否充足
-
这里返回的returnValue是 执行resync方法后的 nextFreeTicketMicros , 还没有执行 处理差额令牌的逻辑。 所以: 如果在执行 resync 方法时判定 【当前请求时间 小于 此时的nextFreeTicketMicros】, 那此处的这个nextFreeTicketMicros是在上一个请求处理得到的值
- 如果充足,则仅仅是更新库存令牌总量storedPermits = 原storedPermits - 需求量requiredPermits
- 如果不充足,
- 更新nextFreeTicketMicros 的值为 原nextFreeTicketMicros + 差额令牌生成需要的时间;
- 更新令牌总量 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, 所以计算得出:
- 截止到此次请求时间点令牌总数 = 原剩余令牌数 + 能够生成的令牌数 (reqTimeMirco - before-nextFreeTicketMicros) / 单个令牌生成时间stableIntervalMicros ), 不超过maxPermits。
- nextFreeTicketMicros 被替换成了reqTimeMirco。
- 接着在reserveEarliestAvailable方法内执行完resync方法后: 接着更新 nextFreeTicketMicros = before-nextFreeTicketMicros + 差额令牌数 ( 需求量 - 令牌总数) * 单个令牌生成时间stableIntervalMicro
所以按这个逻辑:
-
第一次请求
-
虽然 before-nextFreeTicketMicros = 745,但在reserveEarliestAvailable方法中返回的 returnValue的值就是reqTimeMirco值19298 ,所以与请求时间比较获得的等待时间waitSeconds = 0s。
-
差额令牌时间 = (需求量 1 - (请求时间 19298 - 原令牌可供时间 745)/ 单个令牌生成时间 500000) * 单个令牌生成时间 500000 = 481447; 所以更新之后的nextFreeTicketMicros = 481447+ 19298 =500745 !
-
-
第二次请求
-
reqTimeMirco < before-nextFreeTicketMicros,所以不计算能够生成令牌数量。 直接比较等待时间 waitSeconds = 500745 - 21164= 479581= 0.4796s;
-
更新之后的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
来源:oschina
链接:https://my.oschina.net/u/3434392/blog/4513385