​ 在之前调研Sentinel的过程中,为了准备分享内容,自己就简单的写了一些测试代码,不过在测试中遇到了一些问题,其中有一个问题就是Sentinel流控在并发情况下限流并不精确,当时我还在想,这个我在做分享的时候该怎么来自圆其说呢,所以觉得比较有意思,在这里做一个记录。同时在排查这个问题的过程中,为了说清楚问题原因,我觉得有必要理一下它的责任链,所以副标题就是Sentinel的责任链。


一、问题起源

​ 在这里我直接上我的测试代码,我的本意是要起10个线程同时去请求这个资源,每个线程轮询10次。同时,对于这个资源我设置了限流规则为QPS=1。这也就意味着我这10个线程共100个请求,正确的结果应该是成功1个,阻塞99个。

import com.alibaba.csp.sentinel.Entry;
import com.alibaba.csp.sentinel.SphU;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger; /**
* 流量控制演示
*/
public class FlowQpsDemo_Bug { private static final String SOURCE_KEY = "CESHI_KEY"; private static AtomicInteger pass = new AtomicInteger();
private static AtomicInteger block = new AtomicInteger();
private static AtomicInteger total = new AtomicInteger(); public static void main(String[] args) throws InterruptedException {
initFlowQpsRule();
CountDownLatch start = new CountDownLatch(1);
CountDownLatch end = new CountDownLatch(10);
for (int i = 0;i < 10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
start.await();
} catch (InterruptedException e) {
}
for (int i = 0;i < 10;i++) {
fun();
}
end.countDown();
}
}).start();
}
start.countDown();
end.await();
System.out.println("total=" + total.get() + " pass=" + pass.get() + " block=" + block.get());
} public static void fun() {
Entry entry = null;
try {
entry = SphU.entry(SOURCE_KEY);
// todo 业务逻辑
pass.incrementAndGet();
} catch (BlockException e1) {
// todo 流控处理
block.incrementAndGet();
} finally {
total.incrementAndGet();
if (entry != null) {
entry.exit();
}
}
} private static void initFlowQpsRule() {
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource(SOURCE_KEY);
// 采用qps策略,每秒允许通过1个请求
rule1.setCount(1);
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);
}
}

​ 但是,实际运行结果却并非如此,运行好多次都是成功10个,阻塞90个。也有其他情况比如成功5、6个,阻塞90多个。这显然不是我想要的结果。并且考虑到我们在生产环境设置的限流往往是压测的极限值了,如果这时限流还不准确,那岂不是就失去了系统保护的作用了。

​ 为了解决这个问题,最直接的方式就是去走一遍它的源码,这一走就看到了问题。为了说明问题所在,有必要先介绍一下Sentinel内部责任链的使用。

二、Sentinel内部责任链的使用

​ 责任链由一系列节点组成,每个节点拥有一个next属性来指向下一个处理节点。当一个请求进来时,当前节点或者执行完后将请求交个next节点处理,或者先交个next节点处理完后自己再处理。这样就形成一条链,将请求一个节点一个节点的往下传递处理执行。

​ 在Sentinel中,对应的就是Slot,官方文档中翻译为功能插槽。最顶层的插槽定义就是com.alibaba.csp.sentinel.slotchain.ProcessorSlot,这是一个接口,下面有一个抽象类com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot实现了它,其中最主要的逻辑就是entry和exit两个方法,分别在请求进来和退出时调用。

​ 而AbstractLinkedProcessorSlot有一系列的子类,这一系列的子类分别完成不同的功能,列举几个重要的如下:

  • StatisticSlot:处理统计逻辑,它是其它限流、降级等插槽执行的基础;
  • ClusterBuilderSlot :用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • FlowSlot:用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • DegradeSlot:通过统计信息以及预设的规则,来做熔断降级;

​ 整个责任链的构建入口为DefaultSlotChainBuilder.build。其中,每个功能插槽都有对应的顺序,Sentinel在构建链的时候按照优先级从小到大的顺序进行串联构建链路。

