赞
踩
目前主流的限流算法:令牌、漏桶、滑动窗口。Nginx都实现了漏桶算法,Springcloud Gateway和Guava Ratelimiter实现了令牌桶,阿里的 Sentinel实现了滑动窗口。
固定窗口可能遇到的问题
限流不均匀
两倍的配置速率问题
假如限流设置为:
1秒钟1000个请求,在 第一秒的最后100ms,以及 第二秒 最开始100ms,都收到1000次请求。就会出现在这个 200ms 的周期中收到 2000次请求,并且限流通过,这就是 两倍的配置速率问题。
滑动窗口为固定窗口的改良版,解决了固定窗口在窗口切换时会受到两倍于阈值数量的请求,滑动窗口在固定窗口的基础上,将一个窗口分为若干个等份的小窗口,每个小窗口对应不同的时间点,拥有独立的计数器,当请求的时间点大于当前窗口的最大时间点时,则将窗口向前平移一个小窗口(将第一个小窗口的数据舍弃,第二个小窗口变成第一个小窗口,当前请求放在最后一个小窗口),整个窗口的所有请求数相加不能大于阀值。
系统自适应限流 —— 过载保护。定义自适应限流规则需要提供多个参数
- List<SystemRule> rules = new ArrayList<SystemRule>();
- SystemRule rule = new SystemRule();
- rule.setHighestSystemLoad(3.0);
- rule.setAvgRt(10);
- rule.setQps(20);
- rule.setMaxThread(10);
- rules.add(rule);
- SystemRuleManager.loadRules(Collections.singletonList(rule));
从代码中也可以看出系统自适应限流规则不需要定义资源名称,因为它是全局的规则,会自动应用到所有的临界区。如果当负载超标时,所有临界区资源将一起勒紧裤腰带渡过难关。
(1)检查系统指标,qps、thread、rt、load、cpu
定时任务会每秒检查自身系统状况。
- public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
- if (resourceWrapper == null) {
- return;
- }
- // Ensure the checking switch is on.
- // 只有配置了系统自适应限流规则才能进入,SystemRuleManager.loadRules去加载
- if (!checkSystemStatus.get()) {
- return;
- }
-
- // for inbound traffic only
- // 只有入口流量才能进入
- if (resourceWrapper.getEntryType() != EntryType.IN) {
- return;
- }
-
- // total qps
- double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
- if (currentQps > qps) {
- throw new SystemBlockException(resourceWrapper.getName(), "qps");
- }
-
- // total thread
- int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
- if (currentThread > maxThread) {
- throw new SystemBlockException(resourceWrapper.getName(), "thread");
- }
-
- double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
- if (rt > maxRt) {
- throw new SystemBlockException(resourceWrapper.getName(), "rt");
- }
-
- // load. BBR algorithm.
- if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
- if (!checkBbr(currentThread)) {
- throw new SystemBlockException(resourceWrapper.getName(), "load");
- }
- }
-
- // cpu usage
- if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
- throw new SystemBlockException(resourceWrapper.getName(), "cpu");
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
(2) 如何加载系统规则
- List<SystemRule> rules = new ArrayList<SystemRule>();
- SystemRule rule = new SystemRule();
- rule.setHighestSystemLoad(3.0);
- rule.setAvgRt(10);
- rule.setQps(20);
- rule.setMaxThread(10);
- rules.add(rule);
- SystemRuleManager.loadRules(Collections.singletonList(rule));
FlowSlot使用的核心思路是滑动窗口。
(1)定义窗口
- public class StatisticNode implements Node {
-
- /**
- * Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans
- * by given {@code sampleCount}.
- */
- private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
- IntervalProperty.INTERVAL);
-
- /**
- * 滚动计数器:保存最近60秒的统计信息。windowLengthInMs故意设置为1000毫秒,意思是每一个桶每秒,这样我们就可以得到每一秒的准确统计。
- */
- private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
-
- /**
- * 线程数
- */
- private LongAdder curThreadNum = new LongAdder();
-
- /**
- * 获取度量时的最后一个时间戳
- */
- private long lastFetchTime = -1;
- // ...
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
- public class ArrayMetric implements Metric {
-
- private final LeapArray<MetricBucket> data;
-
- public ArrayMetric(int sampleCount, int intervalInMs) {
- this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
- }
-
- public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
- if (enableOccupy) {
- this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
- } else {
- this.data = new BucketLeapArray(sampleCount, intervalInMs);
- }
- }
- // ...
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
要知道有多少个小窗口,在sentinel中也就是sampleCount,比如说我们有60个窗口。
intervalInMs是用来计算这个窗口长度的,intervalInMs/窗口数量= 窗口长度。也就是我给你1分钟,你给我分成60个窗口,这个时候窗口长度就是1s了,那如果我给你1s,你给我分2个窗口,这个时候窗口长度就是500毫秒了,这个1分钟,就是intervalInMs。
是否允许抢占,即当前时间戳已经达到限制后,是否可以占用下一个时间窗口的容量,这里对应 LeapArray 的两个实现类,如果允许抢占,则为 OccupiableBucketLeapArray,否则为 BucketLeapArray。
(2)判断是否触发限流标准
以qps为基准,qps的定义是每秒的流量,sentinel以滑动窗口作为核心,中qps的计算公式:每秒通过数量/每秒的间隔。本质上rollingCounterInSecond.pass()就代表了qps指标,因为默认的间隔为1s,只是窗口数量为2。
- public double passQps() {
- return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
- }
判断qps是否超标
- public boolean canPass(Node node, int acquireCount, boolean prioritized) {
- int curCount = avgUsedTokens(node);
- if (curCount + acquireCount > count) {
- // 流量等级,VIP通道问题
- if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
- long currentTime;
- long waitInMs;
- currentTime = TimeUtil.currentTimeMillis();
- waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
- if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
- node.addWaitingRequest(currentTime + waitInMs, acquireCount);
- node.addOccupiedPass(acquireCount);
- sleep(waitInMs);
-
- // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
- throw new PriorityWaitException(waitInMs);
- }
- }
- // 流量超标,直接拒绝
- return false;
- }
- return true;
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
(3)滑动窗口的设计
计算每秒的通过数量
- public long pass() {
- // 锁定当前时间所在的滑动窗口,为什么没有返回是因为滑动窗口的新建或者更新是需要锁资源的,当无法竞争到资源时需要等待,等待是会释放CPU控制权,而不释放锁
- data.currentWindow();
- long pass = 0;
- // 根据当前时间筛选未过期的滑动窗口,如何判定:当前时间-窗口开始时间>间隔时间
- List<MetricBucket> list = data.values();
-
- for (MetricBucket window : list) {
- pass += window.pass();
- }
- return pass;
- }
如何根据当前时间去锁定滑动窗口(CAS + 可重入锁)
- public WindowWrap<T> currentWindow(long timeMillis) {
- if (timeMillis < 0) {
- return null;
- }
- // 根据当前时间计算滑动窗口下标
- int idx = calculateTimeIdx(timeMillis);
- // 根据当前时间计算滑动窗口的开始时间
- long windowStart = calculateWindowStart(timeMillis);
- while (true) {
- WindowWrap<T> old = array.get(idx);
- if (old == null) {
- // CAS
- if (array.compareAndSet(idx, null, window)) {
- return window;
- } else {
- Thread.yield();
- }
- } else if (windowStart == old.windowStart()) {
- return old;
- } else if (windowStart > old.windowStart()) {
- if (updateLock.tryLock()) {
- try {
- return resetWindowTo(old, windowStart);
- } finally {
- updateLock.unlock();
- }
- } else {
- Thread.yield();
- }
- } else if (windowStart < old.windowStart()) {
- // 基本不可能到这里
- return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
- }
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
前面分析了如何统计qps,现在分析qps如何往上加
- public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
- boolean prioritized, Object... args) throws Throwable {
- try {
- // Do some checking.
- fireEntry(context, resourceWrapper, node, count, prioritized, args);
-
- // 计数
- node.increaseThreadNum();
- node.addPassRequest(count);
-
- if (context.getCurEntry().getOriginNode() != null) {
- // Add count for origin node.
- context.getCurEntry().getOriginNode().increaseThreadNum();
- context.getCurEntry().getOriginNode().addPassRequest(count);
- }
-
- if (resourceWrapper.getEntryType() == EntryType.IN) {
- // Add count for global inbound entry node for global statistics.
- Constants.ENTRY_NODE.increaseThreadNum();
- Constants.ENTRY_NODE.addPassRequest(count);
- }
-
- // Handle pass event with registered entry callback handlers.
- for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
- handler.onPass(context, resourceWrapper, node, count, args);
- }
- } catch (PriorityWaitException ex) {
- // ...
- } catch (Throwable e) {
- // ...
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
4.1 如何判断系统已经到达崩溃边缘,qps、thread、rt、load、cpu?
4.2 LongAdder和AtomicLong有什么区别?
并发场景下的number操作,都会选用java.util.concurrent.atomic包下的数据结构,比如典型的计数场景,而AtomicLong是其中一个,但是jdk1.8后Doug Lea大神又添加LongAdder和DoubleAdder,在大并发场景下性能会远高于AtomicLong。AtomicLong是以CAS保证原子性;而LongAdder是拆分锁的粒度,比如原先是多个线程争抢修改一个value的,变成多个线程争抢修改多个value。
4.3 根据当前时间筛选未过期的滑动窗口为什么不是:当前时间-窗口开始时间>窗口长度时间,而是大于间隔时间。按照样本数量为2计算,窗口长度时间为500ms,而间隔时间为1000ms?
A:pass数量是以整个窗口(包含N个滑动窗口)的长度为测量单位的,滑动窗口越多只能说明更精确。
4.4 固定窗口的两倍速率问题,滑动窗口也存在的吧,只是滑动窗口将大窗口拆细,来减少可能额外超出的量不至于太多。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。