距离上次总结Sentinel的滑动窗口算法已经有些时间了,本来想着一口气将它的core模块所有总结完,可是中间一懒就又松懈下来了,这几天在工做之余又从新整理了一下,在这里作一个学习总结。html
上篇滑动窗口算法总结连接:https://www.cnblogs.com/mrxiaobai-wen/p/14212637.htmljava
今天主要总结了一下Sentinel的快速失败和匀速排队的漏桶算法。由于它的WarmUpController和WarmUpRateLimiterController对应的令牌桶算法的数学计算原理有一点点复杂,因此我准备在后面单独用一篇来总结。因此今天涉及到的主要就是DefaultController和RateLimiterController。node
首先进入到FlowRuleUtil类中,方法generateRater就是对应策略的建立,逻辑比较简单,代码以下:算法
private static TrafficShapingController generateRater(FlowRule rule) { if (rule.getGrade() == RuleConstant.FLOW_GRADE_QPS) { switch (rule.getControlBehavior()) { case RuleConstant.CONTROL_BEHAVIOR_WARM_UP: // WarmUp-令牌桶算法 return new WarmUpController(rule.getCount(), rule.getWarmUpPeriodSec(), ColdFactorProperty.coldFactor); case RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER: // 排队等待-漏桶算法 return new RateLimiterController(rule.getMaxQueueingTimeMs(), rule.getCount()); case RuleConstant.CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER: // 预热和匀速排队结合 return new WarmUpRateLimiterController(rule.getCount(), rule.getWarmUpPeriodSec(), rule.getMaxQueueingTimeMs(), ColdFactorProperty.coldFactor); case RuleConstant.CONTROL_BEHAVIOR_DEFAULT: default: // Default mode or unknown mode: default traffic shaping controller (fast-reject). } } // 快速失败 return new DefaultController(rule.getCount(), rule.getGrade()); }
默认流控算法代码以下:api
@Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { int curCount = avgUsedTokens(node); // 当前阈值 + acquireCount 是否大于规则设定的count,小于等于则表示符合阈值设定直接返回true if (curCount + acquireCount > count) { // 在大于的状况下,针对QPS的状况会对先进来的请求进行特殊处理 if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) { long currentTime; long waitInMs; currentTime = TimeUtil.currentTimeMillis(); // 若是策略是QPS,那么对于优先请求尝试去占用下一个时间窗口中的令牌 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; }
先看一下涉及到的avgUsedTokens方法:app
private int avgUsedTokens(Node node) { if (node == null) { return DEFAULT_AVG_USED_TOKENS; } // 获取当前qps或者当前线程数 return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps()); }
主要是获取已使用的令牌数,若是设置的阈值类型为线程数,那么返回当前统计节点中保存的线程数,若是设置的阈值类型为QPS,那么返回已经经过的QPS数。ide
而后回到上面的canPass方法,其主要逻辑就是在获取到目前节点的统计数据后,将已占用的令牌数与请求的令牌数相加,若是小于设定的阈值,那么直接放行。学习
若是大于设置的阈值,那么在阈值类型为QPS且容许优先处理先到的请求的状况下进行特殊处理,不然返回false不放行。ui
上面特殊处理就是:首先尝试去占用后面的时间窗口的令牌,获取到等待时间,若是等待时间小于设置的最长等待时长,那么就进行等待,当等待到指定时间后返回。不然直接返回false不放行。.net
由代码能够看出,在等待指定时长后,抛出PriorityWaitException进行放行,对应实现的地方在StatisticSlot中,对应entry方法代码以下:
@Override 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); // 说明:省略了执行经过的处理逻辑 } catch (PriorityWaitException ex) { node.increaseThreadNum(); if (context.getCurEntry().getOriginNode() != null) { context.getCurEntry().getOriginNode().increaseThreadNum(); } if (resourceWrapper.getEntryType() == EntryType.IN) { Constants.ENTRY_NODE.increaseThreadNum(); } for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) { handler.onPass(context, resourceWrapper, node, count, args); } } catch (BlockException e) { // 说明:省略了阻塞异常处理逻辑 throw e; } catch (Throwable e) { context.getCurEntry().setError(e); throw e; } }
对这个方法去除了其它多余代码,能够看出在PriorityWaitException异常捕捉的代码中没有继续抛出,因此对该请求进行了放行。
对于漏桶算法,首先在网上盗用一张图以下:
图片来源:https://blog.csdn.net/tianyaleixiaowu/article/details/74942405
其思路是:水流(请求)先进入到漏桶里,漏桶以必定的速率匀速流出,当流入量过大的时候,多余水流(请求)直接溢出,从而达到对系统容量的保护。
对应Sentinel使用漏桶算法进行流量整形的效果就以下图所示:
来看RateLimiterController的canPass方法:
@Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { if (acquireCount <= 0) { return true; } if (count <= 0) { return false; } long currentTime = TimeUtil.currentTimeMillis(); // 计算这次令牌颁发所须要的时间,其中: (1.0 / count * 1000)表明每一个令牌生成的耗时,而后乘以acquireCount获得这次所需令牌生成耗时 long costTime = Math.round(1.0 * (acquireCount) / count * 1000); // 在上次经过时间的基础上加上本次的耗时,获得指望经过的时间点 long expectedTime = costTime + latestPassedTime.get(); if (expectedTime <= currentTime) { // 若是指望时间小于当前时间,那么说明当前令牌充足,能够放行,同时将当前时间设置为上次经过时间 latestPassedTime.set(currentTime); return true; } else { // 当指望时间大于当前时间,那么说明令牌不够,须要等待 long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis(); if (waitTime > maxQueueingTimeMs) { // 若是须要等待时间大于设置的最大等待时长,那么直接丢弃,不用等待,下面同理 return false; } else { long oldTime = latestPassedTime.addAndGet(costTime); try { // 再次检查等待时长 waitTime = oldTime - TimeUtil.currentTimeMillis(); if (waitTime > maxQueueingTimeMs) { latestPassedTime.addAndGet(-costTime); return false; } // in race condition waitTime may <= 0 if (waitTime > 0) { Thread.sleep(waitTime); } return true; } catch (InterruptedException e) { } } } return false; }
Sentinel的令牌桶算法和漏桶算法都参考了Guava RateLimiter的设计。
上面的逻辑很清晰,其思路就是根据当前令牌请求数量acquireCount乘以令牌生成速率获得本次所需令牌的生成时间,而后加上上次经过时间获得一个本次请求的指望经过时间,若是指望经过时间小于当前时间那么说明容量足够直接经过,若是指望经过时间大于当前时间那么说明系统容量不够须要等待,而后结合设置的等待时间判断是继续等待仍是直接放弃。
须要特别注意的是,匀速模式具备局限性,它只支持1000之内的QPS。咱们能够看对应的语句:
long costTime = Math.round(1.0 * (acquireCount) / count * 1000); long expectedTime = costTime + latestPassedTime.get();
很容易获得以下结果,每种阈值对应的令牌生成时间(单位:毫秒):
count | costTime |
---|---|
100 | 10 |
1000 | 1 |
2000 | 1 |
3000 | 0 |
因此当阈值count大于2000后,每一个令牌生成的时间间隔计算为0,那么后面的判断就没有意义了。因此Sentinel的匀速器只支持QPS在1000之内的请求。