三、不精确原因

​ 在这次采坑中,两个主角就是StatisticSlot和FlowSlot,根据源码中的@SpiOrder标注,StatisticSlot为-7000,FlowSlot为-2000,所以FlowSlot的优先级低于StatisticSlot。那么当请求进来的时候先执行的是StatisticSlot。

​ 首先看StatisticSlot的entry方法的执行,这里我删去了BlockException异常处理的逻辑,它大体上与上面异常的处理逻辑一样。

@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); // Request passed, add thread count and pass count.
node.increaseThreadNum();
// todo issue_1620 这个时候才会增加QPS,但是刚才前面的DefaultController.pass方法已经返回了true
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) {
node.increaseThreadNum();
if (context.getCurEntry().getOriginNode() != null) {
// Add count for origin node.
context.getCurEntry().getOriginNode().increaseThreadNum();
} if (resourceWrapper.getEntryType() == EntryType.IN) {
// Add count for global inbound entry node for global statistics.
Constants.ENTRY_NODE.increaseThreadNum();
}
// Handle pass event with registered entry callback handlers.
for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
handler.onPass(context, resourceWrapper, node, count, args);
}
} catch (BlockException e) {
} catch (Throwable e) {
// Unexpected internal error, set error to current entry.
context.getCurEntry().setError(e);
throw e;
}
}

​ 在上面这段代码中,StatisticSlot在entry方法中并不是先执行的自己的逻辑,首先调用的是fireEntry方法,进入到这个方法,在AbstractLinkedProcessorSlot类中,代码如下:

@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}

​ 可以看出,在StatisticSlot的entry方法中,它是先调用父类的fireEntry交由后面的节点执行,等后面如限流、降级节点执行完后返回到StatisticSlot的entry方法中后,它才执行对应的统计逻辑。

​ 那么这时可直接进到FlowSlot的entry方法中,代码如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
checkFlow(resourceWrapper, context, node, count, prioritized); fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

​ 可以看出,checkFlow就是限流规则的执行了。进到这个方法中,其逻辑就是调用FlowRuleChecker检查各项限流规则是否已经达到阈值,如果没有达到阈值则放行,如果达到阈值了则阻塞。在规则的检查中,一路跟到了DefaultController类的canPass方法中,其逻辑如下:

@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = avgUsedTokens(node);
// 当前阈值 + acquireCount 大于规则设定的count,则返回false,否则返回true
// 如果并发执行到这里,并没有加锁,所以多个线程都会返回true,限流失效
if (curCount + acquireCount > count) {
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;
}

​ 问题就出在这个判断中了,要知道前面的统计插槽一定是要等后面的功能插槽执行完后才有统计的依据,而在一个统计时间段开始的时候,10个请求同时进来,这时前面还没有统计数据,那么curCount + acquireCount > count这个条件就可以同时满足,这也就意味着这10个请求可以同时通过,所以就出现了上面最开始我遇到的情况,明明设置了QPS=1,但是还是有10个请求成功通过了,问题点就在这里。

​ 对于这个问题,我后来看了一下git上面有问题说明,对应两个链接如下:

https://github.com/alibaba/Sentinel/issues/1861

https://github.com/alibaba/Sentinel/issues/1620

​ 这个问题在1.6版本就提出了,但是目前1.8版本还是没有修复。

​ 针对这个问题,我的理解就是:Sentinel可能不需要做的那么完美,它可以不完美,但是不可以影响正常的业务系统执行,不能拉低正常业务系统的性能。

​ 同时这也是一个逻辑缺陷,采用了责任链模式,那么在前面的统计插槽中进行统计时,必然要以后面的其它功能插槽的执行结果为依据。而后面的其它功能插槽对规则的检查又依赖前面的统计数据,所以这个问题还真不好【完美】的去解决它。总不能在后面加锁让请求一个一个通过吧,当然这只是一个玩笑。

