流量控制规则,简称流控规则,会对资源的流量进行限制。同一个资源可以对应多条限流规则。Sentinel会对该资源的所有限流规则依次遍历,直到有规则触发限流或者所有规则遍历完毕。
限流的直接表现是抛出FlowException异常。FlowException是BlockException的子类,您可以捕捉BlockException来自定义被限流之后的处理逻辑。
一条限流规则FlowRule主要由下面几个属性组成,我们可以组合这些属性来实现不同的限流效果:
Field | Label | 说明 | 默认值 |
---|---|---|---|
resource | 资源名 | 资源名是限流规则的作用对象 | |
limitApp | 针对来源 | ||
count | 单机阈值 | 限流阈值 | |
grade | 阈值类型 | 限流阈值类型,QPS或线程数模式 | QPS模式 |
strategy | 流控模式 | 调用关系限流策略:直接、链路、关联 | 直接 |
controlBehavior | 流控效果 | 流控效果:直接拒绝、排队等待、慢启动模式 | 直接拒绝 |
流量控制主要有两种统计类型,一种是统计线程数,另外一种则是统计QPS。
类型由FlowRule.grade字段来定义。其中,0代表根据并发数量来限流,1代表根据QPS来进行流量控制。其中线程数、QPS值,都是由StatisticSlot实时统计获取的。
可以通过下面的命令查看实时统计信息:
curl http://localhost:8719/cnode?id=resourceName
例如,注意端口不一定是8719,具体是多少可以通过sentinel控制台->机器列表查看:
curl http://localhost:8720/cnode?id=sentinel-annotation
idx id thread pass blocked success total aRt 1m-pass 1m-block 1m-all exception
2 sentinel-annotation 0 1.0 672.0 1.0 673.0 0.0 8 5117 5125 0.0
其中:
- thread:代表当前处理该资源的线程数;
- pass:代表一秒内到来到的请求;
- blocked:代表一秒内被流量控制的请求数量;
- success:代表一秒内成功处理完的请求;
- total:代表到一秒内到来的请求以及被阻止的请求总和;
- RT:代表一秒内该资源的平均响应时间;
- 1m-pass:则是一分钟内到来的请求;
- 1m-block:则是一分钟内被阻止的请求;
- 1m-all:则是一分钟内到来的请求和被阻止的请求的总和;
- exception:则是一秒内业务本身异常的总和。
QPS
当QPS超过某个阈值的时候,则采取措施进行流量控制。
线程数
线程数限流用于保护业务线程数不被耗尽。例如,当应用所依赖的下游应用由于某种原因导致服务不稳定、响应延迟增加,对于调用者来说,意味着吞吐量下降和更多的线程数占用,极端情况下甚至导致线程池耗尽。为应对高线程占用的情况,业内有使用隔离的方案,比如通过不同业务逻辑使用不同线程池来隔离业务自身之间的资源争抢(线程池隔离),或者使用信号量来控制同时请求的个数(信号量隔离)。这种隔离方案虽然能够控制线程数量,但无法控制请求排队时间。当请求过多时排队也是无益的,直接拒绝能够迅速降低系统压力。Sentinel线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程个数,如果超出阈值,新的请求会被立即拒绝。
根据调用关系限流策略调用关系包括调用方、被调用方;方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel通过NodeSelectorSlot建立不同资源间的调用的关系,并且通过ClusterNodeBuilderSlot记录每个资源的实时统计信息。
有了调用链路的统计信息,我们可以衍生出多种流量控制手段。
根据调用方限流
ContextUtil.enter(resourceName, origin)方法中的origin参数标明了调用方身份。这些信息会在ClusterBuilderSlot中被统计。
可通过以下命令来展示不同的调用方对同一个资源的调用数据:
curl http://localhost:8719/origin?id=nodeA
例如,调用数据示例:
curl http://localhost:8720/origin?id=sentinel-annotation
id: sentinel-annotation
idx originthreadNum passQps blockQps totalQps aRt 1m-pass 1m-block 1m-total
1 nodeA 0 0.0 0.0 0.0 0.0 0 0 0
2 nodeB 0 0.0 0.0 0.0 0.0 100 0 100
上面这个命令展示了资源名为sentinel-annotation
的资源被两个不同的调用方调用的统计。
可以实现接口RequestOriginParser来获取请求中的某个参数来标明调用方身份,例如可以从请求的Header中获取source字段来标明调用方身份,然后可以将source字段配置在流控规则中,可以根据source的值进行限流:
package com.morris.user.config;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class HeaderOriginParser implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest request) {
/**
* 根据header中的source字段来区分请求来源
*/
return request.getHeader("source");
}
}
在sentinel自带的适配器模块sentinel-spring-webmvc-adapter
中默认实现了一个请求来源解析器RequestOriginParser,我们可以将InterceptorConfig注入到项目中来使用:
com.alibaba.csp.sentinel.demo.spring.webmvc.config.InterceptorConfig#addSpringMvcInterceptor
private void addSpringMvcInterceptor(InterceptorRegistry registry) {
SentinelWebMvcConfig config = new SentinelWebMvcConfig();
config.setBlockExceptionHandler(new DefaultBlockExceptionHandler());
config.setHttpMethodSpecify(true);
config.setWebContextUnify(true);
// 请求来源解析器
config.setOriginParser(request -> request.getHeader("S-user"));
registry.addInterceptor(new SentinelWebInterceptor(config)).addPathPatterns("/**");
}
可以看到默认取的是请求头中的S-user
参数来标记请求的调用方来源,所以当请求头部中没有带S-user
参数时sentinel上下文中就无法获取来源,所以就会影响授权规则的限流效果。
限流规则中的limitApp字段用于根据调用方进行流量控制。该字段的值有以下三种选项,分别对应不同的场景:
- default:表示不区分调用者,来自任何调用者的请求都将进行限流统计。如果这个资源名的调用总和超过了这条规则定义的阈值,则触发限流。
- {some_origin_name}:表示针对特定的调用者,只有来自这个调用者的请求才会进行流量控制。例如NodeA配置了一条针对调用者caller1的规则,那么当且仅当来自caller1对NodeA的请求才会触发流量控制。
- other:表示针对除{some_origin_name}以外的其余调用方的流量进行流量控制。例如,资源NodeA配置了一条针对调用者caller1的限流规则,同时又配置了一条调用者为other的规则,那么任意来自非caller1对NodeA的调用,都不能超过other这条规则定义的阈值。
同一个资源名可以配置多条规则,规则的生效顺序为:{some_origin_name} > other > default
。
由于需要为每个调用来源origin的资源建立统计信息StatisticNode,大量使用会造成内存占用过多。这点官方faq中也给出了警示,注意origin数量不能太多,否则会导致内存暴涨,并且目前不支持模式匹配。
例如下面的规则,针对调用方为NodeB的请求进行限流,也就是对请求头中带有source=NodeB的请求进行限流,而不带source请求头或者请求头中source的值不为NodeB的请求不会触发限流。
根据调用链路入口限流:链路限流
NodeSelectorSlot中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为machine-root的虚拟节点,调用链的入口都是这个虚节点的子节点。
一棵典型的调用树如下图所示:
machine-root
/ \
/ \
chainA chainB
/ \
/ \
chain chain
上图中来自入口chainA
和chainB
的请求都调用到了资源chain
,Sentinel允许只根据某个入口的统计信息对资源限流。比如我们可以设置 FlowRule.strategy为RuleConstant.CHAIN,同时设置FlowRule.ref_identity为chainA
来表示只有从入口chainA
的调用才会记录到chain
的限流统计当中,而对来自chainB
的调用漠不关心。
举个例子:chainA、chainB两个接口都调用某一资源chain,chainA->chain、chainB->chain可以看成两个简单的链路,此时可以针对chain配置链路限流,比如限制chainA调用chain,而chainB调用chain则不受影响,它的功能有点类似于针对来源配置项,而链路流控是针对上级接口,它的粒度更细。
示例代码如下:
@RequestMapping("sentinel")
@RestController
public class FlowChainController {
@Resource
private FlowChainService flowChainService;
@RequestMapping("/chainA")
public String chainA(Integer num) {
return flowChainService.chain(num);
}
@RequestMapping("/chainB")
public String chainB(Integer num) {
return flowChainService.chain(num);
}
}
@Service
public class FlowChainService {
@SentinelResource(value = "chain",
fallback = "fallback", fallbackClass = ExceptionUtil.class,
blockHandler = "blockHandler", blockHandlerClass = ExceptionUtil.class
)
public String chain(Integer num) {
return String.valueOf(10 / num);
}
}
规则的配置如下:
这样当/sentinel/chainA
的QPS大于1时,/sentinel/chainA
就会触发限流,而/sentinel/chainB
不会影响。
注意默认调用链路是收敛的,需要打开才可以进行链路流控:
web-context-unify: false # 默认将调用链路收敛,需要打开才可以进行链路流控
调用链的入口是通过API方法ContextUtil.enter(name)定义的。
具有关系的资源流量控制:关联流量控制
当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢。
举例来说,read和write这两个资源分别代表数据库读写,我们可以给read设置限流规则来达到写优先的目的:设置FlowRule.strategy为RuleConstant.RELATE同时设置FlowRule.ref_identity为write。这样当写库操作过于频繁时,读数据的请求会被限流。
@RequestMapping("sentinel")
@RestController
public class FlowRelateController {
@RequestMapping("/read")
public String read(Integer num) {
return String.valueOf(10 / num);
}
@RequestMapping("/write")
public String write(Integer num) {
return String.valueOf(10 / num);
}
}
规则配置如下:
这样当/sentinel/write
的QPS大于1时,/sentinel/read
就会触发限流。
流控效果只用于根据QPS限流。
流量控制的手段包括下面3种,对应FlowRule中的controlBehavior字段:
- 直接拒绝(快速失败)(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式
- WARM_UP(冷启动)(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式
- 排队等待(匀速器)(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式
快速失败
该方式是默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException,不做任何额外的处理,是最简单的效果。这种方式适用于对系统处理能力确切已知的情况下,比如通过压测确定了系统的准确水位时。
Warm Up
该方式主要用于系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮的情况,对应的是令牌桶算法。
当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓缓的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。
这个场景主要用于启动需要额外开销的场景,例如建立数据库连接等。
流控的原理是在流量入口处控制流量,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,以便系统可以预热。最适合突发流量的场景。
冷启动,参考了Guava的算法,通过随时调整斜率,把流量在指定的时间之类缓慢调整到特定的阈值。
举个例子:对/sentinel/annotation
资源设置直接限流,阈值为10,预热时长为10s,则刚访问此接口时,实际限流阈值为10/3,即为3,也就是刚开始的时候阈值只有3,当经过10s后,阈值才慢慢提高到10。
具体配置如下:
可以看到流量的增长趋势如下图所示:
排队等待
这种方式严格控制了请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
请求流量具有波峰波谷的特点,流控的原理是将前面的峰值流量延迟(排队时长)到后面再处理,既能最大化满足所有请求,又能保证用户体验。
让请求以均匀的速度通过,单机阈值为每秒通过数量,其余的排队等待;它还会让设置一个超时时间,当请求超过超时间时间还未处理,则会被丢弃。
测试案例:对/sentinel/annotation
资源进行直接限流,设置阈值为10,设置流控效果为排队等候,等待超时时间设置为1000ms。当每秒10次请求时,再有请求就排队等候,等待超时时间为1000ms, 超时过后,请求将被踢出排队队列,返回限流异常。
可以看到流量的增长趋势如下图所示: