限流系列:resilience4j-ratelimiter
目录
令牌桶算法
Resilience4j-Ratelimiter
数据模型
示例
突刺问题
大致流程
decorateCheckedSupplier
waitForPermission
acquirePermission
updateStateWithBackOff
calculateNextState
令牌桶算法
令牌桶算法就是以固定速率生成令牌放入桶中,每个请求都需要从桶中获取令牌,没有获取到令牌的请求会被阻塞限流(桶中的令牌不够的时候);只要能从桶里取出令牌就能通过。
当令牌消耗速度小于生成的速度时,令牌桶内就会预存这些未消耗的令牌(直到桶的上限);当有突发流量进来时,可以直接从桶中取出令牌,而不会被限流,从而支持突发流量的快速处理。
Resilience4j-Ratelimiter
不管是令牌桶算法还是漏桶算法都可以用延迟计算的方式来实现,延迟计算指的是不需要单独的线程来定时生成令牌或者从漏桶中定时取出请求,而是由调用限流器的线程自己去计算是否有足够的令牌以及需要sleep的时间,延迟计算的方式可以节省一个线程资源。
resilience4j-ratelimiter实现允许突发流量的方式是在每个周期开始时补充全部的令牌(这是与guava rateLimiter差别最大的地方,详见《限流系列:guava rateLimiter》),使得在周期开始时可以处理最多limitForPeriod个请求,形成突发。
同时需要注意,与传统的令牌桶算法不同,这里的令牌不会累积,而是每个周期重置,因此突发流量的大小受限于limitForPeriod。而传统令牌桶可能允许累积未使用的令牌,从而支持更大的突发。
数据模型
字段 | 名称 | 作用 |
limitForPeriod | 在每个周期(period)内允许的最大请求数量。 | 控制在指定时间窗口内的请求上限,防止短时间内请求过多。 |
limitRefreshPeriod | 时间窗口(周期)的长度,即每隔多长时间重置一次请求计数。 | 定义了速率限制的周期,每个周期开始时,请求计数会被重置为 0。 |
示例
|
在这里,配置了:
limitForPeriod = 10
limitRefreshPeriod = 1秒
这意味着:
每秒(limitRefreshPeriod)最多可以处理 10 个请求(limitForPeriod)。
每过 1 秒,请求计数器会重置,新的 1 秒周期内又可以处理最多 10 个请求。
突刺问题
综上所述,resilience4j-ratelimiter 会存在突刺问题,尤其是在使用固定窗口策略时。通过采用滑动窗口策略,可以有效减轻突刺问题的影响,实现更平滑的速率限制。
使用滑动窗口策略配置示例如下:
slidingWindow: enabled: true intervalInMilliseconds: 1000 // 滑动窗口的总时间间隔为 1 秒 numberOfBuckets: 10 // 将 1 秒分成 10 个桶,每个桶代表 100 毫秒 |
这样,速率限制会更加平滑,每个 100 毫秒内的请求数量会被单独统计,减少了突刺的影响。
接下来我们探究一下使用固定窗口策略时的代码处理流程,采用滑动窗口策略的探究等下一次再进行。
使用固定窗口策略时的代码处理流程如下所示:
大致流程
点击示例里的decorateCheckedSupplier()方法,如下所示:
decorateCheckedSupplier
static <T> CheckedFunction0<T> decorateCheckedSupplier(RateLimiter rateLimiter, CheckedFunction0<T> supplier) {
return decorateCheckedSupplier(rateLimiter, 1, supplier);
}
static <T> CheckedFunction0<T> decorateCheckedSupplier(RateLimiter rateLimiter, int permits,
CheckedFunction0<T> supplier) {
return () -> {
waitForPermission(rateLimiter, permits);
try {
T result = supplier.apply();
rateLimiter.onResult(result);
return result;
} catch (Exception exception) {
rateLimiter.onError(exception);
throw exception;
}
};
}
点击waitForPermission()方法,如下所示:
waitForPermission
static void waitForPermission(final RateLimiter rateLimiter, int permits) {
boolean permission = rateLimiter.acquirePermission(permits);
if (Thread.currentThread().isInterrupted()) {
throw new AcquirePermissionCancelledException();
}
if (!permission) {
throw RequestNotPermitted.createRequestNotPermitted(rateLimiter);
}
}
点击acquirePermission()方法,如下所示:
acquirePermission
public boolean acquirePermission(final int permits) {
long timeoutInNanos = state.get().config.getTimeoutDuration().toNanos();
State modifiedState = updateStateWithBackOff(permits, timeoutInNanos);
boolean result = waitForPermissionIfNecessary(timeoutInNanos, modifiedState.nanosToWait);
publishRateLimiterAcquisitionEvent(result, permits);
return result;
}
点击updateStateWithBackOff()方法,如下所示:
updateStateWithBackOff
private State updateStateWithBackOff(final int permits, final long timeoutInNanos) {
AtomicRateLimiter.State prev;
AtomicRateLimiter.State next;
do {
prev = state.get();
next = calculateNextState(permits, timeoutInNanos, prev);
} while (!compareAndSet(prev, next));
return next;
}
点击calculateNextState()方法,如下所示:
calculateNextState
|
在这里:
- 获取配置信息:时间窗口的长度
- 获取配置信息:每个周期内允许的最大请求数量permissionsPerCycle
- 计算了自限流器AtomicRateLimiter实例化以来经过的时长currentNanos,即当前时间减去限流器AtomicRateLimiter实例化的时间
- 计算了当前时间所属周期currentCycle
- 获取上一个请求所属周期activeCycle
- 获取上一个请求令牌数量activePermissions
- 计算当前周期与上个请求所属周期的差值
- 计算上个请求所属周期到当前周期积累的令牌数量
- 对比积累的令牌数量与每个周期内允许的最大请求数量,将比较小的值作为当前周期的令牌数量。