1.2 限流

    科技2025-06-16  19

    任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制,保护系统不会被瞬间的流量冲垮,也可以预防恶意请求。

     

    限流有以下几个角度:

    资源的调用关系,例如资源的调用链路,资源和资源之间的关系;

    运行指标,例如 QPS、线程池、系统负载等;

    控制的效果,例如直接限流、冷启动、排队等。

    Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。

     

    Pt1 限流规则

    流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

    FlowSlot 会根据预设的规则,结合前面 NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot 统计出来的实时信息进行流量控制。

    限流的直接表现是在执行 Entry nodeA = SphU.entry(resourceName) 的时候抛出 FlowException 异常。FlowException 是 BlockException 的子类,您可以捕捉 BlockException 来自定义被限流之后的处理逻辑。

    同一个资源可以创建多条限流规则。FlowSlot 会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。

     

    一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

    resource:资源名,即限流规则的作用对象

    count: 限流阈值

    grade: 限流阈值类型(QPS 或并发线程数)

    limitApp: 流控针对的调用来源,若为 default 则不区分调用来源

    strategy: 调用关系限流策略

    controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

     

    Pt2 基于QPS/并发线程数

    流量控制主要有两种统计类型,一种是统计并发线程数,另外一种则是统计 QPS。类型由 FlowRule 的 grade 字段来定义。其中,0 代表根据并发数量来限流,1 代表根据 QPS 来进行流量控制。其中线程数、QPS 值,都是由 StatisticSlot实时统计获取的。

    我们可以通过下面的命令查看实时统计信息:

    curl http://localhost:8719/cnode?id=resourceName

    输出内容格式如下:

    idx id     thread pass blocked   success total Rt   1m-pass   1m-block   1m-all   exception 2   abc647    0     46      0         46      46   1     2763       0         2763     0

    其中:

    thread: 代表当前处理该资源的并发数;

    pass: 代表一秒内到来到的请求;

    blocked: 代表一秒内被流量控制的请求数量;

    success: 代表一秒内成功处理完的请求;

    total: 代表到一秒内到来的请求以及被阻止的请求总和;

    RT: 代表一秒内该资源的平均响应时间;

    1m-pass: 则是一分钟内到来的请求;

    1m-block: 则是一分钟内被阻止的请求;

    1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;

    exception: 则是一秒内业务本身异常的总和。

     

    Pt2.1 并发线程数控制

    并发数控制用于保护业务线程池不被慢调用耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对太多线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离)。这种隔离方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。

    Sentinel 并发控制不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目(正在执行的调用数目),如果超出阈值,新的请求会被直接拒绝,效果类似于信号量隔离。并发数控制通常在调用端进行配置。

     

    基于并发线程数限流示例说明:

    <!-- 添加SpringBoot项目对Sentinel依赖包 --> <dependency>    <groupId>com.alibaba.cloud</groupId>    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>    <version>2.1.0.RELEASE</version> </dependency> // 1、项目启动加载Sentinel规则 @SpringBootApplication public class MicroserviceApplication { ​    public static void main(String[] args) {        // 启动时将规则加载到内存        initFlowRule(); ​        SpringApplication.run(MicroserviceApplication.class, args);   } ​    // 初始化限流规则    public static void initFlowRule() {        List<FlowRule> rules = new ArrayList<FlowRule>();        FlowRule rule = new FlowRule();        rule.setResource("hello");  // 指明针对hello资源设置规则        rule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 设置限流类型为并发线程数控制        rule.setCount(5);   // 限流阈值为5(最多5个线程)        rule.setLimitApp("default");        rules.add(rule);        FlowRuleManager.loadRules(rules);   } } // 2.资源定义 @Service("sentinelService") public class SentinelService { ​    @SentinelResource(value = "hello", blockHandler = "blockHandler")    public String hello() {        System.out.println("hello world.");        try {            TimeUnit.SECONDS.sleep(30);       } catch (InterruptedException e) {            e.printStackTrace();       }        return "hello world.";   } ​    public String blockHandler() {        System.out.println("被限流了.");        return "被限流了";   } } // 3.请求路径 @RestController public class SentinelController { ​    @Autowired    public SentinelService sentinelService; ​    @RequestMapping("/hello")    public String hello() {        return sentinelService.hello();   } } <!-- 连续请求服务端触发限流 --> hello world. hello world. hello world. hello world. hello world. 2020-09-27 17:02:07.031 ERROR 10436 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]   : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause ​ com.alibaba.csp.sentinel.slots.block.flow.FlowException: null ​ <!-- 间隔30S后再次连续请求服务端触发限流 --> hello world. hello world. hello world. hello world. hello world. 2020-09-27 17:04:10.548 ERROR 10436 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]   : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause ​ com.alibaba.csp.sentinel.slots.block.flow.FlowException: null  

    Pt2.2 QPS流量控制

    当 QPS 超过某个阈值的时候,则采取措施进行流量控制。流量控制的效果包括以下几种:直接拒绝、Warm Up、匀速排队。对应 FlowRule 中的 controlBehavior 字段。

    QPS是上游调用请求方流量超过阈值而采取的措施,并发线程数则不一定是因为上游请求流量异常的原因,也可能是下游服务故障或者别的因素引发当前服务平均处理时间过长,处理能力下降,引发的请求堆积。

    注意:若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。

     

    基于QPS限流示例说明:

    // 定义限流规则 // 其它部分代码和[并发线程数限流示例]相同 @SpringBootApplication public class MicroserviceApplication { ​    public static void main(String[] args) {        // 启动时将规则加载到内存        initFlowRule(); ​        SpringApplication.run(MicroserviceApplication.class, args);   } ​    // 初始化限流规则    public static void initFlowRule() {        List<FlowRule> rules = new ArrayList<FlowRule>();        FlowRule rule = new FlowRule();        rule.setResource("hello");  // 指明针对hello资源设置规则        rule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 设置限流类型为并发线程数控制        rule.setCount(2);   // 限流阈值每秒2个请求        rules.add(rule);        FlowRuleManager.loadRules(rules);   } } <!-- 快速不停刷新页面请求 --> hello world. hello world. hello world. hello world. 2020-09-27 17:05:36.103 ERROR 13172 --- [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]   : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause ​ com.alibaba.csp.sentinel.slots.block.flow.FlowException: null ​ <!-- 快速不停刷新页面请求 --> hello world. hello world. 2020-09-27 17:05:44.088 ERROR 13172 --- [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]   : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.reflect.UndeclaredThrowableException] with root cause ​ com.alibaba.csp.sentinel.slots.block.flow.FlowException: null  

    Pt2.3 限流策略

    直接拒绝

    直接拒绝(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。

    Warm Up

    Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

    通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

     

    匀速排队

    匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。

    该方式的作用如下图所示:

    注意:匀速排队模式暂时不支持 QPS > 1000 的场景。

     

    Pt3 基于调用关系

    调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用的关系,并且通过 ClusterBuilderSlot 记录每个资源的实时统计信息。

    有了调用链路的统计信息,我们可以衍生出多种流量控制手段。

     

    Pt3.1 根据调用方限流

    ContextUtil.enter(resourceName, origin) 方法中的 origin 参数标明了调用方身份。这些信息会在 ClusterBuilderSlot 中被统计。可通过以下命令来展示不同的调用方对同一个资源的调用数据:

    curl http://localhost:8719/origin?id=nodeA

    调用数据示例:

    id: nodeA idx origin threadNum passedQps blockedQps totalQps aRt   1m-passed 1m-blocked 1m-total 1   caller1 0         0         0         0       0     0         0         0 2   caller2 0         0         0         0       0     0         0         0

    上面这个命令展示了资源名为 nodeA 的资源被两个不同的调用方调用的统计。

    流控规则中的 limitApp 字段用于根据调用来源进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:

    default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。

    {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如 NodeA 配置了一条针对调用者caller1的规则,那么当且仅当来自 caller1 对 NodeA 的请求才会触发流量控制。

    other:表示针对除 {some_origin_name} 以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者 caller1 的限流规则,同时又配置了一条调用者为 other 的规则,那么任意来自非 caller1 对 NodeA 的调用,都不能超过 other 这条规则定义的阈值。

    同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default

     

    Pt3.2 根据链路入口限流

    NodeSelectorSlot 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 machine-root 的虚拟节点,调用链的入口都是这个虚节点的子节点。

    一棵典型的调用树如下图所示:

                machine-root                   /       \                   /         \             Entrance1     Entrance2               /             \               /               \     DefaultNode(nodeA)   DefaultNode(nodeA)

    上图中来自入口 Entrance1 和 Entrance2 的请求都调用到了资源 NodeA,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 strategy 为 RuleConstant.STRATEGY_CHAIN,同时设置 refResource 为 Entrance1 来表示只有从入口 Entrance1 的调用才会记录到 NodeA 的限流统计当中,而不关心经 Entrance2 到来的调用。

    调用链的入口(上下文)是通过 API 方法 ContextUtil.enter(contextName) 定义的,其中 contextName 即对应调用链路入口名称。详情可以参考 ContextUtil 文档。

     

    Pt3.3 根据资源关联限流

    当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

     


    Pt4 黑白名单控制

    很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制(黑白名单控制)的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

    调用方信息通过 ContextUtil.enter(resourceName, origin) 方法中的 origin 参数传入。

     

    来源访问控制规则(AuthorityRule)非常简单,主要有以下配置项:

    resource:资源名,即限流规则的作用对象。

    limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB。

    strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。

     

    代码示例:

    比如我们希望控制对资源 test 的访问设置白名单,只有来源为 appA 和 appB 的请求才可通过,则可以配置如下白名单规则:

    package com.lucas.microservice; ​ import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy; import org.springframework.boot.autoconfigure.SpringBootApplication; ​ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; ​ @SpringBootApplication public class MicroserviceApplication { ​   public static void main(String[] args) {       // 启动时将规则加载到内存       initWhiteRules();       // 执行调用逻辑       process();   } ​   private static void initWhiteRules() {       AuthorityRule rule = new AuthorityRule();       rule.setResource("hello");       rule.setStrategy(RuleConstant.AUTHORITY_WHITE);       rule.setLimitApp("appA");       AuthorityRuleManager.loadRules(Collections.singletonList(rule)); ​       rule = new AuthorityRule();       rule.setResource("hello");       rule.setStrategy(RuleConstant.AUTHORITY_BLACK);       rule.setLimitApp("appB");       AuthorityRuleManager.loadRules(Collections.singletonList(rule));   } ​   public static void process() { ​       for (int i = 0; i < 1000; i++) {                   res();       }   } ​   public static void res(){       Entry entry = null;       ContextUtil.enter("entrance1", "appB");       try {           // 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。           entry = SphU.entry("hello");           // 被保护的业务逻辑           System.out.println("hello world.");           sleep();       } catch (BlockException ex) {           // 资源访问阻止,被限流或被降级           System.out.println("Request blocked.");       } finally {           if (entry != null) {               entry.exit();           }           ContextUtil.exit();       }   } ​   private static void sleep() {       try {           TimeUnit.SECONDS.sleep(10);       } catch (InterruptedException e) {           e.printStackTrace();       }   } } // 运行程序appB Request blocked. Request blocked. Request blocked. Request blocked. ...... Request blocked. ​ // 改为appA再次运行 hello world. hello world. hello world. hello world. ...... hello world.  

    Pt5 热点参数限流

    热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

    商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制

    用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制

    热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。

    Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。热点参数限流支持集群模式。

     

    Pt5.1 基本使用

    要使用热点参数限流功能,需要引入以下依赖:

    <dependency>   <groupId>com.alibaba.csp</groupId>   <artifactId>sentinel-parameter-flow-control</artifactId>   <version>x.y.z</version> </dependency>

    然后为对应的资源配置热点参数限流规则,并在 entry 的时候传入相应的参数,即可使热点参数限流生效。

    注:若自行扩展并注册了自己实现的 SlotChainBuilder,并希望使用热点参数限流功能,则可以在 chain 里面合适的地方插入 ParamFlowSlot。

    那么如何传入对应的参数以便 Sentinel 统计呢?我们可以通过 SphU 类里面几个 entry 重载方法来传入:

    public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException ​ public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException

    其中最后的一串 args 就是要传入的参数,有多个就按照次序依次传入。比如要传入两个参数 paramA 和 paramB,则可以:

    // paramA in index 0, paramB in index 1. // 若需要配置例外项或者使用集群维度流控,则传入的参数只支持基本类型。 SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);

    注意:若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(exit(count, args)),否则可能会有统计错误。正确的示例:

    Entry entry = null; try {   entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB);   // Your logic here. } catch (BlockException ex) {   // Handle request rejection. } finally {   if (entry != null) {       entry.exit(1, paramA, paramB);   } }

    对于 @SentinelResource 注解方式定义的资源,若注解作用的方法上有参数,Sentinel 会将它们作为参数传入 SphU.entry(res, args)。比如以下的方法里面 uid 和 type 会分别作为第一个和第二个参数传入 Sentinel API,从而可以用于热点规则判断:

    @SentinelResource("myMethod") public Result doSomething(String uid, int type) { // some logic here... }

     

    Pt5.2 热点参数规则

    热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

    属性说明默认值resource资源名,必填 count限流阈值,必填 grade限流模式QPS 模式durationInSec统计窗口时间长度(单位为秒),1.6.0 版本开始支持1scontrolBehavior流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持快速失败maxQueueingTimeMs最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持0msparamIdx热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置 paramFlowItemList参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型 clusterMode是否是集群参数流控规则falseclusterConfig集群流控相关配置 

    我们可以通过 ParamFlowRuleManager 的 loadRules 方法更新热点参数规则,下面是一个示例:

    ParamFlowRule rule = new ParamFlowRule(resourceName)   .setParamIdx(0)   .setCount(5); // 针对 int 类型的参数 PARAM_B,单独设置限流 QPS 阈值为 10,而不是全局的阈值 5. ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))   .setClassType(int.class.getName())   .setCount(10); rule.setParamFlowItemList(Collections.singletonList(item)); ​ ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

     

    Processed: 0.010, SQL: 8