四、责任链构建

​ 上面提到了责任链构建的地方在DefaultSlotChainBuilder.build里面,方法很简单,如下:

@Override
public ProcessorSlotChain build() {
ProcessorSlotChain chain = new DefaultProcessorSlotChain(); // Note: the instances of ProcessorSlot should be different, since they are not stateless.
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
continue;
} chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
} return chain;
}

​ 其中DefaultProcessorSlotChain只是一个默认的头结点,里面只有first、end两个元素。通过它的SpiLoader.loadPrototypeInstanceListSorted方法将ProcessorSlot的各个插槽实现类加载进来,然后进行链接。

​ 其中SpiLoader.loadPrototypeInstanceListSorted对加载的ProcessorSlot进行排序,排序使用到了一个SpiOrderWrapper包装类,里面有一个order属性,加载排序如下:

public static <T> List<T> loadPrototypeInstanceListSorted(Class<T> clazz) {
try {
// Not use SERVICE_LOADER_MAP, to make sure the instances loaded are different.
ServiceLoader<T> serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz); List<SpiOrderWrapper<T>> orderWrappers = new ArrayList<>();
for (T spi : serviceLoader) {
int order = SpiOrderResolver.resolveOrder(spi);
// Since SPI is lazy initialized in ServiceLoader, we use online sort algorithm here.
SpiOrderResolver.insertSorted(orderWrappers, spi, order);
RecordLog.debug("[SpiLoader] Found {} SPI: {} with order {}", clazz.getSimpleName(),
spi.getClass().getCanonicalName(), order);
}
List<T> list = new ArrayList<>(orderWrappers.size());
for (int i = 0; i < orderWrappers.size(); i++) {
list.add(orderWrappers.get(i).spi);
}
return list;
} catch (Throwable t) {
RecordLog.error("[SpiLoader] ERROR: loadPrototypeInstanceListSorted failed", t);
t.printStackTrace();
return new ArrayList<>();
}
}

​ 上面两段就是Sentinel构建责任链的核心代码。整个代码还是非常漂亮,非常清晰。

五、结束语

​ 问:既然遇到了这个问题,那最后怎么解决呢?

​ 答:不解决,继续用。

Sentinel并发限流不精确-之责任链的更多相关文章

  1. Spring Cloud Alibaba | Sentinel: 服务限流基础篇

    目录 Spring Cloud Alibaba | Sentinel: 服务限流基础篇 1. 简介 2. 定义资源 2.1 主流框架的默认适配 2.2 抛出异常的方式定义资源 2.3 返回布尔值方式定 ...

  2. Spring Cloud Alibaba | Sentinel: 服务限流高级篇

    目录 Spring Cloud Alibaba | Sentinel: 服务限流高级篇 1. 熔断降级 1.1 降级策略 2. 热点参数限流 2.1 项目依赖 2.2 热点参数规则 3. 系统自适应限 ...

  3. Spring Cloud微服务Sentinel+Apollo限流、熔断实战总结

    在Spring Cloud微服务体系中,由于限流熔断组件Hystrix开源版本不在维护,因此国内不少有类似需求的公司已经将眼光转向阿里开源的Sentinel框架.而以下要介绍的正是作者最近两个月的真实 ...

  4. 流量治理神器-Sentinel的限流模式,选单机还是集群?

    大家好,架构摆渡人.这是我的第5篇原创文章,还请多多支持. 上篇文章给大家推荐了一些限流的框架,如果说硬要我推荐一款,我会推荐Sentinel,Sentinel的限流模式分为两种,分别是单机模式和集群 ...

  5. Spring Cloud Gateway 整合阿里 Sentinel网关限流实战!

    大家好,我是不才陈某~ 这是<Spring Cloud 进阶>第八篇文章,往期文章如下: 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强? openFeign夺命连环9问,这谁受得 ...

  6. 快速入门系列--WCF--06并发限流、可靠会话和队列服务

    这部分将介绍一些相对深入的知识点,包括通过并发限流来保证服务的可用性,通过可靠会话机制保证会话信息的可靠性,通过队列服务来解耦客户端和服务端,提高系统的可服务数量并可以起到削峰的作用,最后还会对之前的 ...

  7. wcf利用IDispatchMessageInspector实现接口监控日志记录和并发限流

    一般对于提供出来的接口,虽然知道在哪些业务场景下才会被调用,但是不知道什么时候被调用.调用的频率.接口性能,当出现问题的时候也不容易重现请求:为了追踪这些内容就需要把每次接口的调用信息给完整的记录下来 ...

  8. Dubbo学习系列之十(Sentinel之限流与降级)

    各位看官,先提个问题,如果让你设计一套秒杀系统,核心要点是啥???我认为有三点:缓存.限流和分离.想当年12306大面积崩溃,还有如今的微博整体宕机情况,感觉就是限流降级没做好,"用有限的资 ...

  9. SpringBoot 2.0 + 阿里巴巴 Sentinel 动态限流实战

    前言 在从0到1构建分布式秒杀系统和打造十万博文系统中,限流是不可缺少的一个环节,在系统能承受的范围内既能减少资源开销又能防御恶意攻击. 在前面的文章中,我们使用了开源工具包 Guava 提供的限流工 ...

