​ 在之前调研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. J - Sushi 题解(期望dp)

    题目链接 题目大意 给你n个盘子,每个盘子可能有1,2,3个披萨 你选到每个盘子的概率是一样的. 你如果选到空的盘子什么都不做 如果你选到有披萨的盘子则吃掉一个披萨 求吃完所有披萨的期望 题目思路 设 ...

  2. jstack测试

    1.RUNABLE 2.BLOCKED 3.WAITING/TIMED_WAITING Reference Handler线程与Finalizer线程,这两个线程用于虚拟机处理override了obj ...

  3. 思维导图学 Kotlin

    前言 最近做了<Kotlin实战>的思维导图笔记,Kotlin真香-- 目录 基础 函数 类.对象 λ表达式 类型 约定 高阶函数.泛型 公众号 coding 笔记.点滴记录,以后的文章也 ...

  4. Centos7配置阿里epel源|yum源

    这一步非常重要.重要.重要.在这解释一下源的概念,打个比方如果手机想获取一个软件,可以选择很多途径,如华为的华为商店,小米的应用商店,苹果的App store,源就相当于各种手机获取软件的商店.因为国 ...

  5. 20200311_最新爬取mzitu

    废话不多, 直接上代码, python3.6: import requests from bs4 import BeautifulSoup import os import time; import ...

  6. moviepy音视频剪辑:lum_contrast什么时候使用以及图像处理什么时候需要调整亮度与对比度

    ☞ ░ 前往老猿Python博文目录 ░ 一.亮度.对比度的概念 图像的亮度(luminosity )也即对明度的度量(参考<音视频处理基础知识扫盲:数字视频YUV像素表示法以及视频帧和编解码概 ...

  7. DVWA SQL Injection LOW

    最近在学习SQL注入,初出茅庐,就从dvwa开始吧 sql注入可以通过sqlmap工具实现,为了更好地了解原理,这里主要是手工注入 注入的一般流程为: 1,找到注入点,此步骤可通过工具 2,判断注入类 ...

  8. SQL Server常用函数及命令

    1.字符串函数 --ascii函数,返回字符串最左侧字符的ascii码值 SELECT ASCII('a') AS asciistr --ascii代码转换函数,返回指定ascii值对应的字符 SEL ...

  9. Ubuntu 16.04 系统基础开发环境搭建

    1.安装 Git sudo apt-get update sudo apt-get install git Do you want to continue? [Y/n] Y git --version ...

  10. 【题解】三角形 [P1222] / 三角形覆盖问题 [HNOI2012] [P3219]

    [题解]三角形 [P1222] / 三角形覆盖问题 [HNOI2012] [P3219] 传送门: 三角形 \(\text{[P1222]}\) 三角形覆盖问题 \(\text{[HNOI2012] ...