随机推荐

  1. ConvTranspose2d

    nn.ConvTranspose2d的功能是进行反卷积操作 nn.ConvTranspose2d(in_channels, out_channels, kernel_size, stride=1, p ...

  2. 基于gin的golang web开发:认证利器jwt

    JSON Web Token(JWT)是一种很流行的跨域认证解决方案,JWT基于JSON可以在进行验证的同时附带身份信息,对于前后端分离项目很有帮助. eyJhbGciOiJIUzI1NiIsInR5 ...

  3. fist-第八天冲刺随笔

    这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzzcxy/2018SE1 这个作业要求在哪里 https://edu.cnblogs.com/campus/fz ...

  4. MySQL索引(二):建索引的原则

    在了解了索引的基础知识及B+树索引的原理后(如需复习请点这里),这一节我们了解一下有哪些建索引的原则,来指导我们去建索引. 建索引的原则 1. 联合索引 我们可能听一些数据库方面的专业人士说过:&qu ...

  5. 第13.1节 关于Python的异常处理

    Python的异常网上有很多资料介绍,老猿就不再细说,在这里老猿只挑几件老猿认为重要的内容介绍一下. 一. 异常处理完整语法 异常处理的完整语法语法如下: try: - except (异常1,-,异 ...

  6. PHP代码审计分段讲解(1)

    PHP源码来自:https://github.com/bowu678/php_bugs 快乐的暑期学习生活+1 01 extract变量覆盖 <?php $flag='xxx'; extract ...

  7. flask中的return、过滤器详解

    之前吧一直学习flask的时候,一直不明白response是怎么产生,今天是明白了.retrun  哎呀,这个地方看着挺小心的东西, 蕴含的能量可不小啊.今天我详细总结总结. 先来写jinjia2语法 ...

  8. c++11-17 模板核心知识(十四)—— 解析模板之依赖型模板名称(.template/->template/::template)

    tokenization与parsing 解析模板之类型的依赖名称 Dependent Names of Templates Example One Example Two Example Three ...

  9. 手把手教你写DI_2_小白徒手撸构造函数注入

    小白徒手撸构造函数注入 在上一节:手把手教你写DI_1_DI框架有什么? 我们已经知道我们要撸哪些东西了 那么我们开始动工吧,这里呢,我们找小白同学来表演下 小白同学 :我们先定义一下我们的广告招聘纸 ...

  10. P1654 OSU! 题解

    \(x\) 为该位置有 \(1\) 的期望. 统计两个值 : \(suma\) 和 \(sumb\). \(suma\) 表示连续 \(X\) 个 \(1\) , \(X\) 的平方的期望, \(su ...